From 608aa3e79184b6bce532af48845ab9789e91dc82 Mon Sep 17 00:00:00 2001 From: HunterCML <5335527+HunterCML@users.noreply.github.com> Date: Mon, 11 May 2026 07:47:13 -0500 Subject: [PATCH] Add Qdrant vector database client --- .../arakoodev/src/vector-db/src/index.ts | 1 + .../src/vector-db/src/lib/qdrant/qdrant.ts | 155 ++++++++++++++++++ .../vector-db/src/tests/qdrant/qdrant.test.ts | 123 ++++++++++++++ 3 files changed, 279 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 diff --git a/JS/edgechains/arakoodev/src/vector-db/src/index.ts b/JS/edgechains/arakoodev/src/vector-db/src/index.ts index 557104a14..e68d60f2d 100644 --- a/JS/edgechains/arakoodev/src/vector-db/src/index.ts +++ b/JS/edgechains/arakoodev/src/vector-db/src/index.ts @@ -1 +1,2 @@ export { Supabase } from "./lib/supabase/supabase.js"; +export { Qdrant } 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..dcc60ce20 --- /dev/null +++ b/JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts @@ -0,0 +1,155 @@ +interface QdrantConstructionOptions { + url?: string; + apiKey?: string; +} + +interface QdrantVectorParams { + size: number; + distance: "Cosine" | "Euclid" | "Dot" | "Manhattan"; +} + +interface QdrantPoint { + id: string | number; + vector: number[]; + payload?: Record; +} + +interface QdrantSearchArgs { + collectionName: string; + vector: number[]; + limit?: number; + filter?: Record; + withPayload?: boolean | string[] | Record; + scoreThreshold?: number; +} + +interface QdrantRequestOptions { + method?: string; + body?: Record; +} + +interface QdrantClient { + request(path: string, options?: QdrantRequestOptions): Promise; +} + +export class Qdrant { + QDRANT_URL: string; + QDRANT_API_KEY?: string; + + constructor(options: QdrantConstructionOptions = {}) { + this.QDRANT_URL = (options.url || process.env.QDRANT_URL || "http://localhost:6333").replace( + /\/$/, + "" + ); + this.QDRANT_API_KEY = options.apiKey || process.env.QDRANT_API_KEY; + } + + createClient(): QdrantClient { + return { + request: async (path: string, options: QdrantRequestOptions = {}) => { + const response = await fetch(`${this.QDRANT_URL}${path}`, { + method: options.method || "GET", + headers: { + "Content-Type": "application/json", + ...(this.QDRANT_API_KEY ? { "api-key": this.QDRANT_API_KEY } : {}), + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }); + + const data = await response.json().catch(() => undefined); + if (!response.ok) { + throw new Error( + `Qdrant request failed with status ${response.status}: ${JSON.stringify(data)}` + ); + } + return data; + }, + }; + } + + async createCollection({ + client, + collectionName, + vectors, + }: { + client: QdrantClient; + collectionName: string; + vectors: QdrantVectorParams; + }): Promise { + return client.request(`/collections/${collectionName}`, { + method: "PUT", + body: { vectors }, + }); + } + + async getCollectionInfo({ + client, + collectionName, + }: { + client: QdrantClient; + collectionName: string; + }): Promise { + return client.request(`/collections/${collectionName}`); + } + + async insertVectorData({ + client, + collectionName, + points, + }: { + client: QdrantClient; + collectionName: string; + points: QdrantPoint[]; + }): Promise { + return client.request(`/collections/${collectionName}/points`, { + method: "PUT", + body: { points }, + }); + } + + async getDataFromQuery({ + client, + collectionName, + vector, + limit = 10, + filter, + withPayload = true, + scoreThreshold, + }: QdrantSearchArgs & { client: QdrantClient }): Promise { + return client.request(`/collections/${collectionName}/points/search`, { + method: "POST", + body: { + vector, + limit, + filter, + with_payload: withPayload, + score_threshold: scoreThreshold, + }, + }); + } + + async deleteById({ + client, + collectionName, + points, + }: { + client: QdrantClient; + collectionName: string; + points: Array; + }): Promise { + return client.request(`/collections/${collectionName}/points/delete`, { + method: "POST", + body: { points }, + }); + } + + async deleteCollection({ + client, + collectionName, + }: { + client: QdrantClient; + collectionName: string; + }): Promise { + return client.request(`/collections/${collectionName}`, { method: "DELETE" }); + } +} 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..ad4631099 --- /dev/null +++ b/JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts @@ -0,0 +1,123 @@ +import { Qdrant } from "../../../../../dist/vector-db/src/lib/qdrant/qdrant.js"; + +describe("Qdrant", () => { + const mockClient = { + request: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("creates a fetch-backed client configured for Qdrant", async () => { + const fetchMock = jest.fn().mockResolvedValue({ + ok: true, + json: async () => ({ result: true }), + }); + global.fetch = fetchMock as any; + const qdrant = new Qdrant({ + url: "https://qdrant.example.com/", + apiKey: "test-api-key", + }); + + const client = qdrant.createClient(); + await client.request("/collections/documents", { method: "GET" }); + + expect(fetchMock).toHaveBeenCalledWith("https://qdrant.example.com/collections/documents", { + method: "GET", + headers: { + "Content-Type": "application/json", + "api-key": "test-api-key", + }, + body: undefined, + }); + }); + + it("creates a collection", async () => { + mockClient.request.mockResolvedValueOnce({ result: true }); + const qdrant = new Qdrant(); + + const result = await qdrant.createCollection({ + client: mockClient as any, + collectionName: "documents", + vectors: { size: 1536, distance: "Cosine" }, + }); + + expect(mockClient.request).toHaveBeenCalledWith("/collections/documents", { + method: "PUT", + body: { vectors: { size: 1536, distance: "Cosine" } }, + }); + expect(result).toEqual({ result: true }); + }); + + it("upserts vector points", async () => { + mockClient.request.mockResolvedValueOnce({ status: "ok" }); + const qdrant = new Qdrant(); + + await qdrant.insertVectorData({ + client: mockClient as any, + collectionName: "documents", + points: [ + { + id: 1, + vector: [0.1, 0.2, 0.3], + payload: { content: "hello" }, + }, + ], + }); + + expect(mockClient.request).toHaveBeenCalledWith("/collections/documents/points", { + method: "PUT", + body: { + points: [ + { + id: 1, + vector: [0.1, 0.2, 0.3], + payload: { content: "hello" }, + }, + ], + }, + }); + }); + + it("searches vector points", async () => { + mockClient.request.mockResolvedValueOnce({ result: [{ id: 1, score: 0.9 }] }); + const qdrant = new Qdrant(); + + const result = await qdrant.getDataFromQuery({ + client: mockClient as any, + collectionName: "documents", + vector: [0.1, 0.2, 0.3], + limit: 3, + filter: { must: [{ key: "type", match: { value: "note" } }] }, + }); + + expect(mockClient.request).toHaveBeenCalledWith("/collections/documents/points/search", { + method: "POST", + body: { + vector: [0.1, 0.2, 0.3], + limit: 3, + filter: { must: [{ key: "type", match: { value: "note" } }] }, + with_payload: true, + score_threshold: undefined, + }, + }); + expect(result).toEqual({ result: [{ id: 1, score: 0.9 }] }); + }); + + it("deletes vector points by id", async () => { + mockClient.request.mockResolvedValueOnce({ status: "acknowledged" }); + const qdrant = new Qdrant(); + + await qdrant.deleteById({ + client: mockClient as any, + collectionName: "documents", + points: [1, "doc-2"], + }); + + expect(mockClient.request).toHaveBeenCalledWith("/collections/documents/points/delete", { + method: "POST", + body: { points: [1, "doc-2"] }, + }); + }); +});