Fix hybrid RRF weighting by query type
This commit is contained in:
parent
92aaded36e
commit
004714af48
@ -9,6 +9,7 @@
|
||||
Scoped `--force` clears only collection-owned vectors, preserves shared
|
||||
hashes referenced by sibling collections, and drops `vectors_vec` only
|
||||
when the scoped clear empties all vectors.
|
||||
- Hybrid search: weight RRF lists by query type so original FTS and original vector evidence get the intended 2x boost, instead of accidentally boosting the first lexical expansion. #591
|
||||
- GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529
|
||||
- Fix: preserve original filename case in `handelize()`. The previous
|
||||
`.toLowerCase()` call made indexed paths unreachable on case-sensitive
|
||||
|
||||
20
src/store.ts
20
src/store.ts
@ -4158,6 +4158,21 @@ export type RankedListMeta = {
|
||||
query: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* RRF list weights for hybridQuery.
|
||||
*
|
||||
* Original-query retrieval paths are the primary evidence and get 2x weight:
|
||||
* - original FTS
|
||||
* - original vector search
|
||||
*
|
||||
* Expansion-derived lists (lex/vec/hyde) stay at 1x regardless of list order,
|
||||
* so a lex expansion inserted before original vector search cannot steal the
|
||||
* original vector boost.
|
||||
*/
|
||||
export function getHybridRrfWeights(rankedListMeta: RankedListMeta[]): number[] {
|
||||
return rankedListMeta.map(meta => meta.queryType === "original" ? 2.0 : 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hybrid search: BM25 + vector + query expansion + RRF + chunked reranking.
|
||||
*
|
||||
@ -4289,8 +4304,9 @@ export async function hybridQuery(
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: RRF fusion — first 2 lists (original FTS + first vec) get 2x weight
|
||||
const weights = rankedLists.map((_, i) => i < 2 ? 2.0 : 1.0);
|
||||
// Step 4: RRF fusion — original-query FTS and vector lists get 2x weight;
|
||||
// expansion-derived lists stay at 1x independent of insertion order.
|
||||
const weights = getHybridRrfWeights(rankedListMeta);
|
||||
const fused = reciprocalRankFusion(rankedLists, weights);
|
||||
const rrfTraceByFile = explain ? buildRrfTrace(rankedLists, weights, rankedListMeta) : null;
|
||||
const candidates = fused.slice(0, candidateLimit);
|
||||
|
||||
@ -52,10 +52,12 @@ import {
|
||||
insertDocument,
|
||||
generateEmbeddings,
|
||||
reindexCollection,
|
||||
getHybridRrfWeights,
|
||||
type Store,
|
||||
type DocumentResult,
|
||||
type SearchResult,
|
||||
type RankedResult,
|
||||
type RankedListMeta,
|
||||
} from "../src/store.js";
|
||||
import type { CollectionConfig } from "../src/collections.js";
|
||||
|
||||
@ -2046,6 +2048,38 @@ describe("Reciprocal Rank Fusion", () => {
|
||||
expect(fused[0]!.file).toBe("doc1");
|
||||
});
|
||||
|
||||
test("hybrid RRF weights boost original vector evidence over expansion-only hits", () => {
|
||||
const originalFtsOnly = makeResult("original-fts-only.md", 0.95);
|
||||
const expansionOnly = makeResult("lex-expansion-only.md", 0.95);
|
||||
const originalVector = makeResult("original-vector.md", 0.95);
|
||||
|
||||
// Mirrors hybridQuery's common list order when a lex expansion exists:
|
||||
// original FTS, lex expansion FTS, original vector.
|
||||
const rankedLists = [
|
||||
[originalFtsOnly],
|
||||
[expansionOnly],
|
||||
[originalVector],
|
||||
];
|
||||
const rankedListMeta: RankedListMeta[] = [
|
||||
{ source: "fts", queryType: "original", query: "user query" },
|
||||
{ source: "fts", queryType: "lex", query: "lex expansion" },
|
||||
{ source: "vec", queryType: "original", query: "user query" },
|
||||
];
|
||||
|
||||
const positionBasedWeights = rankedLists.map((_, i) => i < 2 ? 2.0 : 1.0);
|
||||
const buggyOrder = reciprocalRankFusion(rankedLists, positionBasedWeights);
|
||||
|
||||
expect(buggyOrder.findIndex(r => r.file === "lex-expansion-only.md"))
|
||||
.toBeLessThan(buggyOrder.findIndex(r => r.file === "original-vector.md"));
|
||||
|
||||
const semanticWeights = getHybridRrfWeights(rankedListMeta);
|
||||
const fixedOrder = reciprocalRankFusion(rankedLists, semanticWeights);
|
||||
|
||||
expect(semanticWeights).toEqual([2.0, 1.0, 2.0]);
|
||||
expect(fixedOrder.findIndex(r => r.file === "original-vector.md"))
|
||||
.toBeLessThan(fixedOrder.findIndex(r => r.file === "lex-expansion-only.md"));
|
||||
});
|
||||
|
||||
test("RRF adds top-rank bonus", () => {
|
||||
// doc1 is #1 in list1, doc2 is #2 in list1
|
||||
const list1 = [makeResult("doc1", 0.9), makeResult("doc2", 0.8)];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user