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
6 changes: 6 additions & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
184 changes: 184 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,184 @@
import retry from "retry";
import { config } from "dotenv";
config();

type QdrantPointId = number | string;
type QdrantVector = number[];
type QdrantPayload = Record<string, any>;

export interface QdrantPoint {
id: QdrantPointId;
vector: QdrantVector | Record<string, QdrantVector>;
payload?: QdrantPayload;
}

export interface QdrantSearchArgs {
collectionName: string;
vector: QdrantVector | Record<string, QdrantVector>;
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<T>(path: string, init: RequestInit = {}): Promise<T> {
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<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 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<any> {
return this.request(`/collections/${collectionName}`, {
method: "PUT",
body: JSON.stringify({ vectors }),
});
}

async deleteCollection(collectionName: string): Promise<any> {
return this.request(`/collections/${collectionName}`, { method: "DELETE" });
}

async upsertPoints({
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 searchPoints({
collectionName,
vector,
limit = 10,
filter,
withPayload = true,
withVector = false,
scoreThreshold,
params,
}: QdrantSearchArgs): Promise<any> {
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<any> {
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<any> {
return this.request(
`/collections/${collectionName}/points/delete?wait=${wait}`,
{
method: "POST",
body: JSON.stringify({ points }),
},
);
}
}
102 changes: 102 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,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] }),
}),
);
});
});
2 changes: 2 additions & 0 deletions JS/edgechains/examples/qdrant-vector-db/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=
22 changes: 22 additions & 0 deletions JS/edgechains/examples/qdrant-vector-db/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
31 changes: 31 additions & 0 deletions JS/edgechains/examples/qdrant-vector-db/readme.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading
Loading