intermediate probabilistic-ir 32 min read

The Inverted Index and Safe Dynamic Pruning (WAND, BlockMax-WAND)

How BM25 is actually computed at scale — and the provably exact pruning that skips the documents that cannot enter the top-k

BM25 tells us how to score one document against a query. It does not tell us how to find the ten best documents in a collection of ten million without computing ten million scores. That gap — between a scoring function and a retrieval system — is where the inverted index and dynamic pruning live, and it is the one place in classical IR where the mathematics is about algorithms rather than relevance.

The promise of dynamic pruning is unusually strong, and worth stating precisely up front: we will skip the vast majority of the collection and still return the exact top-k — the identical documents and scores an exhaustive scan would produce. Not an approximation, not a probabilistic guarantee with a failure rate, but the provably correct answer. The price, which we will be equally precise about, is that the guarantee is only on correctness, never on speed: a pathological query can force the algorithm to score everything.

A. postings lists — cell = BM25 contribution; columns dimmed under WAND were never fully scored
d0-margind1-hedged2-fxd3-macrod4-creditd5-liquidityd6-capitald7-guidanced8-taxd9-boilerUB
interest1.531.28···0.89···0.891.532
rate0.440.37·0.36·0.260.260.260.250.260.442
exposure1.030.691.03·0.99····0.691.027
B. documents fully scored (lower is cheaper) — all three return the same top-3
exhaustive10 / 10
WAND6 / 10
BlockMax-WAND6 / 10
C. top-3 for “interest rate exposure”
  1. 1.d0-margin3.001
  2. 2.d1-hedge2.338
  3. 3.d9-boiler1.845

On ten documents the gap is small; at scale it is not — the notebook scores 209 of 5000 documents with BlockMax-WAND where the exhaustive scan scores 2870, for the identical top-k.

The visualizer above runs all three methods over a ten-document worked corpus for the query interest rate exposure. Switch between exhaustive scanning, WAND, and BlockMax-WAND and watch the “documents fully scored” bars: every method returns the same top-k ranking, but the pruning methods compute far fewer full scores. On ten documents the saving is modest; the closing section shows it scoring just 7% of the documents an exhaustive scan does at scale.

What we cover

  1. The inverted index: postings lists, document frequency, the length array.
  2. Document-at-a-time scoring, the exhaustive baseline.
  3. WAND: per-term upper bounds, the threshold, and the pivot.
  4. The safety theorem — WAND returns the exact top-k.
  5. BlockMax-WAND: tighter local bounds and deeper skipping.
  6. The honest limit: no asymptotic guarantee, and a finance case study at scale.

The inverted index

The data structure is the obvious one once you need it. Rather than store, per document, the list of terms it contains, store per term the list of documents that contain it.

Definition 1 (Inverted index).

For a collection of NN documents, the inverted index maps each term tt to a postings list — the documents containing tt, sorted by document id, each annotated with the term frequency tft,d\text{tf}_{t,d}:

t    [(d1,tft,d1),(d2,tft,d2),],d1<d2<t \;\longmapsto\; \big[(d_1, \text{tf}_{t,d_1}),\, (d_2, \text{tf}_{t,d_2}),\, \dots\big], \qquad d_1 < d_2 < \cdots

Alongside it sit the document frequency dft\text{df}_t (the length of tt‘s postings list) and the length array dld\text{dl}_d (each document’s token count), which together are all the statistics BM25 needs.

These are exactly the ingredients of the vector space model: the postings are the nonzero entries of the sparse term–document matrix, dft\text{df}_t drives IDF, and dld\text{dl}_d drives length normalization. Storing them term-major rather than document-major is what lets us answer a query by reading only the postings of the query’s terms — a handful of lists — instead of touching every document.

There are two ways to walk those lists. Term-at-a-time (TAAT) processing reads one full postings list, accumulates partial scores into an array of size NN, then moves to the next term. Document-at-a-time (DAAT) processing advances a cursor on every query term’s list in lockstep, fully scoring one document before moving on. DAAT is what modern engines use and what dynamic pruning requires, because pruning decisions are made per document, against a threshold that evolves as documents are scored.


Exhaustive document-at-a-time scoring

The baseline is honest and exact: score every document that contains at least one query term.

Definition 2 (Exhaustive DAAT top-k).

Maintain a min-heap of the kk best (score,d)(\text{score}, d) pairs seen so far. Advance the query terms’ cursors in document-id order; for each document dd that appears in any query term’s postings, compute its full BM25 score tqcontributiont(d)\sum_{t \in q} \text{contribution}_t(d), and offer it to the heap. After all postings are consumed, the heap holds the exact top-k.

This is correct by construction — it scores everything, so it cannot miss anything — and it is the ground truth every pruning method must reproduce. Its cost is one full score per document touched: on the union of the query terms’ postings lists, which for a common term can be a large fraction of the collection. The entire point of WAND is to compute the same answer while evaluating far fewer of those documents.


WAND: upper bounds, a threshold, and the pivot

The key observation is that we do not need a document’s exact score to rule it out — an upper bound suffices. For each term, precompute the largest contribution it can ever make.

Definition 3 (Per-term upper bound and the pivot).

For each query term tt, let its upper bound be its maximum contribution over its postings,

UBt=maxdpostings(t)contributiont(d).\text{UB}_t = \max_{d \,\in\, \text{postings}(t)} \text{contribution}_t(d).

Let θ\theta be the current threshold — the kk-th largest score found so far (or -\infty until kk documents have been scored). Sort the query terms by their cursors’ current document ids. The pivot is the first term in this order whose cumulative upper bound reaches the threshold:

pivot=min{p:ipUBtiθ},\text{pivot} = \min\Big\{\, p : \textstyle\sum_{i \le p} \text{UB}_{t_i} \ge \theta \,\Big\},

and the pivot document is that term’s current document id. Any document with a smaller id cannot reach θ\theta and is skipped; the pivot document is fully scored only if all earlier cursors already align on it.

WAND — “Weak AND,” or “Weighted AND” — is the loop built on this rule (Broder et al., 2003). The threshold rises as better documents are found, which raises the pivot, which skips ever more documents. The interactive panel above shows the upper bounds UBt\text{UB}_t at the right of each postings row; the dimmed columns are the documents WAND’s pivot rule never fully scores.


The safety theorem

Everything rests on one guarantee: that skipping never discards a document that belongs in the top-k.

Theorem 1 (WAND returns the exact top-k).

For any query, any kk, and any documents, WAND fully scores every document that appears in the true top-k, and therefore returns exactly the same kk documents and scores as the exhaustive scan.

Proof.

Two facts drive the proof. First, the threshold is monotonically non-decreasing: the heap only ever replaces its smallest element with a larger score, so its minimum θ\theta can only rise. Let θ\theta^\star be the final threshold — the kk-th largest score in the collection (assuming at least kk documents match; otherwise every matching document is returned and there is nothing to prove).

Second, the score of any document is bounded by the upper bounds of the terms it contains. Suppose, for contradiction, that WAND skips a document dd that belongs in the true top-k, so score(d)θ\text{score}(d) \ge \theta^\star. A document is skipped only at some step where the cumulative upper bound over the terms whose cursors sit at or before dd falls short of the current threshold θ\theta:

score(d)  =  t:dpostings(t)contributiont(d)    t:dpostings(t)UBt  <  θ    θ.\text{score}(d) \;=\; \sum_{t \,:\, d \,\in\, \text{postings}(t)} \text{contribution}_t(d) \;\le\; \sum_{t \,:\, d \,\in\, \text{postings}(t)} \text{UB}_t \;<\; \theta \;\le\; \theta^\star.

The first inequality is the definition of the upper bound, applied term by term; the middle inequality is the skip condition — the only condition under which WAND advances past dd without scoring it; the last is monotonicity, since the current threshold never exceeds the final one. But this gives score(d)<θ\text{score}(d) < \theta^\star, contradicting score(d)θ\text{score}(d) \ge \theta^\star. Hence no top-k document is ever skipped. Every top-k document is fully scored and offered to the heap, which retains the kk largest, so WAND’s output equals the exhaustive scan’s.   \;\blacksquare

The proof is worth dwelling on because it isolates exactly what the algorithm needs to be correct: a valid upper bound (one that is never smaller than the true contribution) and a monotone threshold. Any tighter upper bound that remains valid keeps the algorithm exact while pruning more — which is precisely the door BlockMax-WAND walks through.


BlockMax-WAND: tighter local bounds

A single global UBt\text{UB}_t is pessimistic. A term’s postings list might contain one document where it scores 1.51.5 and a thousand where it scores 0.30.3; the global upper bound is 1.51.5 everywhere, so the pivot rule treats every one of those thousand documents as if it might score 1.51.5. BlockMax-WAND (Ding and Suel, 2011) fixes this by storing, for each fixed-size block of a postings list, the maximum contribution within that block.

Proposition 1 (Block maxima are valid, tighter upper bounds).

Partition each postings list into blocks and store each block’s maximum contribution. For a candidate pivot document, the sum of the relevant blocks’ maxima is an upper bound on the document’s true score (so pruning against it stays exact), and it is no larger than the global bound tUBt\sum_t \text{UB}_t (so it prunes at least as much). When the block-max bound falls below the threshold, the document’s full score need not be computed at all.

Because each block maximum is taken over a subset of the postings, it is at most the global maximum, so the block-max bound is tighter; and because it is still a maximum over the actual contributions in that block, it remains a valid upper bound, so the safety proof goes through verbatim with UBt\text{UB}_t replaced by the local block maximum. The result is the same exact top-k with strictly more skipping — the notebook confirms BlockMax-WAND never scores more documents than WAND, and far fewer at scale. Variable-sized blocks (VBMW; Mallia et al., 2017) push this further by choosing block boundaries to make the maxima as tight as possible.


The honest limit: no asymptotic guarantee

Dynamic pruning is one of the rare algorithms whose correctness is provable but whose speedup is not.


Finance case study

In practice the index is also compressed — postings are delta-encoded and packed — so block-max indexes do double duty: the block boundaries that bound the scores are also the units of decompression, and skipping a block means skipping its decompression entirely. The I/O saved by not touching a block is often a larger win than the arithmetic saved by not scoring a document, which is why block-max methods, not plain WAND, are what production engines such as Lucene and PISA actually ship.


Implementation

The companion notebook builds an inverted index with real postings lists and scores it with BM25 replicated verbatim from the BM25 topic (k1=1.5k_1 = 1.5, b=0.75b = 0.75), so the documents score identically to that page. It implements all three methods — exhaustive DAAT, WAND, and BlockMax-WAND — and turns the safety theorem into an assertion: across both the worked corpus and 150 random instances, the pruned methods return the exact top-k of the exhaustive scan, and the threshold is verified monotone.

The pruning is then measured rather than asserted. On a skewed synthetic collection the documents fully scored fall from 2870 (exhaustive) to 792 (WAND) to 209 (BlockMax-WAND) for the top-10 over N=5000N = 5000 — BlockMax-WAND computes full scores for just 7% of the documents the exhaustive scan does (209209 of 28702870, about 4%4\% of the full collection) while returning the identical ranking. The honest counterpoint runs in the same harness: a flat-score query where every document is tied forces WAND to score all 4040 documents, exactly as many as the exhaustive scan, demonstrating that the guarantee is on correctness alone. The worked corpus the visualizer mirrors (N=10N = 10, avgdl=9\text{avgdl} = 9) and its per-term upper bounds are printed by the harness so the page, the notebook, and the visualizer share one set of numbers.

Connections

  • the inverted index is the structure BM25 scores are accumulated over; WAND and BlockMax-WAND prune documents whose BM25 upper bound cannot enter the top-k bm25-binary-independence-model
  • the postings lists, document frequencies, and length array are exactly the sparse term-weight representation the vector space model defines, now stored for fast retrieval vector-space-model-tfidf
  • learned sparse retrievers (SPLADE) produce weighted term expansions that live in the same inverted index and are served by the same dynamic-pruning machinery late-interaction-learned-sparse

References & Further Reading