UNPKG

@claude-flow/embeddings

Version:

V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking

97 lines • 3.75 kB
/** * ADR-121 Phase 11 — Reciprocal Rank Fusion (RRF). * * When you have N ranked result lists for the same query (or for N * variants of the same intent) — e.g. from a question-reformulation * pipeline, from a hybrid lexical+vector search, or from multiple * embedding models — RRF combines them into a single fused ranking * without needing the underlying scores to be comparable. * * Cormack, Clarke, Büttcher (SIGIR 2009): * * RRF_score(item) = Σ over lists i where item appears: 1 / (k_rrf + rank_i) * * where rank_i is the item's 1-based position in list i and k_rrf is a * small constant (the original paper recommends 60 — small enough that * top results dominate, large enough that mid-list ranks still * contribute). The score is heuristic but practically excellent: RRF * consistently matches or beats supervised learning-to-rank in TREC * evaluations despite needing zero training data. * * This module ships the algorithm as a pure function so callers can * compose it with any source of ranked lists — AnnRouter results, * lexical search, MMR-reranked picks, etc. The CLI's * `embeddings_search_text_ensemble` tool composes * `search_text_batch` + `reciprocalRankFusion` for the common case of * "expand one user query into N variants, retrieve top-k for each, * fuse to a single ranking". */ /** * Iterative RRF over N ranked lists. Time complexity: * O(N · L) where N = number of lists, L = avg list length * * Lists are 1-indexed inside the formula (first item = rank 1). */ export function reciprocalRankFusion(lists, options) { if (!Array.isArray(lists) || lists.length === 0) return []; const kRrf = options.kRrf ?? 60; if (kRrf <= 0) { throw new Error('kRrf must be > 0'); } if (options.listWeights && options.listWeights.length !== lists.length) { throw new Error(`listWeights.length (${options.listWeights.length}) must match lists.length (${lists.length})`); } const weights = options.listWeights ?? lists.map(() => 1); // id → accumulated fused state const acc = new Map(); for (let i = 0; i < lists.length; i++) { const list = lists[i]; const w = weights[i]; for (let j = 0; j < list.length; j++) { const item = list[j]; const rank = j + 1; // 1-based per SIGIR 2009 const contribution = w / (kRrf + rank); const entry = acc.get(item.id); if (entry) { entry.score += contribution; entry.ranks[i] = rank; entry.listOccurrences += 1; } else { const ranks = new Array(lists.length).fill(null); ranks[i] = rank; acc.set(item.id, { payload: item.payload, score: contribution, ranks, listOccurrences: 1, }); } } } // Sort by score desc, stable on id for determinism on ties. const sorted = Array.from(acc.entries()) .map(([id, v]) => ({ id, payload: v.payload, score: v.score, ranks: v.ranks, listOccurrences: v.listOccurrences, })) .sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; }); const k = Math.max(0, Math.min(options.k, sorted.length)); return sorted.slice(0, k); } /** * Convenience: same as reciprocalRankFusion but returns just the fused * ids in rank order. */ export function rrfIds(lists, options) { return reciprocalRankFusion(lists, options).map(h => h.id); } //# sourceMappingURL=rrf.js.map