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>
45 lines
1.6 KiB
TypeScript
45 lines
1.6 KiB
TypeScript
/**
|
|
* Note Search Tool
|
|
* Uses the unified SemanticSearchService (FTS + pgvector + RRF).
|
|
*/
|
|
|
|
import { tool } from 'ai'
|
|
import { z } from 'zod'
|
|
import { toolRegistry } from './registry'
|
|
import { semanticSearchService } from '@/lib/ai/services/semantic-search.service'
|
|
|
|
toolRegistry.register({
|
|
name: 'note_search',
|
|
description: 'Search the user\'s notes using hybrid semantic + keyword search. Returns matching notes with titles and content excerpts.',
|
|
isInternal: true,
|
|
buildTool: (ctx) =>
|
|
tool({
|
|
description: 'Search the user\'s notes by keyword or semantic meaning. Returns matching notes with titles and content excerpts. Optionally restrict to a specific notebook.',
|
|
inputSchema: z.object({
|
|
query: z.string().describe('The search query'),
|
|
limit: z.number().optional().describe('Max results to return (default 5)').default(5),
|
|
notebookId: z.string().optional().describe('Optional notebook ID to restrict search to a specific notebook'),
|
|
}),
|
|
execute: async ({ query, limit = 5, notebookId: explicitNotebookId }) => {
|
|
const notebookId = explicitNotebookId || ctx.notebookId
|
|
try {
|
|
const results = await semanticSearchService.searchAsUser(ctx.userId, query, {
|
|
limit,
|
|
threshold: 0.25,
|
|
notebookId
|
|
})
|
|
|
|
return results.map(r => ({
|
|
id: r.noteId,
|
|
title: r.title || 'Untitled',
|
|
excerpt: r.content.substring(0, 300),
|
|
score: r.score,
|
|
matchType: r.matchType,
|
|
}))
|
|
} catch (e: any) {
|
|
return { error: `Note search failed: ${e.message}` }
|
|
}
|
|
},
|
|
}),
|
|
})
|