fix(memory-v2): improve pgvector recall and WeChat memory injection
Add keyword fallback and dedupe for pgvector resolve, score personal vs episodic memories by relevance, record WeChat recall product events, and disable broken chatrecall on Postgres session storage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+76
-2
@@ -53,6 +53,62 @@ function lexicalQueryCoverage(query, text) {
|
||||
return overlap / queryGrams.size;
|
||||
}
|
||||
|
||||
const KEYWORD_STOP_TERMS = new Set([
|
||||
'记得', '忘记', '之前', '我们', '聊过', '讨论', '继续', '聊聊', '什么', '吗', '呢',
|
||||
'有没有', '是否', '告诉', '提到', '说过', '以前', '上次', '对话', '会话', '回忆',
|
||||
'搜索', '帮助', '可以', '一下', '还是', '然后', '现在', '今天', '晚上', '你好',
|
||||
]);
|
||||
|
||||
export function extractKeywordTerms(query, { maxTerms = 8, minLength = 2 } = {}) {
|
||||
const normalized = String(query ?? '').normalize('NFKC').trim();
|
||||
if (!normalized) return [];
|
||||
const terms = new Set();
|
||||
const cjkOnly = normalized.replace(/[^\u4e00-\u9fff]/gu, '');
|
||||
for (let index = 0; index < cjkOnly.length; index += 1) {
|
||||
for (const size of [4, 3, 2]) {
|
||||
if (index + size > cjkOnly.length) continue;
|
||||
const term = cjkOnly.slice(index, index + size);
|
||||
if (term.length >= minLength && !KEYWORD_STOP_TERMS.has(term)) {
|
||||
terms.add(term);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const match of normalized.matchAll(/[a-z0-9]{3,}/gi)) {
|
||||
terms.add(match[0].toLowerCase());
|
||||
}
|
||||
return [...terms]
|
||||
.sort((left, right) => right.length - left.length)
|
||||
.slice(0, maxTerms);
|
||||
}
|
||||
|
||||
function dedupeContentPrefix(text, length = 96) {
|
||||
return normalizeSearchText(String(text ?? '').slice(0, length));
|
||||
}
|
||||
|
||||
async function fetchKeywordCandidates(pool, {
|
||||
userId,
|
||||
query,
|
||||
tableName,
|
||||
limit = 20,
|
||||
maxTerms = 8,
|
||||
} = {}) {
|
||||
const terms = extractKeywordTerms(query, { maxTerms });
|
||||
if (!terms.length) return [];
|
||||
const clauses = terms.map((_term, index) => `content ILIKE $${index + 2}`);
|
||||
const params = [userId, ...terms.map((term) => `%${term}%`)];
|
||||
const sql = `
|
||||
SELECT id, content, type, created_at, updated_at, 1.0 AS score
|
||||
FROM ${tableName}
|
||||
WHERE user_id = $1
|
||||
AND (${clauses.join(' OR ')})
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT $${params.length + 1}
|
||||
`;
|
||||
params.push(Math.max(1, Math.min(50, Number(limit) || 20)));
|
||||
const result = await pool.query(sql, params);
|
||||
return result?.rows ?? [];
|
||||
}
|
||||
|
||||
function timestampValue(value) {
|
||||
if (value == null) return 0;
|
||||
const numeric = Number(value);
|
||||
@@ -76,9 +132,13 @@ function normalizeRow(row) {
|
||||
|
||||
function rankHybridCandidates(rows, query, limit) {
|
||||
const byId = new Map();
|
||||
const byPrefix = new Set();
|
||||
for (const row of rows ?? []) {
|
||||
const memory = normalizeRow(row);
|
||||
if (!memory) continue;
|
||||
const prefixKey = dedupeContentPrefix(memory.text);
|
||||
if (prefixKey && byPrefix.has(prefixKey)) continue;
|
||||
if (prefixKey) byPrefix.add(prefixKey);
|
||||
const key = memory.id ?? `${memory.label}:${memory.text}`;
|
||||
if (!byId.has(key)) byId.set(key, memory);
|
||||
}
|
||||
@@ -171,8 +231,20 @@ export function createPgvectorMemoryBackend({
|
||||
) AS candidates
|
||||
ORDER BY id, source_priority
|
||||
`;
|
||||
const result = await pool.query(sql, [userId, vectorLiteral(embedding), candidateLimit]);
|
||||
const memories = rankHybridCandidates(result?.rows ?? [], input.query, limit);
|
||||
const [result, keywordRows] = await Promise.all([
|
||||
pool.query(sql, [userId, vectorLiteral(embedding), candidateLimit]),
|
||||
fetchKeywordCandidates(pool, {
|
||||
userId,
|
||||
query: input.query,
|
||||
tableName: resolvedTableName,
|
||||
limit: Math.max(limit, candidateLimit),
|
||||
}).catch(() => []),
|
||||
]);
|
||||
const memories = rankHybridCandidates(
|
||||
[...(result?.rows ?? []), ...keywordRows],
|
||||
input.query,
|
||||
limit,
|
||||
);
|
||||
return {
|
||||
semanticMemories: memories.map((item) => item.text),
|
||||
memories,
|
||||
@@ -184,4 +256,6 @@ export function createPgvectorMemoryBackend({
|
||||
export const pgvectorMemoryBackendInternals = {
|
||||
lexicalQueryCoverage,
|
||||
rankHybridCandidates,
|
||||
extractKeywordTerms,
|
||||
dedupeContentPrefix,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user