matrixorigin / matrixorigin/matrixone
[Feature Request]: WAND-based retrieval index — BM25 top-K retrieval without full-scan
- Dominant language
- Go
- Stars
- 1.9k
- Forks
- 311
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 768
Description
### Is there an existing issue for the same feature request?
- [x] I have checked the existing issues.
### Is your feature request related to a problem?
# WAND — the algorithm
**WAND** = **W**eak **AND** (a.k.a. Weighted AND). It's a *document-at-a-time (DAAT) top-K retrieval* algorithm — the standard fast way to answer "give me the K highest-BM25-scoring documents for this multi-term query" without scoring every matching document. Introduced by Broder et al. (2003); the **Block-Max WAND (BMW)** refinement is Ding & Suel (2011). It is what the `retrieval` fulltext index uses.
## The problem it solves
A query like `MATCH(txt) AGAINST('red apple banana')` is an **OR** over terms: a document matches if it contains *any* term, and its rank is `score(d) = Σ_t score(t, d)` (BM25 per term). The naive plan scores **every** document containing **any** query term, then sorts and takes the top-K. On a large corpus with common terms, that scores most of the collection — and you only wanted K rows. WAND skips the documents that provably **cannot** enter the top-K.
## The core idea: a threshold + upper bounds
Two ingredients:
1. **A running threshold `θ`** = the K-th best score seen so far (the smallest score currently in a size-K min-heap). A document can only matter if its score can beat `θ`.
2. **A per-term score upper bound `UB(t)`** = the maximum score term `t` can contribute to *any* document (precomputed from its posting list — for BM25, roughly `idf(t) × max-tf-component`). Then for any document `d`:
```
score(d) ≤ Σ_{t ∈ query, t ∈ d} UB(t)
```
If that **upper bound < θ**, the document cannot reach the top-K → **skip it, never score it.**
## The mechanism: pivoting over sorted cursors
Each query term keeps a **cursor** over its posting list (posting lists are sorted by doc id). One DAAT step:
1. **Sort the cursors by their current doc id** (ascending).
2. **Find the pivot.** Walk the cursors in doc-id order, accumulating `UB(t)`. The first term at which the running sum **exceeds `θ`** is the **pivot term**; its current doc id is the **pivot doc** — the *smallest* doc id whose upper bound could possibly beat `θ` (everything before it has too few high-value terms).
3. **Act on the pivot:**
- If every cursor *before* the pivot is already **at** the pivot doc → all those terms are present in the pivot doc → **fully score it**; if its real score > `θ`, push it into the heap and raise `θ`.
- Otherwise → the lagging cursors are on smaller doc ids that cannot win, so **advance them forward to (at least) the pivot doc** — jumping over a whole run of hopeless documents.
As the heap fills, `θ` **rises monotonically**, which makes the upper-bound test stricter, which makes the pivot jumps larger — the search accelerates as it goes. Only the documents that could plausibly enter the top-K are ever fully scored.
## Block-Max WAND (the refinement in use)
A single global `UB(t)` is loose — one outlier posting inflates it everywhere. **BMW** divides each posting list into **blocks** and stores a **max score per block**. The pivot test then uses the *block-local* max for each cursor's current region, giving a **much tighter** upper bound → more aggressive skipping, and it can **skip whole blocks** at once. (In the index, this is the per-block metadata `finalizeScoring` computes alongside the postings.)
## Why it is both fast and correct
- **Exact, not approximate.** WAND/BMW only skips a document when its *upper bound* is below `θ`. Since the upper bound ≥ the true score, a skipped document truly could not have entered the top-K — the returned top-K is **identical** to the exhaustive-scoring result. (This is why the gold correctness test diff-checks WAND's top-K against a brute-force `Σ tf·idf²` reference.)
- **Fast** because it evaluates a small fraction of the OR-matching documents — the win grows with corpus size and with how selective K is.
## How it maps to the MO `retrieval` index
- **Storage** = the postings (`docID`, `tf`) per term + docmap (pk, doc length) + per-block max scores, serialized into the tag=0 base sub-indexes (and tag=1 tail deltas). BM25 needs corpus stats `N` (doc count) and `avgdl` (average doc length), precomputed at load.
- **Query** = the pushed-down top-K (`LIMIT k` → heap size K), an optional membership bitmap (the `WHERE` prefilter, AND-ed into the walk so filtered-out docs are skipped like non-matches), and the multi-segment liveness bitmap (deletes/updates). The engine runs the BMW walk over the live, filtered postings and returns the exact BM25 top-K **directly — no SQL SORT node.**
## In one line
> **WAND is a safe top-K pruning strategy** — keep a threshold of the current K-th best score, bound each candidate by the sum of its terms' max possible contributions, and skip every document whose bound cannot beat the threshold — so you score only the handful of documents that could actually win.
## References
- A. Z. Broder, D. Carmel, M. Herscovici, A. Soffer, J. Zien. *Efficient Query Evaluation using a Two-Level Retrieval Process.* CIKM 2003. (original WAND)
- S. Ding, T. Suel. *Faster Top-k Document Retrieval Using Block-Max Indexes.* SIGIR 2011. (Block-Max WAND)
### Describe the feature you'd like
### Describe implementation you've considered
_No response_
### Documentation, Adoption, Use Case, Migration Strategy
```Markdown
```
### Additional information
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.