feat: migrate semantic search to pgvector + full-text search
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m12s
Replace JSON-string embeddings with native pgvector(1536) storage and add PostgreSQL full-text search (tsvector/GIN) with Reciprocal Rank Fusion for hybrid keyword + semantic ranking. Changes: - NoteEmbedding.embedding: String → vector(1536) via pgvector - NoteEmbedding: added updatedAt for reindex tracking - Note: added tsv (tsvector) with auto-update trigger for FTS - semantic-search.service: hybrid FTS + vector search with RRF fusion - embedding.service: toVectorString() for pgvector SQL literals - Removed JS-side cosine similarity loops (now DB-side via <=>) - Added HNSW index on NoteEmbedding.embedding (cosine distance) - Added GIN index on Note.tsv for FTS queries Schema migration in: prisma/migrations/20260512120000_pgvector_and_fts_search/ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -538,21 +538,25 @@ export function registerTools(server, prisma) {
|
||||
}
|
||||
|
||||
case 'search_notes': {
|
||||
const where = noteWhere(uid, {
|
||||
isArchived: args.includeArchived || false,
|
||||
OR: [
|
||||
{ title: { contains: args.query } },
|
||||
{ content: { contains: args.query } },
|
||||
],
|
||||
...(args.notebookId ? { notebookId: args.notebookId } : {}),
|
||||
});
|
||||
const safeQuery = (args.query || '').replace(/'/g, "''");
|
||||
const userClause = uid ? `AND "userId" = '${uid}'` : '';
|
||||
const notebookClause = args.notebookId ? `AND "notebookId" = '${args.notebookId.replace(/'/g, "''")}'` : '';
|
||||
|
||||
const notes = await prisma.note.findMany({
|
||||
where,
|
||||
orderBy: [{ isPinned: 'desc' }, { updatedAt: 'desc' }],
|
||||
take: DEFAULT_SEARCH_LIMIT,
|
||||
});
|
||||
return textResult(notes.map(parseNoteLightweight));
|
||||
const ftsRows = await prisma.$queryRawUnsafe(`
|
||||
SELECT id, title, content, color, type, "isPinned", "isArchived",
|
||||
"isMarkdown", size, "createdAt", "updatedAt", "notebookId",
|
||||
images, labels, "checkItems", reminder, "isReminderDone"
|
||||
FROM "Note"
|
||||
WHERE "tsv" @@ plainto_tsquery('simple', '${safeQuery}')
|
||||
AND "trashedAt" IS NULL
|
||||
AND "isArchived" = ${args.includeArchived ? 'true' : 'false'}
|
||||
${userClause}
|
||||
${notebookClause}
|
||||
ORDER BY ts_rank("tsv", plainto_tsquery('simple', '${safeQuery}')) DESC
|
||||
LIMIT ${DEFAULT_SEARCH_LIMIT}
|
||||
`);
|
||||
|
||||
return textResult(ftsRows.map(parseNoteLightweight));
|
||||
}
|
||||
|
||||
case 'move_note': {
|
||||
|
||||
Reference in New Issue
Block a user