Improve MemFuse recall via hybrid ranking and candidate generation.
Add RRF fusion with English word-level lexical scoring, tiered keyword fetch, and vector margin expansion (0.15/200) to fix pre-rank truncation; wire DashScope embedding bench path and update baseline to 28.8% recall@20. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+130
-2
@@ -88,9 +88,10 @@ test('pgvector backend performs parameterized vector lookup when explicitly enab
|
||||
|
||||
assert.equal(queries.length, 2);
|
||||
assert.match(queries[0].sql, /FROM memory_embeddings/);
|
||||
assert.match(queries[0].sql, /WITH vector_candidates/);
|
||||
assert.match(queries[0].sql, /WITH top_score/);
|
||||
assert.match(queries[0].sql, /vector_candidates/);
|
||||
assert.match(queries[0].sql, /recent_candidates/);
|
||||
assert.deepEqual(queries[0].params, ['user-1', '[0.25,0.5,0.75]', 50]);
|
||||
assert.deepEqual(queries[0].params, ['user-1', '[0.25,0.5,0.75]', 50, 0.15, 200]);
|
||||
assert.match(queries[1].sql, /ILIKE/);
|
||||
assert.deepEqual(result.semanticMemories, ['用户关注 Memory V2 的 facade 边界']);
|
||||
assert.deepEqual(result.memories, [
|
||||
@@ -153,6 +154,71 @@ test('pgvector hybrid ranking recovers a recent Chinese memory missed by vector
|
||||
assert.match(result.memories[0].text, new RegExp(marker));
|
||||
});
|
||||
|
||||
test('recallRankingWeights boosts vector only when semantic spread is visible', () => {
|
||||
const { recallRankingWeights } = pgvectorMemoryBackendInternals;
|
||||
const flat = recallRankingWeights('Why did Sarah close the curtains?', [
|
||||
{ vectorScore: 0.41 },
|
||||
{ vectorScore: 0.39 },
|
||||
{ vectorScore: 0.38 },
|
||||
]);
|
||||
assert.deepEqual(flat, { lexical: 1, vector: 1 });
|
||||
|
||||
const semantic = recallRankingWeights('Why did Sarah close the curtains?', [
|
||||
{ vectorScore: 0.82 },
|
||||
{ vectorScore: 0.55 },
|
||||
{ vectorScore: 0.41 },
|
||||
]);
|
||||
assert.deepEqual(semantic, { lexical: 0.45, vector: 1.55 });
|
||||
});
|
||||
|
||||
test('pgvector RRF hybrid ranking promotes semantic vector match over topical noise', () => {
|
||||
const ranked = pgvectorMemoryBackendInternals.rankHybridCandidates([
|
||||
{
|
||||
id: 'noise',
|
||||
content: 'The curtains the curtains the curtains were recently updated in the living room',
|
||||
score: 0.42,
|
||||
},
|
||||
{
|
||||
id: 'gold',
|
||||
content: 'Sarah closed the smart curtains to reduce pollen entry',
|
||||
score: 0.86,
|
||||
},
|
||||
], 'Why did Sarah close the curtains?', 1);
|
||||
assert.equal(ranked[0].id, 'gold');
|
||||
});
|
||||
|
||||
test('pgvector keyword-only rows do not pollute vector RRF ranks', () => {
|
||||
const ranked = pgvectorMemoryBackendInternals.rankHybridCandidates([
|
||||
{
|
||||
id: 'keyword-noise',
|
||||
content: 'The curtains the curtains the curtains were recently updated in the living room',
|
||||
score: null,
|
||||
},
|
||||
{
|
||||
id: 'vector-gold',
|
||||
content: 'Sarah closed the smart curtains to reduce pollen entry',
|
||||
score: 0.86,
|
||||
},
|
||||
], 'Why did Sarah close the curtains?', 1);
|
||||
assert.equal(ranked[0].id, 'vector-gold');
|
||||
});
|
||||
|
||||
test('pgvector semantic spread attenuates zero-overlap vector-only noise', () => {
|
||||
const ranked = pgvectorMemoryBackendInternals.rankHybridCandidates([
|
||||
{
|
||||
id: 'vector-noise',
|
||||
content: 'Ambient living room humidity sensor calibration report for May',
|
||||
score: 0.91,
|
||||
},
|
||||
{
|
||||
id: 'weak-overlap-gold',
|
||||
content: 'David reported his back felt sore after the morning stretch routine',
|
||||
score: 0.68,
|
||||
},
|
||||
], "How was David's back today?", 1);
|
||||
assert.equal(ranked[0].id, 'weak-overlap-gold');
|
||||
});
|
||||
|
||||
test('pgvector hybrid ranking keeps vector order when query has no lexical overlap', () => {
|
||||
const ranked = pgvectorMemoryBackendInternals.rankHybridCandidates([
|
||||
{ id: 1, content: 'alpha', score: 0.2 },
|
||||
@@ -257,6 +323,68 @@ test('extractKeywordTerms keeps topic phrases and drops recall boilerplate', ()
|
||||
assert.equal(topicTerms.some((term) => term.includes('德川')), true);
|
||||
});
|
||||
|
||||
test('latinWordQueryCoverage ranks topical English content over bigram noise', () => {
|
||||
const { latinWordQueryCoverage, queryScriptProfile } = pgvectorMemoryBackendInternals;
|
||||
const query = 'Why did Sarah close the curtains?';
|
||||
assert.equal(queryScriptProfile(query), 'latin');
|
||||
const gold = 'Sarah closed the smart curtains to reduce pollen entry';
|
||||
const noise = 'The curtains the curtains the curtains were recently updated';
|
||||
assert.ok(
|
||||
latinWordQueryCoverage(query, gold) > latinWordQueryCoverage(query, noise),
|
||||
);
|
||||
});
|
||||
|
||||
test('extractKeywordTerms drops English recall boilerplate', () => {
|
||||
const terms = pgvectorMemoryBackendInternals.extractKeywordTerms('Why did Sarah close the curtains?');
|
||||
assert.equal(terms.includes('why'), false);
|
||||
assert.equal(terms.includes('did'), false);
|
||||
assert.equal(terms.includes('the'), false);
|
||||
assert.equal(terms.includes('sarah'), true);
|
||||
assert.equal(terms.includes('curtains'), true);
|
||||
});
|
||||
|
||||
test('extractKeywordTerms prioritizes proper nouns over generic English terms', () => {
|
||||
const terms = pgvectorMemoryBackendInternals.extractKeywordTerms(
|
||||
'Can you piece together what happened with Ethan from soccer practice until he got home?',
|
||||
);
|
||||
assert.equal(terms[0], 'ethan');
|
||||
assert.equal(terms.includes('together'), false);
|
||||
assert.equal(terms.includes('happened'), false);
|
||||
assert.equal(terms.includes('soccer'), true);
|
||||
});
|
||||
|
||||
test('selectKeywordCandidateRows keeps priority-term gold under broad OR truncation', () => {
|
||||
const { selectKeywordCandidateRows, extractKeywordTerms } = pgvectorMemoryBackendInternals;
|
||||
const query = 'Why did David promise Ethan extra LEGO time on weekends?';
|
||||
const terms = extractKeywordTerms(query);
|
||||
const goldId = 'gold-lego';
|
||||
const rows = [
|
||||
{ id: goldId, content: 'David promised Ethan extra LEGO time on weekends during recovery' },
|
||||
...Array.from({ length: 900 }, (_entry, index) => ({
|
||||
id: `noise-${index}`,
|
||||
content: `David mentioned schedule item ${index} for the household calendar update`,
|
||||
})),
|
||||
];
|
||||
const selected = selectKeywordCandidateRows(rows, query, terms, { returnLimit: 50 });
|
||||
assert.ok(selected.some((row) => row.id === goldId));
|
||||
});
|
||||
|
||||
test('selectVectorCandidateRows expands margin band beyond fixed top-N', () => {
|
||||
const { selectVectorCandidateRows } = pgvectorMemoryBackendInternals;
|
||||
const scored = [
|
||||
{ row: { id: 'top' }, score: 0.9 },
|
||||
...Array.from({ length: 120 }, (_entry, index) => ({
|
||||
row: { id: `filler-${index}` },
|
||||
score: 0.85 - index * 0.001,
|
||||
})),
|
||||
{ row: { id: 'near-gold' }, score: 0.79 },
|
||||
];
|
||||
const selected = selectVectorCandidateRows(scored, { baseLimit: 100, margin: 0.12, expandCap: 200 });
|
||||
const ids = selected.map((entry) => entry.row.id);
|
||||
assert.ok(ids.includes('near-gold'));
|
||||
assert.ok(!ids.includes('filler-119') || ids.includes('near-gold'));
|
||||
});
|
||||
|
||||
test('pgvector backend validates table names before building SQL', () => {
|
||||
assert.throws(
|
||||
() => createPgvectorMemoryBackend({ tableName: 'memory_embeddings;DROP TABLE users' }),
|
||||
|
||||
Reference in New Issue
Block a user