feat: chunks recherche (snippets) + script migration
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

1. Recherche: fetchChunkSnippets() — après le classement RRF existant,
   récupère les passages précis qui matchent depuis NoteEmbeddingChunk.
   Pur affichage, AUCUN changement de classement.

2. Script migration: scripts/migrate-chunk-embeddings.ts
   Indexe toutes les notes existantes en fragments.
   Batch de 10, barre de progression.
   Usage: npx tsx scripts/migrate-chunk-embeddings.ts

3. Memory Echo chunk-level: à faire (US restante)
This commit is contained in:
Antigravity
2026-06-20 17:07:38 +00:00
parent e9e829e579
commit 52c4cb1dee
22 changed files with 373 additions and 32 deletions

View File

@@ -100,20 +100,60 @@ export class SemanticSearchService {
const fusedResults = await this.reciprocalRankFusion(keywordResults, semanticResults)
return fusedResults
const topResults = fusedResults
.sort((a, b) => b.score - a.score)
.slice(0, opts.limit)
.map(result => ({
...result,
title: result.title || opts.defaultTitle,
matchType: result.score > 0.8 ? 'exact' as const : 'related' as const
}))
// Fetch chunk snippets for top results (display only — no ranking change)
const noteIds = topResults.map(r => r.noteId)
const snippetMap = await this.fetchChunkSnippets(query, noteIds)
return topResults.map(result => ({
...result,
title: result.title || opts.defaultTitle,
matchType: result.score > 0.8 ? 'exact' as const : 'related' as const,
matchedSnippets: snippetMap.get(result.noteId),
}))
} catch (error) {
console.error('Error in hybrid search:', error)
return this._ftsFallback(query, userId, opts)
}
}
/**
* Fetch matching chunk snippets for display (no ranking impact).
* Finds chunks whose content contains the query terms.
*/
private async fetchChunkSnippets(
query: string,
noteIds: string[]
): Promise<Map<string, string[]>> {
if (noteIds.length === 0 || !query.trim()) return new Map()
try {
const rows: Array<{ noteId: string; content: string }> = await prisma.$queryRawUnsafe(
`SELECT "noteId", content FROM "NoteEmbeddingChunk"
WHERE "noteId" = ANY($1::text[])
AND content ILIKE $2
ORDER BY "noteId", "chunkIndex"
LIMIT 30`,
noteIds,
`%${query.slice(0, 100)}%`
)
const map = new Map<string, string[]>()
for (const row of rows) {
const existing = map.get(row.noteId) || []
const snippet = row.content.length > 200 ? row.content.slice(0, 200) + '...' : row.content
if (existing.length < 3) existing.push(snippet)
map.set(row.noteId, existing)
}
return map
} catch {
return new Map()
}
}
/**
* PostgreSQL full-text search using tsvector + GIN index.
* SECURITY: Uses $queryRawUnsafe with parameterized bind params ($1, $2…).