/** * Search utilities — shared helpers for memory search across backends. * * Contains: * - RRF (Reciprocal Rank Fusion) merge — used by SQLite hybrid search * (eliminates the 3x duplication in auto-recall, memory-search, conversation-search) * - FTS query building — re-exported from sqlite for convenience */ // ============================ // RRF (Reciprocal Rank Fusion) // ============================ /** * Standard RRF constant from the original RRF paper. * Higher k → more weight on lower-ranked items (smoother distribution). */ export const RRF_K = 60; /** * Merge multiple ranked lists via Reciprocal Rank Fusion. * * Each item's RRF score = sum over all lists of 1/(k + rank + 1). * Items appearing in multiple lists get their scores summed. * * @param lists Array of ranked lists. Each list must have items with an `id` field. * @param k RRF constant (default: 60). * @returns Merged list sorted by descending RRF score, with `rrfScore` attached. * * @example * ```ts * const merged = rrfMerge( * [ftsResults, vecResults], * (item) => item.record_id, * ); * ``` */ export function rrfMerge( lists: T[][], getId: (item: T) => string, k: number = RRF_K, ): Array { const map = new Map(); for (const list of lists) { for (let rank = 0; rank < list.length; rank++) { const item = list[rank]; const id = getId(item); const score = 1 / (k + rank + 1); const existing = map.get(id); if (existing) { existing.rrfScore += score; } else { map.set(id, { item, rrfScore: score }); } } } return [...map.values()] .sort((a, b) => b.rrfScore - a.rrfScore) .map(({ item, rrfScore }) => ({ ...item, rrfScore })); }