/** * @factory/search — reciprocal rank fusion (search-architecture §4.5). * * `score(d) = Σ 1 / (k + rank_i(d))` over the ranked lists, with the standard * RRF constant k = 60. Ranks are 1-based. Documents missing from a list * simply do not contribute for that list. * * Pure and deterministic: same lists → same fused order (ties broken by * docId for stability). */ /** The standard RRF constant (search-architecture §4.5). */ export const RRF_K = 60; export interface RankedList { name: string; /** Doc ids in rank order (1-based ranks derived from index). */ docIds: string[]; } export interface FusedResult { docId: string; score: number; /** Which lists contained the doc (sorted list names). */ sources: string[]; } /** * Fuse two (or more) ranked lists with RRF. * * @param lists ranked lists (typically `bm25` + `vec`) * @param k RRF constant, default 60 * @param limit max results (0 = all) */ export function reciprocalRankFusion(lists: readonly RankedList[], k: number = RRF_K, limit = 0): FusedResult[] { if (k <= 0) throw new Error("RRF k must be positive"); const scores = new Map }>(); for (const list of lists) { list.docIds.forEach((docId, index) => { const rank = index + 1; const contribution = 1 / (k + rank); const existing = scores.get(docId); if (existing) { existing.score += contribution; existing.sources.add(list.name); } else { scores.set(docId, { score: contribution, sources: new Set([list.name]) }); } }); } const fused: FusedResult[] = []; for (const [docId, { score, sources }] of scores) { fused.push({ docId, score, sources: [...sources].sort() }); } fused.sort((a, b) => (b.score !== a.score ? b.score - a.score : a.docId < b.docId ? -1 : 1)); return limit > 0 ? fused.slice(0, limit) : fused; } /** * Convenience: fuse the two hybrid-search lists (BM25 rank list + kNN rank * list) and return doc ids with their fused scores. */ export function fuseHybrid(bm25: readonly string[], vec: readonly string[], limit = 0): FusedResult[] { return reciprocalRankFusion( [ { name: "bm25", docIds: [...bm25] }, { name: "vec", docIds: [...vec] }, ], RRF_K, limit, ); }