207 lines
4.9 KiB
TypeScript
207 lines
4.9 KiB
TypeScript
// src/services/vector-store.ts
|
|
// Copyright (C) 2025 DTP Technologies, LLC
|
|
// All Rights Reserved
|
|
|
|
import env from "../config/env.js";
|
|
import assert from "node:assert";
|
|
|
|
import { QdrantClient } from "@qdrant/js-client-rest";
|
|
|
|
import { DtpService } from "../lib/service.js";
|
|
import AiService from "./ai.js";
|
|
|
|
const VECTOR_DIMENSION = 768; //TODO: This should match the dimension of the embeddings from the AI provider
|
|
|
|
class VectorStoreService extends DtpService {
|
|
private qdrant?: QdrantClient;
|
|
|
|
get name(): string {
|
|
return "VectorStoreService";
|
|
}
|
|
|
|
get slug(): string {
|
|
return "vector-store";
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
this.log.info("Initializing Qdrant client");
|
|
this.qdrant = new QdrantClient({
|
|
url: env.qdrant.host,
|
|
});
|
|
|
|
this.log.info("service started");
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.log.info("service stopped");
|
|
}
|
|
|
|
async ensureCollection(name: string): Promise<void> {
|
|
assert(this.qdrant, "Qdrant client not initialized");
|
|
const collections = await this.qdrant.getCollections();
|
|
const exists = collections.collections.some((col) => col.name === name);
|
|
|
|
if (!exists) {
|
|
await this.qdrant.createCollection(name, {
|
|
vectors: {
|
|
size: VECTOR_DIMENSION,
|
|
distance: "Cosine",
|
|
},
|
|
});
|
|
this.log.info("Qdrant collection initialized", { name });
|
|
} else {
|
|
this.log.info("Qdrant collection already exists", { name });
|
|
}
|
|
}
|
|
|
|
private async getEmbedding(
|
|
userId: string,
|
|
content: string,
|
|
): Promise<number[]> {
|
|
const client = await AiService.getVectorClient(userId);
|
|
const model = await AiService.getVectorModel(userId);
|
|
const response = await client.embeddings(content, model);
|
|
return response.embedding;
|
|
}
|
|
|
|
async addDocument(
|
|
userId: string,
|
|
collectionName: string,
|
|
id: string,
|
|
content: string,
|
|
metadata?: any,
|
|
) {
|
|
assert(this.qdrant, "Qdrant client not initialized");
|
|
try {
|
|
await this.ensureCollection(collectionName);
|
|
|
|
const embedding = await this.getEmbedding(userId, content);
|
|
|
|
await this.qdrant.upsert(collectionName, {
|
|
points: [
|
|
{
|
|
id,
|
|
vector: embedding,
|
|
payload: {
|
|
content,
|
|
metadata: metadata || {},
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
this.log.info(`Document added to Qdrant`, {
|
|
collectionName,
|
|
id,
|
|
content,
|
|
metadata,
|
|
});
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
this.log.error("Error adding document to Qdrant", {
|
|
error: errorMessage,
|
|
collectionName,
|
|
id,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async search(
|
|
userId: string,
|
|
collectionName: string,
|
|
query: string,
|
|
topK: number = 5,
|
|
filter?: Record<string, unknown>,
|
|
) {
|
|
assert(this.qdrant, "Qdrant client not initialized");
|
|
try {
|
|
const queryVector = await this.getEmbedding(userId, query);
|
|
|
|
const searchOptions: any = {
|
|
vector: queryVector,
|
|
limit: topK,
|
|
with_payload: true,
|
|
with_vector: false,
|
|
};
|
|
|
|
if (filter) {
|
|
searchOptions.filter = {
|
|
must: Object.entries(filter).map(([key, value]) => ({
|
|
key,
|
|
match: { value },
|
|
})),
|
|
};
|
|
}
|
|
|
|
const results = await this.qdrant.search(collectionName, searchOptions);
|
|
|
|
this.log.debug("Vector search completed", {
|
|
collectionName,
|
|
query,
|
|
resultCount: results.length,
|
|
});
|
|
|
|
return results.map((result: any) => ({
|
|
id: result.id,
|
|
content: result.payload?.content,
|
|
metadata: result.payload?.metadata,
|
|
score: result.score,
|
|
}));
|
|
} catch (error) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : String(error);
|
|
this.log.error("Error searching in Qdrant", {
|
|
error: errorMessage,
|
|
collectionName,
|
|
query,
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async searchWithContext(
|
|
userId: string,
|
|
collectionName: string,
|
|
query: string,
|
|
topK: number = 5,
|
|
): Promise<string> {
|
|
const results = await this.search(
|
|
userId,
|
|
collectionName,
|
|
query,
|
|
topK,
|
|
undefined,
|
|
);
|
|
return results
|
|
.map(
|
|
(result) => `Document ID: ${result.id}\nContent: ${result.content}\n`,
|
|
)
|
|
.join("\n---\n");
|
|
}
|
|
|
|
async removeDocument(collectionName: string, id: string) {
|
|
assert(this.qdrant, "Qdrant client not initialized");
|
|
await this.qdrant.delete(collectionName, {
|
|
filter: {
|
|
must: [
|
|
{
|
|
key: "id",
|
|
match: {
|
|
value: id,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
this.log.info("Document removed from Qdrant", {
|
|
collectionName,
|
|
id,
|
|
});
|
|
}
|
|
}
|
|
|
|
export default new VectorStoreService();
|