search_memory
Search stored memories using BM25 full-text search, weighted by importance and recency. Optionally filter by tags to narrow results.
Instructions
Search memories using BM25 full-text search with importance and recency weighting. Supports optional tag filtering to narrow scope.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query — natural language or keywords | |
| limit | No | Max results to return (default: 5) | |
| tags | No | Only search memories with these tags |
Implementation Reference
- src/index.ts:183-203 (handler)The 'search_memory' tool handler — parses query/limit/tags arguments, calls the BM25 search function, touches accessed memories, and formats results.
case 'search_memory': { const query = String(a['query'] ?? '').trim(); if (!query) return err('query is required'); const limit = a['limit'] !== undefined ? Math.max(1, Number(a['limit'])) : 5; const tags = Array.isArray(a['tags']) ? (a['tags'] as string[]) : undefined; const results = search(store.all(), query, { limit, tags }); if (results.length === 0) { return ok(`No memories found matching "${query}".`); } results.forEach(r => store.touch(r.memory.key)); const lines = results.map((r, i) => { const m = r.memory; const tagStr = m.tags.length ? `[${m.tags.join(', ')}]` : ''; return `${i + 1}. ${m.key} (importance: ${m.importance}/10${tagStr ? ' ' + tagStr : ''})\n ${m.content}`; }); return ok(`Found ${results.length} result${results.length !== 1 ? 's' : ''} for "${query}":\n\n${lines.join('\n\n')}`); } - src/index.ts:55-71 (schema)The 'search_memory' tool registration with inputSchema defining query (required string), limit (optional number), and tags (optional array of strings).
name: 'search_memory', description: 'Search memories using BM25 full-text search with importance and recency weighting. ' + 'Supports optional tag filtering to narrow scope.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query — natural language or keywords' }, limit: { type: 'number', description: 'Max results to return (default: 5)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Only search memories with these tags', }, }, required: ['query'], }, - src/index.ts:54-71 (registration)Tool name 'search_memory' registered in the ListToolsRequestSchema handler, exposing it as an MCP tool.
{ name: 'search_memory', description: 'Search memories using BM25 full-text search with importance and recency weighting. ' + 'Supports optional tag filtering to narrow scope.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query — natural language or keywords' }, limit: { type: 'number', description: 'Max results to return (default: 5)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Only search memories with these tags', }, }, required: ['query'], }, - src/search.ts:15-79 (helper)The core BM25 search function used by search_memory — performs full-text scoring with importance weighting, recency decay, and tag filtering.
export function search( memories: Memory[], query: string, opts: { limit?: number; tags?: string[] } = {} ): SearchResult[] { const { limit = 10, tags } = opts; const candidates = tags?.length ? memories.filter(m => tags.some(t => m.tags.includes(t))) : memories; if (candidates.length === 0) return []; const queryTerms = tokenize(query); if (queryTerms.length === 0) return []; // Pre-tokenize all documents const tokenized = candidates.map(m => ({ m, terms: tokenize(buildDocText(m)) })); // Compute document frequency for IDF const N = candidates.length; const df = new Map<string, number>(); queryTerms.forEach(qt => { const count = tokenized.filter(({ terms }) => terms.includes(qt)).length; df.set(qt, count); }); const avgLen = tokenized.reduce((a, { terms }) => a + terms.length, 0) / N; const k1 = 1.5; const b = 0.75; const scored = tokenized.map(({ m, terms }) => { const tf = new Map<string, number>(); terms.forEach(t => tf.set(t, (tf.get(t) ?? 0) + 1)); let bm25 = 0; for (const qt of queryTerms) { const freq = tf.get(qt) ?? 0; if (freq === 0) continue; const n = df.get(qt) ?? 0; const idf = Math.log((N - n + 0.5) / (n + 0.5) + 1); const tfNorm = (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * (terms.length / avgLen))); bm25 += idf * tfNorm; } // Exact key match boost const exactBoost = m.key.toLowerCase().includes(query.toLowerCase()) ? 2 : 0; // Tag exact match boost const tagBoost = m.tags.some(t => queryTerms.includes(t)) ? 1 : 0; // Importance weight: shifts score ±25% const importanceW = 1 + (m.importance - 5) * 0.05; // Gentle recency decay (half-life ~200 days) const daysSince = (Date.now() - new Date(m.updatedAt).getTime()) / 86_400_000; const recency = Math.exp(-daysSince * 0.003); const score = (bm25 + exactBoost + tagBoost) * importanceW * recency; const matchType: SearchResult['matchType'] = exactBoost > 0 ? 'exact' : 'keyword'; return { memory: m, score, matchType }; }); return scored .filter(r => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit); }