From bec7f6b8d693140bb67ccc5115ae335ba7415801 Mon Sep 17 00:00:00 2001 From: xiaoxin112 <13514907+xiaoxin112@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:09:29 +0800 Subject: [PATCH] Add Qdrant vector database support --- .../arakoodev/src/vector-db/src/index.ts | 6 + .../src/vector-db/src/lib/qdrant/qdrant.ts | 184 ++++++++++++++++++ .../vector-db/src/tests/qdrant/qdrant.test.ts | 102 ++++++++++ .../examples/qdrant-vector-db/.env.example | 2 + .../examples/qdrant-vector-db/package.json | 22 +++ .../examples/qdrant-vector-db/readme.md | 31 +++ .../examples/qdrant-vector-db/src/index.ts | 49 +++++ .../examples/qdrant-vector-db/tsconfig.json | 14 ++ 8 files changed, 410 insertions(+) create mode 100644 JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts create mode 100644 JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts create mode 100644 JS/edgechains/examples/qdrant-vector-db/.env.example create mode 100644 JS/edgechains/examples/qdrant-vector-db/package.json create mode 100644 JS/edgechains/examples/qdrant-vector-db/readme.md create mode 100644 JS/edgechains/examples/qdrant-vector-db/src/index.ts create mode 100644 JS/edgechains/examples/qdrant-vector-db/tsconfig.json diff --git a/JS/edgechains/arakoodev/src/vector-db/src/index.ts b/JS/edgechains/arakoodev/src/vector-db/src/index.ts index 557104a14..52d8b3d14 100644 --- a/JS/edgechains/arakoodev/src/vector-db/src/index.ts +++ b/JS/edgechains/arakoodev/src/vector-db/src/index.ts @@ -1 +1,7 @@ export { Supabase } from "./lib/supabase/supabase.js"; +export { Qdrant } from "./lib/qdrant/qdrant.js"; +export type { + QdrantCollectionArgs, + QdrantPoint, + QdrantSearchArgs, +} from "./lib/qdrant/qdrant.js"; diff --git a/JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts b/JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts new file mode 100644 index 000000000..8f16719cb --- /dev/null +++ b/JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts @@ -0,0 +1,184 @@ +import retry from "retry"; +import { config } from "dotenv"; +config(); + +type QdrantPointId = number | string; +type QdrantVector = number[]; +type QdrantPayload = Record; + +export interface QdrantPoint { + id: QdrantPointId; + vector: QdrantVector | Record; + payload?: QdrantPayload; +} + +export interface QdrantSearchArgs { + collectionName: string; + vector: QdrantVector | Record; + limit?: number; + filter?: QdrantPayload; + withPayload?: boolean | string[]; + withVector?: boolean | string[]; + scoreThreshold?: number; + params?: QdrantPayload; +} + +export interface QdrantCollectionArgs { + collectionName: string; + vectors: QdrantPayload; +} + +export class Qdrant { + QDRANT_URL: string; + QDRANT_API_KEY?: string; + + constructor(QDRANT_URL?: string, QDRANT_API_KEY?: string) { + this.QDRANT_URL = (QDRANT_URL || process.env.QDRANT_URL || "").replace( + /\/$/, + "", + ); + this.QDRANT_API_KEY = QDRANT_API_KEY || process.env.QDRANT_API_KEY; + } + + private async request(path: string, init: RequestInit = {}): Promise { + if (!this.QDRANT_URL) { + throw new Error("QDRANT_URL is required"); + } + + return new Promise((resolve, reject) => { + const operation = retry.operation({ + retries: 5, + factor: 3, + minTimeout: 1 * 1000, + maxTimeout: 60 * 1000, + randomize: true, + }); + + operation.attempt(async () => { + try { + const headers: Record = { + "content-type": "application/json", + ...(init.headers as Record | undefined), + }; + + if (this.QDRANT_API_KEY) { + headers["api-key"] = this.QDRANT_API_KEY; + } + + const response = await fetch(`${this.QDRANT_URL}${path}`, { + ...init, + headers, + }); + const text = await response.text(); + const body = text ? JSON.parse(text) : {}; + + if (!response.ok) { + if (operation.retry(new Error())) return; + reject( + new Error( + `Qdrant request failed with status ${response.status}: ${text}`, + ), + ); + return; + } + + resolve(body as T); + } catch (error: any) { + if (operation.retry(error)) return; + reject(error); + } + }); + }); + } + + async createCollection({ + collectionName, + vectors, + }: QdrantCollectionArgs): Promise { + return this.request(`/collections/${collectionName}`, { + method: "PUT", + body: JSON.stringify({ vectors }), + }); + } + + async deleteCollection(collectionName: string): Promise { + return this.request(`/collections/${collectionName}`, { method: "DELETE" }); + } + + async upsertPoints({ + collectionName, + points, + wait = true, + }: { + collectionName: string; + points: QdrantPoint[]; + wait?: boolean; + }): Promise { + return this.request(`/collections/${collectionName}/points?wait=${wait}`, { + method: "PUT", + body: JSON.stringify({ points }), + }); + } + + async searchPoints({ + collectionName, + vector, + limit = 10, + filter, + withPayload = true, + withVector = false, + scoreThreshold, + params, + }: QdrantSearchArgs): Promise { + return this.request(`/collections/${collectionName}/points/search`, { + method: "POST", + body: JSON.stringify({ + vector, + limit, + filter, + with_payload: withPayload, + with_vector: withVector, + score_threshold: scoreThreshold, + params, + }), + }); + } + + async getPoint({ + collectionName, + id, + withPayload = true, + withVector = false, + }: { + collectionName: string; + id: QdrantPointId; + withPayload?: boolean | string[]; + withVector?: boolean | string[]; + }): Promise { + return this.request(`/collections/${collectionName}/points/${id}`, { + method: "POST", + body: JSON.stringify({ + with_payload: withPayload, + with_vector: withVector, + }), + }); + } + + async deletePoints({ + collectionName, + points, + wait = true, + }: { + collectionName: string; + points: QdrantPointId[]; + wait?: boolean; + }): Promise { + return this.request( + `/collections/${collectionName}/points/delete?wait=${wait}`, + { + method: "POST", + body: JSON.stringify({ points }), + }, + ); + } +} diff --git a/JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts b/JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts new file mode 100644 index 000000000..274115a35 --- /dev/null +++ b/JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts @@ -0,0 +1,102 @@ +import { Qdrant } from "../../../../../dist/vector-db/src/lib/qdrant/qdrant.js"; + +const MOCK_QDRANT_API_KEY = "mock-api-key"; +const MOCK_QDRANT_URL = "https://mock-qdrant.example"; + +describe("Qdrant", () => { + beforeEach(() => { + global.fetch = jest.fn(async () => ({ + ok: true, + status: 200, + text: async () => JSON.stringify({ result: "ok", status: "ok" }), + })) as jest.Mock; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should create a collection", async () => { + const qdrant = new Qdrant(MOCK_QDRANT_URL, MOCK_QDRANT_API_KEY); + + const result = await qdrant.createCollection({ + collectionName: "documents", + vectors: { size: 1536, distance: "Cosine" }, + }); + + expect(result).toEqual({ result: "ok", status: "ok" }); + expect(global.fetch).toHaveBeenCalledWith( + `${MOCK_QDRANT_URL}/collections/documents`, + expect.objectContaining({ + method: "PUT", + body: JSON.stringify({ vectors: { size: 1536, distance: "Cosine" } }), + }), + ); + }); + + it("should upsert vector points", async () => { + const qdrant = new Qdrant(MOCK_QDRANT_URL, MOCK_QDRANT_API_KEY); + + await qdrant.upsertPoints({ + collectionName: "documents", + points: [ + { + id: 1, + vector: [0.1, 0.2, 0.3], + payload: { content: "hello" }, + }, + ], + }); + + expect(global.fetch).toHaveBeenCalledWith( + `${MOCK_QDRANT_URL}/collections/documents/points?wait=true`, + expect.objectContaining({ + method: "PUT", + body: JSON.stringify({ + points: [ + { id: 1, vector: [0.1, 0.2, 0.3], payload: { content: "hello" } }, + ], + }), + }), + ); + }); + + it("should search vector points", async () => { + const qdrant = new Qdrant(MOCK_QDRANT_URL, MOCK_QDRANT_API_KEY); + + await qdrant.searchPoints({ + collectionName: "documents", + vector: [0.1, 0.2, 0.3], + limit: 5, + filter: { must: [{ key: "namespace", match: { value: "docs" } }] }, + }); + + expect(global.fetch).toHaveBeenCalledWith( + `${MOCK_QDRANT_URL}/collections/documents/points/search`, + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + vector: [0.1, 0.2, 0.3], + limit: 5, + filter: { must: [{ key: "namespace", match: { value: "docs" } }] }, + with_payload: true, + with_vector: false, + }), + }), + ); + }); + + it("should delete vector points", async () => { + const qdrant = new Qdrant(MOCK_QDRANT_URL, MOCK_QDRANT_API_KEY); + + await qdrant.deletePoints({ collectionName: "documents", points: [1, 2] }); + + expect(global.fetch).toHaveBeenCalledWith( + `${MOCK_QDRANT_URL}/collections/documents/points/delete?wait=true`, + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ points: [1, 2] }), + }), + ); + }); +}); diff --git a/JS/edgechains/examples/qdrant-vector-db/.env.example b/JS/edgechains/examples/qdrant-vector-db/.env.example new file mode 100644 index 000000000..aaf733265 --- /dev/null +++ b/JS/edgechains/examples/qdrant-vector-db/.env.example @@ -0,0 +1,2 @@ +QDRANT_URL=http://localhost:6333 +QDRANT_API_KEY= diff --git a/JS/edgechains/examples/qdrant-vector-db/package.json b/JS/edgechains/examples/qdrant-vector-db/package.json new file mode 100644 index 000000000..dfa4cf2b1 --- /dev/null +++ b/JS/edgechains/examples/qdrant-vector-db/package.json @@ -0,0 +1,22 @@ +{ + "name": "qdrant-vector-db", + "version": "1.0.0", + "description": "Qdrant vector database example for EdgeChains", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc -b", + "start": "tsc && node dist/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@arakoodev/edgechains.js": "^0.25.0", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^20.14.1", + "typescript": "^5.6.3" + } +} diff --git a/JS/edgechains/examples/qdrant-vector-db/readme.md b/JS/edgechains/examples/qdrant-vector-db/readme.md new file mode 100644 index 000000000..d516c39ab --- /dev/null +++ b/JS/edgechains/examples/qdrant-vector-db/readme.md @@ -0,0 +1,31 @@ +# Qdrant vector database example + +This example shows how to use the EdgeChains Qdrant vector database client. + +## Prerequisites + +Run Qdrant locally: + +```bash +docker run -p 6333:6333 qdrant/qdrant +``` + +Or point `QDRANT_URL` to a hosted Qdrant instance. + +## Setup + +```bash +cd JS/edgechains/examples/qdrant-vector-db +npm install +cp .env.example .env +npm run start +``` + +The example: + +1. Creates a `documents` collection. +2. Upserts two vector points with payloads. +3. Searches for the nearest vectors. +4. Deletes the inserted points. + +For Qdrant Cloud, set `QDRANT_API_KEY` in `.env`. diff --git a/JS/edgechains/examples/qdrant-vector-db/src/index.ts b/JS/edgechains/examples/qdrant-vector-db/src/index.ts new file mode 100644 index 000000000..e7070e719 --- /dev/null +++ b/JS/edgechains/examples/qdrant-vector-db/src/index.ts @@ -0,0 +1,49 @@ +import { config } from "dotenv"; +import { Qdrant } from "@arakoodev/edgechains.js/vector-db"; + +config(); + +const collectionName = "documents"; + +async function main() { + const qdrant = new Qdrant(process.env.QDRANT_URL, process.env.QDRANT_API_KEY); + + await qdrant.createCollection({ + collectionName, + vectors: { + size: 3, + distance: "Cosine", + }, + }); + + await qdrant.upsertPoints({ + collectionName, + points: [ + { + id: 1, + vector: [0.1, 0.2, 0.3], + payload: { content: "EdgeChains supports Qdrant" }, + }, + { + id: 2, + vector: [0.2, 0.1, 0.4], + payload: { content: "Qdrant stores vector points" }, + }, + ], + }); + + const result = await qdrant.searchPoints({ + collectionName, + vector: [0.1, 0.2, 0.31], + limit: 2, + }); + + console.log(JSON.stringify(result, null, 2)); + + await qdrant.deletePoints({ collectionName, points: [1, 2] }); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/JS/edgechains/examples/qdrant-vector-db/tsconfig.json b/JS/edgechains/examples/qdrant-vector-db/tsconfig.json new file mode 100644 index 000000000..f080302e8 --- /dev/null +++ b/JS/edgechains/examples/qdrant-vector-db/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"] +}