fix(fts): deterministic top-k tiebreak (score DESC, row_id ASC)#7846
Open
sbrunk wants to merge 1 commit into
Open
fix(fts): deterministic top-k tiebreak (score DESC, row_id ASC)#7846sbrunk wants to merge 1 commit into
sbrunk wants to merge 1 commit into
Conversation
BM25 top-k resolved equal scores by encounter and pruning order, which varies with k and with partition completion order. top-k1 was not a prefix of top-k2, so paginating a tied-score query duplicated and skipped rows. Give the top-k a total order (score DESC, row_id ASC), matching Lucene: - ScoredDoc::cmp breaks score ties by ascending row_id (the final sort). - The per-partition collector and the cross-partition merge evict on the full (score, row_id) key, so ties resolve by row_id regardless of visit order or which partition finishes first. - WAND pruning lowers the threshold one ULP below the k-th score, so the score-only prune tests keep, rather than drop, docs tied at the k-th score, including a slower partition's ties behind the shared floor. This needs no change to the pruning kernels. - On the no-filter path, where row_ids are resolved after the WAND walk, the collector retains the k-th-score tie band and the merge re-selects by row_id. This keeps the result correct when doc_id order diverges from row_id order after a compaction remap. Non-tied queries keep full pruning power. A large exactly-tied band is scanned instead of pruned and, on the no-filter path, buffered in memory for row_id resolution.
Contributor
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: QUIET Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughChangesDeterministic top-k ranking
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Query as WAND search
participant Collector as TopKCollector
participant Results as Final candidates
Query->>Collector: insert scored documents
Collector->>Collector: retain k-th-score boundary ties
Collector->>Results: materialize heap and deferred ties
Results->>Query: return deterministic top-k candidates
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Lance's BM25 top-k resolved equal scores by encounter and pruning order, which varies with
kand with the order partitions finish. Sotop-k1was not an ordered prefix oftop-k2, and paginating a broad tied-score term (many docs sharing tf and doc length, so identical BM25) duplicated and skipped rows acrossfrom/sizepages.Lucene avoids this with a deterministic secondary sort
(score DESC, docId ASC). This PR brings the same guarantee to Lance, keyed onrow_id.Change
Give the FTS top-k a total order
(score DESC, row_id ASC):ScoredDoc::cmpbreaks score ties by ascendingrow_id(the final sort).(score, row_id)key, so ties resolve byrow_idregardless of visit order or which partition finishes first.admit_ties_floor), so the score-only prune tests keep, rather than drop, docs tied at the k-th score, including a slower partition's ties behind the shared cross-partition floor. This needs no change to the pruning kernels.row_ids are resolved only after the WAND walk, so the collector tiebreaks by a local doc_id proxy. That proxy diverges fromrow_idorder after a stable-row-id compaction remap, so the collector retains the k-th-score tie band and the merge re-selects byrow_idonce resolved.Row id stability
Correctness within a dataset version does not depend on stable row ids. The tiebreak only needs
row_idto be unique and comparable, which holds for both legacy row addresses and stable row ids, so the dup/skip fix works in either mode. What stable row ids add is stability across compaction: legacy row addresses change when fragments are compacted, so the tiebreak key moves and the order of tied docs can shift between versions, whereas stable row ids persist so the order survives compaction. This mirrors Lucene, whosedocIdtiebreak is deterministic within a searcher version but not stable across segment merges.Tests
top-k1is an ordered prefix oftop-k2) for single-partition (sorted and post-compaction unsorted), multi-partition, and filtered (non-deferred) paths.Performance
The change is a no-op on the non-tied path by construction:
admit_ties_flooronly alters pruning when a block max bit-equals the k-th score (which does not happen for distinct float scores), the tie overflow stays empty, and the row_id tiebreak never engages. A large exactly-tied band is scanned instead of pruned and, on the no-filter path, buffered in memory for row_id resolution. That is the inherent cost of an exact row_id tiebreak on that path.Local benchmarking on a laptop was too thermally noisy to confirm this (identical-code runs swung by 3 to 15%). Please run
@bench-bot benchmarkfor an authoritative measurement on the CI ARM64 runner, since this touches the WAND pruning inequality.