{"version":3,"file":"hybrid-rank.mjs","names":[],"sources":["../../../../../../../../ai/src/rag/hybrid/hybrid-rank.ts"],"sourcesContent":["import { bm25Rank, type LexicalDoc } from \"./bm25\";\nimport { reciprocalRankFusion, type RankedItem } from \"./rrf\";\n\n/**\n * Hybrid rank (A4) — fuse a dense (vector) ranking with a BM25 lexical\n * ranking over the same candidate set via Reciprocal Rank Fusion. Dense\n * retrieval captures semantic similarity; BM25 captures exact-term\n * matches dense embeddings miss (names, ids, rare tokens). Fusing both\n * beats either alone for keyword-heavy queries.\n *\n * `dense` is the vector retriever's result in rank order; `candidates`\n * supplies the text for the lexical pass (typically the same over-fetched\n * set). Returns the fused ranking, highest score first.\n *\n * @example\n * const fused = hybridRank({\n *   query: \"invoice 8842 refund\",\n *   dense: vectorHits,                 // [{ id }, ...] in similarity order\n *   candidates: vectorHits.map(h => ({ id: h.id, text: h.text })),\n * });\n */\nexport function hybridRank(params: {\n  query: string;\n  dense: ReadonlyArray<{ id: string }>;\n  candidates: ReadonlyArray<LexicalDoc>;\n  k?: number;\n}): RankedItem[] {\n  const denseIds = params.dense.map(d => d.id);\n  const lexicalIds = bm25Rank(params.query, params.candidates).map(r => r.id);\n\n  return reciprocalRankFusion([denseIds, lexicalIds], params.k);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,QAKV;CAIf,OAAO,qBAAqB,CAHX,OAAO,MAAM,KAAI,MAAK,EAAE,EAGL,GAFjB,SAAS,OAAO,OAAO,OAAO,UAAU,CAAC,CAAC,KAAI,MAAK,EAAE,EAExB,CAAC,GAAG,OAAO,CAAC;AAC9D"}