Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
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<number[]> {
|
|
// 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<Array<{ id: string; articleId: string; distance: number; title: string; slug: string }>>`
|
|
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;
|
|
}
|