Skip to content
1 change: 1 addition & 0 deletions JS/edgechains/arakoodev/src/vector-db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Supabase } from "./lib/supabase/supabase.js";
export { Qdrant } from "./lib/qdrant/qdrant.js";
274 changes: 274 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,274 @@
import retry from "retry";
import { config } from "dotenv";
config();

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

interface QdrantPoint {
id: QdrantPointId;
vector: QdrantVector;
payload?: QdrantPayload;
}

interface QdrantRequestArgs {
body?: any;
method?: "GET" | "POST" | "PUT" | "DELETE";
path: string;
query?: Record<string, string | number | boolean | undefined>;
}

interface CreateCollectionArgs {
collectionName: string;
distance?: "Cosine" | "Dot" | "Euclid" | "Manhattan";
onDisk?: boolean;
vectorName?: string;
vectorSize: number;
}

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

interface GetDataFromQueryArgs {
collectionName: string;
filter?: QdrantFilter;
limit?: number;
scoreThreshold?: number;
vector: QdrantVector;
withPayload?: boolean | string[] | Record<string, any>;
withVector?: boolean | string[];
}

interface GetDataArgs {
collectionName: string;
filter?: QdrantFilter;
limit?: number;
offset?: QdrantPointId | Record<string, any>;
withPayload?: boolean | string[] | Record<string, any>;
withVector?: boolean | string[];
}

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,
distance = "Cosine",
onDisk,
vectorName,
vectorSize,
}: CreateCollectionArgs): Promise<any> {
const vectorConfig = {
distance,
on_disk: onDisk,
size: vectorSize,
};

return this.request({
body: {
vectors: vectorName ? { [vectorName]: vectorConfig } : vectorConfig,
},
method: "PUT",
path: `/collections/${encodeURIComponent(collectionName)}`,
});
}

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

async insertVectorData({ collectionName, points, wait = true }: InsertVectorDataArgs): Promise<any> {
return this.request({
body: { points },
method: "PUT",
path: `/collections/${encodeURIComponent(collectionName)}/points`,
query: { wait },
});
}

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

async getData({
collectionName,
filter,
limit = 10,
offset,
withPayload = true,
withVector = false,
}: GetDataArgs): Promise<any> {
return this.request({
body: {
filter,
limit,
offset,
with_payload: withPayload,
with_vector: withVector,
},
method: "POST",
path: `/collections/${encodeURIComponent(collectionName)}/points/scroll`,
});
}

async getDataById({
collectionName,
id,
withPayload = true,
withVector = false,
}: {
collectionName: string;
id: QdrantPointId;
withPayload?: boolean | string[] | Record<string, any>;
withVector?: boolean | string[];
}): Promise<any> {
const result = await this.request({
body: {
ids: [id],
with_payload: withPayload,
with_vector: withVector,
},
method: "POST",
path: `/collections/${encodeURIComponent(collectionName)}/points`,
});

return Array.isArray(result) ? result[0] || null : result;
}

async updateById({
collectionName,
id,
payload,
wait = true,
}: {
collectionName: string;
id: QdrantPointId;
payload: QdrantPayload;
wait?: boolean;
}): Promise<any> {
return this.request({
body: {
payload,
points: [id],
},
method: "PUT",
path: `/collections/${encodeURIComponent(collectionName)}/points/payload`,
query: { wait },
});
}

async deleteById({
collectionName,
id,
wait = true,
}: {
collectionName: string;
id: QdrantPointId;
wait?: boolean;
}): Promise<any> {
return this.request({
body: { points: [id] },
method: "POST",
path: `/collections/${encodeURIComponent(collectionName)}/points/delete`,
query: { wait },
});
}

async request({ body, method = "GET", path, query }: QdrantRequestArgs): Promise<any> {
if (!this.QDRANT_URL) {
throw new Error("QDRANT_URL is required");
}

return new Promise((resolve, reject) => {
const operation = retry.operation({
factor: 3,
maxTimeout: 60 * 1000,
minTimeout: 1 * 1000,
randomize: true,
retries: 5,
});

operation.attempt(async () => {
try {
const response = await fetch(this.buildUrl(path, query), {
body: body === undefined ? undefined : JSON.stringify(body),
headers: this.createHeaders(body),
method,
});
const text = await response.text();
const data = text ? JSON.parse(text) : {};

if (!response.ok) {
const errorMessage = data?.status?.error || data?.message || response.statusText;
if (operation.retry(new Error(errorMessage))) {
return;
}
reject(new Error(`Qdrant request failed: ${errorMessage}`));
return;
}

resolve(data?.result ?? data);
} catch (error: any) {
if (operation.retry(error)) {
return;
}
reject(error);
}
});
});
}

private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>) {
const url = new URL(`${this.QDRANT_URL}${path}`);
Object.entries(query || {}).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}

private createHeaders(body: any) {
const headers: Record<string, string> = {};
if (body !== undefined) {
headers["content-type"] = "application/json";
}
if (this.QDRANT_API_KEY) {
headers["api-key"] = this.QDRANT_API_KEY;
}
return headers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { Qdrant } from "../../lib/qdrant/qdrant.js";

const createFetchResponse = (body: any, ok = true) =>
({
ok,
statusText: ok ? "OK" : "Bad Request",
text: vi.fn(async () => JSON.stringify(body)),
}) as any;

describe("Qdrant", () => {
beforeEach(() => {
vi.stubGlobal("fetch", vi.fn(async () => createFetchResponse({ result: { status: "ok" } })));
});

afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});

it("creates a collection through the Qdrant REST API", async () => {
const qdrant = new Qdrant("https://qdrant.example.com", "api-key");

await qdrant.createCollection({
collectionName: "documents",
distance: "Cosine",
vectorSize: 1536,
});

expect(fetch).toHaveBeenCalledWith("https://qdrant.example.com/collections/documents", {
body: JSON.stringify({
vectors: {
distance: "Cosine",
size: 1536,
},
}),
headers: {
"api-key": "api-key",
"content-type": "application/json",
},
method: "PUT",
});
});

it("inserts vector points without using a qdrant package", async () => {
const qdrant = new Qdrant("https://qdrant.example.com");

await qdrant.insertVectorData({
collectionName: "documents",
points: [
{
id: 1,
payload: { content: "hello" },
vector: [0.1, 0.2, 0.3],
},
],
});

expect(fetch).toHaveBeenCalledWith("https://qdrant.example.com/collections/documents/points?wait=true", {
body: JSON.stringify({
points: [
{
id: 1,
payload: { content: "hello" },
vector: [0.1, 0.2, 0.3],
},
],
}),
headers: {
"content-type": "application/json",
},
method: "PUT",
});
});

it("searches points with a vector query", async () => {
const qdrant = new Qdrant("https://qdrant.example.com");

await qdrant.getDataFromQuery({
collectionName: "documents",
limit: 3,
vector: [0.1, 0.2, 0.3],
});

expect(fetch).toHaveBeenCalledWith("https://qdrant.example.com/collections/documents/points/search", {
body: JSON.stringify({
limit: 3,
vector: [0.1, 0.2, 0.3],
with_payload: true,
with_vector: false,
}),
headers: {
"content-type": "application/json",
},
method: "POST",
});
});
});
Loading
Loading