feat: Memory Echo chunk-level — détecte connexions au niveau section
Quand la similarité whole-note ne passe pas le seuil, vérifie les chunks. Si une section spécifique de la note A résonne avec une section de la note B, la connexion est créée avec le snippet précis qui a matché. SQL: cross-join LATERAL sur NoteEmbeddingChunk avec pgvector <=>. Fallback gracieux si la table chunks est vide ou erreur.
This commit is contained in:
@@ -259,15 +259,43 @@ export class MemoryEchoService {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate cosine similarity
|
||||
// Calculate cosine similarity — whole note level
|
||||
const similarity = cosineSimilarity(note1.embedding!, note2.embedding!)
|
||||
|
||||
// Similarity threshold for meaningful connections (adjusted by feedback)
|
||||
const baseThreshold = this.pairSimilarityThreshold(note1, note2, demoMode)
|
||||
const adjustedThreshold = baseThreshold
|
||||
+ (notePenalty.get(note1.id) || 0)
|
||||
+ (notePenalty.get(note2.id) || 0)
|
||||
if (similarity >= adjustedThreshold) {
|
||||
// Also check chunk-level similarity for more precise connections
|
||||
// This catches cases where two notes share a similar SECTION even if overall different
|
||||
let bestChunkSimilarity = similarity
|
||||
let chunkSnippet: string | undefined
|
||||
if (similarity < adjustedThreshold) {
|
||||
// Only check chunks if whole-note similarity didn't pass — saves DB queries
|
||||
try {
|
||||
const chunkRows: Array<{ content: string; embedding: string }> = await prisma.$queryRawUnsafe(
|
||||
`SELECT a.content AS content,
|
||||
1 - (a."embedding"::vector <=> b."embedding"::vector) AS chunk_sim
|
||||
FROM "NoteEmbeddingChunk" a
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT "embedding"
|
||||
FROM "NoteEmbeddingChunk"
|
||||
WHERE "noteId" = $2 AND "embedding" IS NOT NULL
|
||||
ORDER BY "embedding"::vector <=> a."embedding"::vector ASC
|
||||
LIMIT 1
|
||||
) b
|
||||
WHERE a."noteId" = $1 AND a."embedding" IS NOT NULL
|
||||
ORDER BY chunk_sim DESC
|
||||
LIMIT 1`,
|
||||
note1.id, note2.id
|
||||
)
|
||||
if (chunkRows.length > 0 && chunkRows[0]) {
|
||||
const row = chunkRows[0] as any
|
||||
if (row.chunk_sim && row.chunk_sim > bestChunkSimilarity) {
|
||||
bestChunkSimilarity = row.chunk_sim
|
||||
chunkSnippet = row.content?.slice(0, 200)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (bestChunkSimilarity >= adjustedThreshold) {
|
||||
connections.push({
|
||||
note1: {
|
||||
id: note1.id,
|
||||
@@ -281,9 +309,10 @@ export class MemoryEchoService {
|
||||
content: this.connectionPlainText(note2.title, note2.content || ''),
|
||||
createdAt: note2.createdAt
|
||||
},
|
||||
similarityScore: similarity,
|
||||
similarityScore: bestChunkSimilarity,
|
||||
insight: '', // Will be generated by AI
|
||||
daysApart
|
||||
daysApart,
|
||||
...(chunkSnippet ? { contextSnippet: chunkSnippet } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user