/** * Embedding + Qdrant client for vector search. * * Ollama nomic-embed-text (768 dim) → Qdrant collections. * Supports: products, datasheets, FAQs, manuals, troubleshooting, news. */ const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333"; const EMBED_MODEL = process.env.EMBED_MODEL || "nomic-embed-text"; export type CollectionName = | "product_embeddings" | "datasheet_chunks" | "faq_embeddings" | "manual_chunks" | "troubleshooting_embeddings" | "news_embeddings"; /** Generate embedding vector from text */ export async function embed(text: string): Promise { const resp = await fetch(`${OLLAMA_URL}/api/embeddings`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: EMBED_MODEL, prompt: text }), signal: AbortSignal.timeout(30000), }); if (!resp.ok) { throw new Error(`Ollama embed failed: ${resp.status} ${await resp.text()}`); } const data = await resp.json() as { embedding: number[] }; return data.embedding; } /** Batch embed multiple texts */ export async function embedBatch(texts: ReadonlyArray): Promise { const results: number[][] = []; // Ollama doesn't support batch embedding natively, so we serialize // with concurrency limit to avoid overloading const CONCURRENCY = 3; for (let i = 0; i < texts.length; i += CONCURRENCY) { const batch = texts.slice(i, i + CONCURRENCY); const embeddings = await Promise.all(batch.map((t) => embed(t))); results.push(...embeddings); } return results; } /** Upsert a point into Qdrant */ export async function upsertPoint( collection: CollectionName, id: string, vector: number[], payload: Record, ): Promise { const resp = await fetch(`${QDRANT_URL}/collections/${collection}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: [{ id, vector, payload }], }), signal: AbortSignal.timeout(10000), }); if (!resp.ok) { throw new Error(`Qdrant upsert failed: ${resp.status} ${await resp.text()}`); } } /** Batch upsert points */ export async function upsertPoints( collection: CollectionName, points: ReadonlyArray<{ id: string; vector: number[]; payload: Record }>, ): Promise { if (points.length === 0) return; const resp = await fetch(`${QDRANT_URL}/collections/${collection}/points`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points }), signal: AbortSignal.timeout(30000), }); if (!resp.ok) { throw new Error(`Qdrant batch upsert failed: ${resp.status} ${await resp.text()}`); } } /** Search Qdrant with vector similarity + optional payload filter */ export async function searchSimilar( collection: CollectionName, queryVector: number[], limit: number = 10, filter?: Record, ): Promise }>> { const body: Record = { vector: queryVector, limit, with_payload: true, }; if (filter) { body.filter = filter; } const resp = await fetch(`${QDRANT_URL}/collections/${collection}/points/search`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(10000), }); if (!resp.ok) { throw new Error(`Qdrant search failed: ${resp.status} ${await resp.text()}`); } const data = await resp.json() as { result: Array<{ id: string; score: number; payload: Record }> }; return data.result; } /** Semantic search: embed query text then search Qdrant */ export async function semanticSearch( collection: CollectionName, query: string, limit: number = 10, filter?: Record, ): Promise }>> { const vector = await embed(query); return searchSimilar(collection, vector, limit, filter); } /** Get collection info (point count, etc.) */ export async function getCollectionInfo( collection: CollectionName, ): Promise<{ pointsCount: number; vectorsCount: number }> { const resp = await fetch(`${QDRANT_URL}/collections/${collection}`, { signal: AbortSignal.timeout(5000), }); if (!resp.ok) { throw new Error(`Qdrant info failed: ${resp.status}`); } const data = await resp.json() as { result: { points_count: number; vectors_count: number } }; return { pointsCount: data.result.points_count, vectorsCount: data.result.vectors_count, }; }