import { prisma } from '../db.js'; // Simulated embedding generation for Phase 14 demo // In production, this would call OpenAI text-embedding-3-small or similar export async function generateEmbedding(text: string): Promise { // Deterministic-ish random for demo purposes based on text length const vector = new Array(768).fill(0).map((_, i) => Math.sin(text.length * i)); // Normalize vector const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); return vector.map(val => val / magnitude); } export async function storeArticleEmbedding(articleId: string, text: string) { const vector = await generateEmbedding(text); // Format for pgvector: '[0.1, 0.2, ...]' const vectorString = `[${vector.join(',')}]`; await prisma.$executeRaw` INSERT INTO "ArticleEmbedding" ("id", "articleId", "embedding", "updatedAt") VALUES (gen_random_uuid(), ${articleId}, ${vectorString}::vector, NOW()) ON CONFLICT ("articleId") DO UPDATE SET "embedding" = EXCLUDED."embedding", "updatedAt" = NOW(); `; } export async function semanticSearch(query: string, limit = 5) { const queryVector = await generateEmbedding(query); const vectorString = `[${queryVector.join(',')}]`; // Cosine distance operator <=> in pgvector const results = await prisma.$queryRaw>` SELECT e."id", e."articleId", (e."embedding" <=> ${vectorString}::vector) as distance, a."title", a."slug" FROM "ArticleEmbedding" e JOIN "Article" a ON e."articleId" = a."id" ORDER BY distance ASC LIMIT ${limit}; `; return results; }