Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { Supabase } from "./lib/supabase/supabase.js";
export { Qdrant } from "./lib/qdrant/qdrant.js";
export type { QdrantCollectionArgs, QdrantPoint, QdrantSearchArgs } from "./lib/qdrant/qdrant.js";
152 changes: 152 additions & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/lib/qdrant/qdrant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
type QdrantPayload = Record<string, unknown>;

export interface QdrantPoint {
id: string | number;
vector: number[];
payload?: QdrantPayload;
}

export interface QdrantSearchArgs {
collectionName: string;
vector: number[];
limit?: number;
filter?: QdrantPayload;
withPayload?: boolean | QdrantPayload;
withVector?: boolean;
}

export interface QdrantCollectionArgs {
collectionName: string;
vectorSize: number;
distance?: "Cosine" | "Dot" | "Euclid" | "Manhattan";
}

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;

if (!this.QDRANT_URL) {
throw new Error("QDRANT_URL is required");
}
}

private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(init.headers as Record<string, string> | 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 data = text ? JSON.parse(text) : {};

if (!response.ok) {
throw new Error(
`Qdrant request failed with ${response.status}: ${JSON.stringify(data)}`
);
}

return data as T;
}

async createCollection({
collectionName,
vectorSize,
distance = "Cosine",
}: QdrantCollectionArgs): Promise<any> {
return this.request(`/collections/${collectionName}`, {
method: "PUT",
body: JSON.stringify({
vectors: {
size: vectorSize,
distance,
},
}),
});
}

async insertVectorData({
collectionName,
points,
wait = true,
}: {
collectionName: string;
points: QdrantPoint[];
wait?: boolean;
}): Promise<any> {
return this.request(`/collections/${collectionName}/points?wait=${wait}`, {
method: "PUT",
body: JSON.stringify({ points }),
});
}

async getDataById({
collectionName,
ids,
withPayload = true,
withVector = false,
}: {
collectionName: string;
ids: Array<string | number>;
withPayload?: boolean | QdrantPayload;
withVector?: boolean;
}): Promise<any> {
return this.request(`/collections/${collectionName}/points`, {
method: "POST",
body: JSON.stringify({
ids,
with_payload: withPayload,
with_vector: withVector,
}),
});
}

async getDataFromQuery({
collectionName,
vector,
limit = 10,
filter,
withPayload = true,
withVector = false,
}: QdrantSearchArgs): Promise<any> {
return this.request(`/collections/${collectionName}/points/search`, {
method: "POST",
body: JSON.stringify({
vector,
limit,
filter,
with_payload: withPayload,
with_vector: withVector,
}),
});
}

async deleteById({
collectionName,
ids,
wait = true,
}: {
collectionName: string;
ids: Array<string | number>;
wait?: boolean;
}): Promise<any> {
return this.request(`/collections/${collectionName}/points/delete?wait=${wait}`, {
method: "POST",
body: JSON.stringify({
points: ids,
}),
});
}
}
122 changes: 122 additions & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/tests/qdrant/qdrant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Qdrant } from "../../lib/qdrant/qdrant.js";

const jsonResponse = (body: unknown, ok = true, status = 200) =>
({
ok,
status,
text: async () => JSON.stringify(body),
}) as Response;

describe("Qdrant", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it("creates a collection using the Qdrant REST API", async () => {
const fetchMock = jest
.spyOn(global, "fetch")
.mockResolvedValue(jsonResponse({ result: true }));

const qdrant = new Qdrant("http://localhost:6333", "test-key");
await qdrant.createCollection({
collectionName: "documents",
vectorSize: 1536,
});

expect(fetchMock).toHaveBeenCalledWith(
"http://localhost:6333/collections/documents",
expect.objectContaining({
method: "PUT",
headers: expect.objectContaining({
"Content-Type": "application/json",
"api-key": "test-key",
}),
body: JSON.stringify({
vectors: {
size: 1536,
distance: "Cosine",
},
}),
})
);
});

it("upserts vector points", async () => {
const fetchMock = jest
.spyOn(global, "fetch")
.mockResolvedValue(jsonResponse({ result: { operation_id: 1 } }));

const qdrant = new Qdrant("http://localhost:6333");
await qdrant.insertVectorData({
collectionName: "documents",
points: [
{
id: 1,
vector: [0.1, 0.2, 0.3],
payload: { content: "hello" },
},
],
});

expect(fetchMock).toHaveBeenCalledWith(
"http://localhost:6333/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("searches vectors with filters", async () => {
const fetchMock = jest.spyOn(global, "fetch").mockResolvedValue(
jsonResponse({
result: [{ id: 1, score: 0.9, payload: { content: "hello" } }],
})
);

const qdrant = new Qdrant("http://localhost:6333");
await qdrant.getDataFromQuery({
collectionName: "documents",
vector: [0.1, 0.2, 0.3],
limit: 3,
filter: { must: [{ key: "source", match: { value: "docs" } }] },
});

expect(fetchMock).toHaveBeenCalledWith(
"http://localhost:6333/collections/documents/points/search",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
vector: [0.1, 0.2, 0.3],
limit: 3,
filter: { must: [{ key: "source", match: { value: "docs" } }] },
with_payload: true,
with_vector: false,
}),
})
);
});

it("throws readable errors from Qdrant", async () => {
jest.spyOn(global, "fetch").mockResolvedValue(
jsonResponse({ status: { error: "missing collection" } }, false, 404)
);

const qdrant = new Qdrant("http://localhost:6333");

await expect(
qdrant.getDataById({
collectionName: "missing",
ids: [1],
})
).rejects.toThrow("Qdrant request failed with 404");
});
});
30 changes: 30 additions & 0 deletions JS/edgechains/examples/qdrant-vector-search/jsonnet/main.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
qdrant: {
url: std.extVar("QDRANT_URL"),
apiKey: std.extVar("QDRANT_API_KEY"),
collectionName: "edgechains_qdrant_example",
vectorSize: 3,
},
documents: [
{
id: 1,
vector: [0.1, 0.2, 0.3],
payload: {
content: "EdgeChains can query Qdrant using the REST API.",
source: "docs",
},
},
{
id: 2,
vector: [0.2, 0.1, 0.4],
payload: {
content: "Qdrant stores vectors and metadata payloads.",
source: "docs",
},
},
],
query: {
vector: [0.1, 0.2, 0.3],
limit: 2,
},
}
12 changes: 12 additions & 0 deletions JS/edgechains/examples/qdrant-vector-search/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "qdrant-vector-search",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts"
},
"dependencies": {
"@arakoodev/edgechains.js": "file:../../arakoodev",
"tsx": "^4.7.2"
}
}
27 changes: 27 additions & 0 deletions JS/edgechains/examples/qdrant-vector-search/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Qdrant Vector Search

This example demonstrates using EdgeChains' Qdrant vector database client.

The client talks to Qdrant through the REST API directly. It does not require a
Qdrant npm package.

## Run

Start Qdrant locally:

```sh
docker run -p 6333:6333 qdrant/qdrant
```

Then run:

```sh
export QDRANT_URL=http://localhost:6333
export QDRANT_API_KEY=
export QDRANT_COLLECTION=edgechains_qdrant_example
bun install
bun run start
```

The example creates a collection, upserts two points, and searches for the
closest matches with a payload filter.
Loading
Loading