Notebook 08 · Retrieval
Notebook 07 proved hybrid fusion beats either retriever alone. But "beats" isn't "perfect", the right document can still land at #2 or #3. Today's job is a second pass that fixes exactly that.
The setup
Same 96 passages, same 16 gold-labeled queries. BM25 and bge-small retrieve, RRF fuses them, exactly as before. Reranking slots in right after.
Why does fusion need a second stage at all? Because RRF and weighted fusion are still built from fast, shallow scorers. They're excellent at deciding which 20 documents deserve a look. They're not always right about which one of those 20 is best.
Back up one step first
Every retriever so far, notebook 04's embeddings, notebook 05's vector database, notebook 07's dense scores, ends with the same tiny bit of arithmetic: cosine similarity, comparing two lists of numbers. That arithmetic has no idea what "HTTP logs" means. It just measures an angle. So the real question is: who turns the sentence into a list of numbers in the first place?
That's the missing piece. The transformer, the same self-attention architecture from module 0, GPT's own family, has been running quietly since notebook 04, every single time you called encoder.encode(text).
Now map that onto bi-encoder vs cross-encoder
That's the bi-encoder you already know, run the same transformer twice, separately, then compare the two outputs with cosine similarity. The cross-encoder below is the exact same kind of neural network, used differently: run once, on both texts glued together, no cosine similarity step at all.
That's why the cross-encoder only ever touches the shortlist fusion already built, never the whole 96-document corpus. Doing the careful read 20 times per query is fine. Doing it 96 million times would never finish.
A real query where hybrid gets it wrong
Before the details
Notice the order here is roughly speed-first, it doesn't match the order we'll walk through next, the notebook introduces them by mechanism instead (pointwise, then lightweight, then listwise, then managed, then late-interaction). Keep this picture in mind as the throughline.
Strategy 1 of 5
Scores each (query, document) pair independently, then sorts by score. This is what just fixed the HTTP logs query. ms-marco-MiniLM is small and fast; BGE-reranker-v2-m3 is the larger, higher-quality open self-hosted option.
Strategy 2 of 5
A quantized cross-encoder with no torch dependency. Same pointwise idea as strategy 1, tuned to be tiny and run anywhere, including serverless functions and edge devices with no GPU.
Strategy 3 of 5
Instead of scoring pairs one at a time, hand the LLM the whole candidate list at once and ask it to reorder them. It can reason about nuance and relative context across the whole set, not just one pair in isolation.
Strategy 4 of 5
No model to host, no infra to run, a per-call API instead. The notebook actually called it live:
Strategy 5 of 5
Covered conceptually, not run live, it needs heavier infrastructure. Instead of one vector per document, ColBERT keeps one vector per token and scores with MaxSim across all of them, finer-grained than a single-vector bi-encoder. It can even serve as a first-stage retriever, not just a reranker. Reach for it when single-vector recall plateaus on token-sensitive corpora, code search, part numbers, that kind of thing.
The proof, across all 16 queries
| Method | MRR | R@1 | R@3 | nDCG@10 |
|---|---|---|---|---|
| Hybrid, no rerank | 0.969 | 0.94 | 1.00 | 0.977 |
| Hybrid + FlashRank | 1.000 | 1.00 | 1.00 | 1.000 |
| Hybrid + Cross-encoder | 1.000 | 1.00 | 1.00 | 1.000 |
| Hybrid + LLM listwise | 1.000 | 1.00 | 1.00 | 1.000 |
| Hybrid + Cohere Rerank | 0.969 | 0.94 | 1.00 | 0.977 |
Three of the four rerankers lift the fused baseline to a perfect score. Cohere Rerank matched the baseline exactly, it didn't fix the one query the others did, and it didn't break anything either.
Before drawing conclusions
The two takeaways the notebook actually supports:
The tradeoff nobody skips
Measured on the same 20 candidates per query.
The surprise: the full cross-encoder actually ran faster than FlashRank here, small local models don't always sort the way their category label suggests, and this is exactly why the notebook keeps saying "benchmark on your own set." LLM listwise is over 20× slower than either, that's a real network round trip plus generation on top of an already-slow strategy.
Three shapes of "rerank"
| Reranker | Quality | Latency | Use when |
|---|---|---|---|
| FlashRank | good | very low, CPU | edge / serverless / cost-sensitive |
| Cross-encoder | high | medium | default self-hosted choice |
| Cohere / Jina | high | low–med | zero-ops, no model to run |
| LLM listwise | highest | high | small candidate sets, nuanced intent |
| ColBERT | high | medium | token-sensitive corpora at scale |
Notebook 08 · summary