import { LocalIndex } from "vectra";
import path from "path";
import fs from "fs/promises";
export interface VectorDocument {
id: string;
content: string;
metadata?: Record<string, any>;
}
export interface SearchResult {
id: string;
score: number;
metadata?: Record<string, any>;
}
export class VectorStore {
private index!: LocalIndex;
private embedder: any;
private dataDir: string;
private indexPath: string;
private initialized = false;
constructor(dataDir: string) {
this.dataDir = dataDir;
this.indexPath = path.join(dataDir, "vectors.index");
}
async initialize(): Promise<void> {
// Ensure data directory exists
await fs.mkdir(this.dataDir, { recursive: true });
// Initialize the embedding model using dynamic import
const { pipeline } = await import("@xenova/transformers");
this.embedder = await pipeline(
"feature-extraction",
"Xenova/all-MiniLM-L6-v2"
);
// Check if index exists
try {
await fs.access(this.indexPath);
// Load existing index
this.index = new LocalIndex(this.indexPath);
} catch {
// Create new index
this.index = new LocalIndex(this.indexPath);
await this.index.createIndex();
}
this.initialized = true;
}
async add(document: VectorDocument): Promise<void> {
if (!this.initialized) {
throw new Error("VectorStore not initialized");
}
// Generate embedding for the content
const embedding = await this.generateEmbedding(document.content);
// Add to index
await this.index.insertItem({
vector: embedding,
metadata: {
id: document.id,
...document.metadata,
},
});
}
async search(
query: string,
limit: number = 5,
threshold: number = 0.7
): Promise<SearchResult[]> {
if (!this.initialized) {
throw new Error("VectorStore not initialized");
}
// Generate embedding for the query
const queryEmbedding = await this.generateEmbedding(query);
// Search the index - updated API call
const results = await this.index.queryItems(queryEmbedding, query, limit);
// Filter by threshold and format results
return results
.filter((result: any) => result.score >= threshold)
.map((result: any) => ({
id: result.item.metadata.id as string,
score: result.score,
metadata: result.item.metadata,
}));
}
async delete(id: string): Promise<boolean> {
if (!this.initialized) {
throw new Error("VectorStore not initialized");
}
// Find the item by id in metadata
const allItems = await this.index.listItems();
const itemToDelete = allItems.find((item: any) => item.metadata?.id === id);
if (itemToDelete) {
await this.index.deleteItem(itemToDelete.id);
return true;
}
return false;
}
async update(document: VectorDocument): Promise<void> {
// Delete existing and add new
await this.delete(document.id);
await this.add(document);
}
private async generateEmbedding(text: string): Promise<number[]> {
// Generate embeddings using the model
const output = await this.embedder(text, {
pooling: "mean",
normalize: true,
});
// Convert to array
return Array.from(output.data);
}
async close(): Promise<void> {
// Vectra handles cleanup automatically
this.initialized = false;
}
}