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
10 changes: 10 additions & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
export { Supabase } from "./lib/supabase/supabase.js";
export {
Qdrant,
QdrantDistanceMetric,
type QdrantCreateCollectionArgs,
type QdrantDeletePointsArgs,
type QdrantPoint,
type QdrantSearchPointsArgs,
type QdrantSearchResult,
type QdrantUpsertPointsArgs,
} from "./lib/qdrant/qdrant.js";
155 changes: 155 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,155 @@
export interface QdrantPoint {
id: string | number;
vector: number[];
payload?: Record<string, unknown>;
}

export interface QdrantSearchResult {
id: string | number;
score: number;
payload?: Record<string, unknown>;
vector?: number[];
}

export interface QdrantCreateCollectionArgs {
collectionName: string;
vectorSize: number;
distance?: QdrantDistanceMetric;
}

export interface QdrantUpsertPointsArgs {
collectionName: string;
points: QdrantPoint[];
wait?: boolean;
}

export interface QdrantSearchPointsArgs {
collectionName: string;
vector: number[];
limit?: number;
filter?: Record<string, unknown>;
withPayload?: boolean | string[];
withVector?: boolean | string[];
scoreThreshold?: number;
}

export interface QdrantDeletePointsArgs {
collectionName: string;
points: Array<string | number>;
wait?: boolean;
}

export enum QdrantDistanceMetric {
COSINE = "Cosine",
DOT = "Dot",
EUCLID = "Euclid",
MANHATTAN = "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;
}

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

async upsertPoints({
collectionName,
points,
wait = true,
}: QdrantUpsertPointsArgs): Promise<any> {
return this.request(`/collections/${collectionName}/points?wait=${wait}`, {
method: "PUT",
body: { points },
});
}

async searchPoints({
collectionName,
vector,
limit = 10,
filter,
withPayload = true,
withVector = false,
scoreThreshold,
}: QdrantSearchPointsArgs): Promise<QdrantSearchResult[]> {
const response = await this.request(`/collections/${collectionName}/points/search`, {
method: "POST",
body: {
vector,
limit,
filter,
with_payload: withPayload,
with_vector: withVector,
score_threshold: scoreThreshold,
},
});

return response.result;
}

async deletePoints({
collectionName,
points,
wait = true,
}: QdrantDeletePointsArgs): Promise<any> {
return this.request(`/collections/${collectionName}/points/delete?wait=${wait}`, {
method: "POST",
body: {
points,
},
});
}

private async request(path: string, options: { method: string; body?: unknown }): Promise<any> {
if (!this.QDRANT_URL) {
throw new Error("QDRANT_URL is required");
}

const response = await fetch(`${this.QDRANT_URL}${path}`, {
method: options.method,
headers: this.headers(),
body: options.body ? JSON.stringify(options.body) : undefined,
});

const responseBody = await response.json().catch(() => null);

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

return responseBody;
}

private headers(): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};

if (this.QDRANT_API_KEY) {
headers["api-key"] = this.QDRANT_API_KEY;
}

return headers;
}
}
138 changes: 138 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,138 @@
import { Qdrant, QdrantDistanceMetric } from "../../lib/qdrant/qdrant.js";
import { afterEach, describe, expect, it, vi } from "vitest";

describe("Qdrant", () => {
const originalFetch = global.fetch;

afterEach(() => {
global.fetch = originalFetch;
});

it("creates a collection using the Qdrant HTTP API", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ result: true }),
});
global.fetch = fetchMock;

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

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

it("upserts points without using a Qdrant package", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ result: { operation_id: 1 } }),
});
global.fetch = fetchMock;

const qdrant = new Qdrant("http://localhost:6333");
await qdrant.upsertPoints({
collectionName: "documents",
points: [
{
id: 1,
vector: [0.1, 0.2, 0.3],
payload: { raw_text: "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: { raw_text: "hello" },
},
],
}),
})
);
});

it("returns search results from Qdrant", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
result: [
{
id: 1,
score: 0.98,
payload: { raw_text: "matched document" },
},
],
}),
});
global.fetch = fetchMock;

const qdrant = new Qdrant("http://localhost:6333");
const result = await qdrant.searchPoints({
collectionName: "documents",
vector: [0.1, 0.2, 0.3],
limit: 3,
});

expect(result).toEqual([
{
id: 1,
score: 0.98,
payload: { raw_text: "matched document" },
},
]);
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,
with_payload: true,
with_vector: false,
}),
})
);
});

it("throws an error when Qdrant returns a failed response", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
json: async () => ({ status: { error: "Collection not found" } }),
});

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

await expect(
qdrant.searchPoints({
collectionName: "missing",
vector: [0.1],
})
).rejects.toThrow("Qdrant request failed with status 404");
});
});
Loading