Deep Dive · Retrieval & RecSys

Two Towers, One Index: How Retrieval Models and ANN Search Are Co-Designed

The dual-encoder didn’t win because it was the most accurate model. It won because its one sacrifice — the query and item never meet until the final dot product — is exactly what a billion-vector search index needs to exist. This is the story of that bargain.

The whole idea in 30 seconds

Modern retrieval is one idea told twice: shape a model so item vectors can be computed ahead of time, then shape an index so the search can be approximate. Neither half is useful without the other — and together they power both web search and the recommendation feeds you scroll.

That’s the gist. Below is the long version, in plain language — the “Go deeper” boxes hold the math and the papers, so skip them freely and the main thread still reads straight through.

Picture the job a search box or a feed has to do. A user shows up. Somewhere in a catalog of a hundred million items is the handful they’d love. You have about 50 milliseconds to find them — and you pay that bill on every single request. Scoring all hundred million with a good model takes minutes. So the real question was never “what’s the most accurate model?” but “what shape of model lets you avoid running it a hundred million times per query?”

The answer has two parts, so tightly co-designed you can’t understand one without the other. The first is a model — the two-tower model (also called a dual encoder or bi-encoder). The second is a data structure — the approximate nearest neighbor (ANN) index. They look like two separate tools you happen to use together. They aren’t — they’re one system. Here it is, end to end — everything below is a detail of this one picture:

The system at a glance

Two towers, one index, then a ranker. Hover any box for what it does.

Offline · build the index once Online · every request (~50 ms) nearest-neighbor search Your whole catalog — every video, product, page or post you might return. Millions to billions of them. Items the catalog A neural network that turns each item's features into one vector. Runs offline, once per item — so its output can be stored. Item tower encoder What you're matching against: a search query, or — in a recommender — the user plus their recent history and context. Query / User + context A second network that turns the query (or user) into a vector in the SAME space. Runs live, once per request. Query tower encoder All item vectors, precomputed and stored. Approximate nearest-neighbor search finds the closest few hundred to the query vector — fast, without checking them all. ANN index item vectors + search The shortlist the ANN index returns: a few hundred candidates out of millions–billions. Recall here is the ceiling on final quality. Top-k candidates ~hundreds A heavy cross-feature model that looks at query and item together. Too slow for the whole catalog — so it only reorders the shortlist. Ranker cross-encoder · heavy The final ordered list shown to the user. Results
The item tower runs offline to fill the index; the query/user tower runs per request; ANN fetches the shortlist; a heavy ranker orders it. The two towers never meet except as vectors in the same index — that’s the whole trick, and the rest of this post is its consequences.

01The bargain: why a split score is the whole point

Here is the one idea everything else hangs on. A two-tower model scores how well a query q matches an item i as the dot product of two vectors, one per side — multiply them slot by slot, add up the results, and you get a single number for how aligned they are:

score(q, i) = f(q) · g(i)   — the two sides never mix until this multiply

That separation is the sacrifice and the superpower. Because the item vector g(i) doesn’t depend on the query, you build it once, offline, for the whole catalog, and stash it in an index. At request time you run only the small query tower, then solve a pure geometry problem: which stored vectors are closest to this one? That question has a fast answer.

Now compare the most accurate thing you could build instead: a cross-encoder, which reads the query and item together and lets every layer compare them directly. It catches subtleties a dot product can’t. But its score depends on the pair, so nothing can be precomputed: every query means a fresh pass over every candidate.

Work to score 100M items, per query
ApproachWork at query timeWorkable at 100M?
Cross-encoder (reads pairs together)100,000,000 model runsNo — minutes/query
Two-tower + check every vector1 query run + 100M dot productsBorderline — ~seconds
Two-tower + ANN index1 query run + ~thousands of dot productsYes — single-digit ms

So the cross-encoder doesn’t disappear — it moves. Real systems run in stages: a cheap two-tower model pulls a few hundred candidates out of a billion, then a heavy model reorders that shortlist. Two-tower is the first stage; the cross-encoder is what you can finally afford in the second. And fix one framing error: two-tower replaced candidate generation, not final ranking. Whatever the retriever misses, no ranker can rescue — so how much of the good stuff retrieval catches (its recall) sets the ceiling on everything.

The one sentence to remember

The towers are split not because two networks beat one — they’re split so item vectors can be computed ahead of time and matching becomes a fast nearest-neighbor lookup. The accuracy ceiling and the speed win are the same fact seen from two sides.

Play with it · Exact search vs. ANN

Drag the pink query point around. Watch how skipping most of the index goes faster — and exactly when it quietly misses a truly-close neighbor. The same machine powers search and recommendations: flip the view to relabel it.

View as
Method
Higher = more accurate but slower. At 12 it checks every cell (= Exact).
Items compared
 
Recall — true neighbors found
 
Speed
vs. checking everything
Query Top matches found Missed (truly close, in a skipped cell) Searched cells Other items
This is a 2-D cartoon. Real retrieval embeddings have hundreds to thousands of dimensions, where points spread out (the “curse of dimensionality”), clusters are far messier, and exact scan is genuinely expensive — so ANN’s speedups and its mistakes are both much larger than what you see here. Distance shown is straight-line (Euclidean), standing in for the cosine / dot-product similarity real two-tower models use.

02Where it came from, and why it won

The design has two ancestors. One is the Siamese network (Bromley, Guyon & LeCun, 1993) — twin networks that encode two inputs and compare the results, first used to verify signatures. The other is classic information retrieval. Their modern child is DSSM (Huang et al., Microsoft, 2013): push a query and a document through separate networks into the same space, score by similarity, and train on user clicks. It introduced two tricks that still echo: a hashing scheme shrinking a 500K-word vocabulary to ~30K inputs, and training against one clicked result plus a few random “wrong” ones (the seed of today’s in-batch negatives). One detail people miss: DSSM predates transformers entirely. It’s a plain feed-forward net, not BERT.

Why did this beat the matrix factorization that ruled the Netflix-Prize era? Because matrix factorization only memorizes: one vector per user-ID and item-ID it has already seen. Add a new item and it’s stuck until you retrain. A two-tower model instead reads features off an item and places it in the space, so it handles brand-new items on day one.

In plain terms: matrix factorization is a phone book — useless the moment someone new moves to town. A two-tower model reads the address off any business card and drops a pin on the map. (Matrix factorization is literally the stripped-down case where each “tower” is just a lookup; swap the lookup for a real encoder and you get generalization back.)

One attribution people usually get wrong

The famous YouTube paper (Covington et al., 2016) popularized embedding + ANN retrieval, but its retriever is technically a single tower, not a symmetric two-tower. The canonical named two-tower formulation is Yi et al. (2019), with DSSM (2013) as the ancestor and DPR (2020) as the NLP cousin.

Go deeper: “Siamese” and “two-tower” aren’t synonyms

Siamese means the two networks share weights — right when both inputs are the same kind of thing (two signatures, two sentences). Recommendation two-towers almost never share weights: the user side and item side have totally different features, so the towers are built separately. The technical name is an asymmetric dual encoder. Even in language tasks, symmetric sentence-similarity (SBERT) may share weights while question-answering (DPR) deliberately doesn’t — a question and its answer are different kinds of text.

03Inside the towers: a few choices that matter

Query / user tower
user, context, history
encoder (MLP or BERT)
q ∈ ℝd
Item / document tower
item content features
encoder (MLP or BERT)
v ∈ ℝd
they meet only here — score = q · v query tower runs online · item tower runs offline → into the index

The towers themselves are ordinary networks — MLPs for recommendations, BERT encoders for text. But three knobs inside punch above their weight:

Go deeper: collapse, and the re-embedding tax

Collapse is when a model crams everything into too small a corner of the space and stops telling rare items apart; the fixes are normalization, temperature, and simply enough dimensions (64–256 for recsys, 384–1024 for text). The re-embedding tax is unavoidable once the towers are separate: retrain the item tower and you must re-encode the entire catalog and rebuild the index, because the old and new vector spaces don’t line up. Newer expressiveness tricks (mixture-of-logits, LLM-backbone encoders) are careful to keep the item tower precomputable so this stays possible at all.

04Training against a billion classes

Here’s where the index reaches back into the model. The “correct” way to train a retriever is to treat every item as a class and ask the model to pick the right one — but that means scoring all 106–109 items for every training example, which becomes impractical somewhere past a few hundred thousand classes. So you approximate it: score the true item against a small handful of “wrong” ones (negatives) instead of all of them. It exists for the same reason ANN does: you simply cannot touch every item.

The cheapest source of negatives is the batch you’re already processing. In a batch of, say, 4,000 (query, item) pairs, each query treats the other 3,999 items as its negatives — thousands of nearly free negatives, since those item vectors are already computed. That’s why two-tower training uses giant batches. But there’s a catch: popular items show up as negatives constantly, so the model unfairly learns to push them down.

The fix is the logQ correction (Yi et al., 2019): nudge each negative’s score down by how often that item naturally appears, so common items aren’t penalized for being common. The clever part is that “how often” is estimated live, as the data streams by, because the catalog never stops changing. In a real YouTube A/B test, this bias correction meaningfully lifted the engagement win versus the naive version — a notable result at that scale.

In plain terms: if you grade a multiple-choice test where the popular answer is offered as a wrong option in almost every question, you’ll wrongly “learn” it’s never right. The logQ correction is the grader subtracting that built-in unfairness back out.
Go deeper: the formula, and a 2025 correction to the correction

Formally you subtract log Q(i) — how likely the item was to be sampled — from each negative’s score. That makes the cheap estimate an unbiased stand-in for the full calculation. This whole family is sampled softmax (Bengio & Sénécal, 2003), and the modern contrastive losses (InfoNCE, NT-Xent) are the same idea. A 2025 paper from Yandex (“Correcting the LogQ Correction”) showed the standard version is slightly mis-derived — it treats the right answer as if it were randomly sampled too — and fixing it lifted Recall@1000 from 0.431 to 0.462 on 300B real interactions.

05Negatives are the model

Past a certain point, how you pick negatives matters more than the architecture. A random negative is obviously wrong, so the model learns almost nothing from it. It learns from negatives that look plausible and aren’t — “hard” negatives. The baseline trick (from DPR, 2020) was to add one strong lexical near-match per example, which alone beat keyword search by a wide margin.

MethodThe ideaThe catch
ANCE (2021) Search the whole corpus for the model’s hardest negatives, refreshed every so often. Trains against a slightly stale search index — re-searching every step is too expensive.
RocketQA (2021) Pool negatives across many GPUs, and filter out ones that are secretly correct. The filtering needs a separate, heavier model to judge.
SimANS (2022) Pick negatives that are close-but-not-top — the “ambiguous” zone. A deliberate middle ground, tuned by hand.

And that exposes the real hazard: false negatives. Dig hard enough for plausible wrong answers and you start dredging up items that are actually right but just weren’t labeled — and training on those as “wrong” poisons the model. Half the cleverness in modern negative mining is about being aggressive without crossing that line.

06The distance trap

You have to tell the index what “close” means. There are three common choices — cosine (angle only), dot product (angle and length), and Euclidean (straight-line distance) — and on normalized vectors they all rank results the same way. So far, harmless.

The trap is subtle. If you don’t normalize and use raw dot product, “closeness” stops behaving like real distance: a single item with a very long vector can end up looking like the best match for almost any query. That breaks the shortcuts most indexes rely on to skip work, and it quietly wrecks results.

The most common silent production bug

Training with one notion of distance and indexing with another. Train on cosine but build the index on raw dot product — or normalize in the wrong place — and recall quietly slides toward random, with no error anywhere. A specific landmine: FAISS has no real “cosine” mode; you normalize first and then use inner product. Rule of thumb: match the index’s distance to the one you trained with, and normalize in exactly one place.

Go deeper: why raw dot product isn’t a real distance (and the fix)

Searching by raw dot product (“MIPS,” maximum inner-product search) isn’t a proper distance: it breaks the triangle inequality, and an item’s nearest neighbor by dot product isn’t even itself. That’s the “long vector wins everything” pathology. The classic fixes (ALSH, 2014; SIMPLE-LSH, 2015) bolt an extra coordinate onto each vector so all the lengths roughly match. That reduces dot-product search to the ordinary angle-or-distance search an index can handle.

07HNSW: the default index

Now the search itself: one query vector, a hundred million stored vectors, return the closest few hundred. Checking all of them is too slow for a 50 ms budget. The key insight behind ANN: you almost never need the exact closest items — return 98 of the true top 100 plus two near-misses and nobody notices, because the reranker sorts it out. You trade a little accuracy for a huge speedup.

The most popular index is HNSW (Malkov & Yashunin, 2016). Think of it as a navigable graph of vectors, with a few long-range “express” links layered on top of dense local ones. To search, you start at the top, ride the express links to get roughly close, then drop down for the last mile. It powers most of the vector databases you’ve heard of — FAISS, Qdrant, Weaviate, Milvus, pgvector, Elasticsearch and more.

In plain terms: HNSW is a subway map. The top layer is the express line with a few far-apart stops to cover ground fast; the bottom layers are local buses for the final blocks. You ride express until you’re near, then transfer down.

It has exactly three knobs, and only one of them is something you tune live:

KnobWhat it doesWhen you set it
MHow many links each node keeps (more = better, heavier).When building
efConstructionHow hard to work while building the graph.When building
efSearchThe main accuracy/speed dial. Higher = more accurate, slower.Live — no rebuild

The price of all that speed is memory: the whole graph and all the vectors sit in RAM. That caps a single machine at roughly 100–200M vectors before you need disk-based methods or splitting across servers. HNSW hits 95–99% accuracy at sub-millisecond speeds, but squeezing out the last percent or two gets very expensive, and deleting items is awkward enough that heavy churn forces periodic rebuilds.

Go deeper: the “H” may be optional (2024)

A 2024 result — “Down with the Hierarchy: The H in HNSW Stands for Hubs” — found that for high-dimensional vectors, the fancy layered express lanes are mostly redundant. A plain flat graph matches HNSW’s speed and accuracy with less memory, because a few naturally well-connected “hub” nodes already act as the express line. In a big enough city, the subway draws its own express route.

08IVF, compression, and the rest of the index zoo

When memory is the binding constraint, you switch from graphs to group-and-compress. Both ideas come from one 2011 paper (Jégou et al.), tested on a billion vectors.

IVF sorts vectors into clusters up front; at query time you only scan the few clusters nearest the query instead of everything. The dial here, nprobe, sets how many clusters to check — more clusters, more accuracy, more time. Its weakness is the boundary problem: a great match sitting just across a cluster line can be missed entirely.

Product Quantization (PQ) is the compressor. Instead of storing a full vector, it chops it into pieces and replaces each piece with a short code — turning a 3,000-byte vector into about 16 bytes, a ~190× shrink, so a billion vectors actually fit. Combine the two and you get IVF-PQ, the standard memory-saver.

In plain terms: IVF is sorting a library into rooms and only walking into the few rooms that could hold your book. PQ is describing each book by a short tag — “mystery / hardcover / blue” — instead of photographing every page. You only pull the full book for the final handful of candidates.

Layered on top, the rest of the zoo is best read as a three-way trade between accuracy, speed, and memory:

IndexThe ideaProfile
HNSW / CAGRAIn-memory graph; CAGRA is the GPU-native version (much faster build and search).Fastest, hungriest for RAM
IVF-PQGroup + compress. ScaNN is a related method whose compression is tuned for dot-product search.Lowest RAM, some accuracy lost
DiskANN (2019)One graph tuned to live on SSD: compressed vectors in RAM to navigate, full ones on disk to confirm.Billion vectors on a single box, ~5ms
SPANN (2021)Keep only the cluster centers in RAM, the rest on SSD.Lowest RAM, slightly slower
LSH / AnnoyOlder methods. Still useful for guarantees or simple static files, but graphs usually win on real data.Mostly superseded
Go deeper: ScaNN’s insight and the bit-compression frontier

ScaNN (Google, 2020) noticed that for dot-product search, not all compression error hurts equally — error along the vector’s own direction distorts the scores that matter most, so it weights compression to protect that direction. The 2024–26 frontier (RaBitQ, binary and int8 quantization) pushes this to the limit: binary compresses 32× and, with a quick full-precision recheck of the finalists, recovers ~96% of accuracy at 15–45× speedup; int8 gives 4× at >99%. The universal pattern is “search cheap and compressed, then recheck the shortlist at full precision.”

09Buying interaction back

The single dot product threw away word-by-word matching. You can buy it back, at rising cost, and the production answer is to line the options up as a cascade rather than pick one:

Bi-encodercheapest
One vector per side, one dot product, fast index search. Most precomputable, least nuanced — this is the retrieval stage.
Late interactionColBERT
Keep one vector per word and compare word-to-word at the end. Still mostly precomputable, but recovers fine-grained matching. The cost is a much bigger index.
Cross-encodermost precise
Read query and item together with full attention. Most accurate, but nothing precomputes — so it only reranks a small shortlist.

ColBERT (2020) is the influential middle ground: it keeps a vector for every word and scores by matching each query word to its best partner in the document. That recovers much of a cross-encoder’s nuance while still letting you index documents ahead of time. Its cost isn’t compute — it’s storage, since one vector per word is a lot of vectors. Follow-ups (ColBERTv2, PLAID) mostly exist to shrink and speed up that index, and the idea has since jumped to images, too.

10Search, RAG, and a reality check

The same architecture powers text search and the retrieval step in RAG. The lineage runs through DPR (2020), and Sentence-BERT (2019) made the speed case stark: comparing 10,000 sentences pairwise drops from ~65 hours with a cross-encoder to ~5 seconds with a bi-encoder. Today’s embedding models — E5, BGE, GTE, OpenAI’s text-embedding-3, Cohere, Voyage — cluster closely on the public leaderboards.

But here’s the humbling part. The BEIR benchmark showed that on data they weren’t trained on, these dense models often generalize worse than plain old keyword search (BM25), which remains shockingly hard to beat out of the box.

Two things that silently cost you accuracy

The wrong prefix. Many retrieval models expect you to label inputs (“query:” vs. “passage:”) because a question and its answer are different kinds of text; use the wrong label and quality drops with no warning. And the embedding model isn’t the main lever. For RAG, how you chunk your documents, whether you combine keyword + vector search, and whether you rerank usually matter more than swapping one good embedding model for another.

Go deeper: one vector, many budgets (Matryoshka)

Matryoshka embeddings (2022) train a vector so its first chunk is already a good summary — so you can chop a 2048-number vector down to 256 and keep most of the accuracy, trading size for cost on the fly. It ships in OpenAI’s text-embedding-3 (the dimensions option). The catch: only models trained this way survive being chopped — slicing an ordinary embedding just breaks it.

11Running it in production

This is where most of the real work lives, and almost none of it is in the papers:

12Measuring it honestly

The standard benchmark (ANN-Benchmarks) makes one thing clear: there’s no single “best” index, only a trade-off curve between accuracy and speed. So “index A is faster than B” is meaningless unless you hold accuracy fixed and then compare speed. Accuracy, speed, and memory form a triangle — push one and another gives.

A couple of honest cautions: benchmarks usually measure a single query at a time, but production runs many at once, where memory pressure can flip the winner; and what matters is usually the worst-case latency (p99), not the average, because graph search has long tails.

The insight that reframes the whole metric

“Accuracy” here usually means “did we return the exact same neighbors as a brute-force search.” But the neighbors a fast index misses are often near-duplicates of ones it did return — so the final answer barely changes even when this score drops. Recent work (2026) finds that when quality truly matters, the fanciest index buys only about a 2.5× speedup over brute force. Translation: “99% recall” does not mean your answers are 99% as good — tune only to the accuracy your reranker can actually use.

13The full funnel

Every large system arranges the same pipeline: a cheap vector search narrows millions-to-billions of items down to a few hundred in well under 50 ms, then a feature-rich ranker reorders that shortlist. The famous write-ups all teach a piece of it:

SystemWhat it taught
YouTube (2019)The canonical two-tower recipe, plus the popularity-bias fix that meaningfully lifted engagement at scale.
Meta search (2020)The big lesson: tune the model, the compression, and the index together with the ranker. A model that looks better on paper can lose once compression and approximation hit. The index is the easy 10%.
Pinterest (2018)Scaled graph-based embeddings to billions of nodes using local neighborhoods and a training schedule that feeds in progressively harder negatives.
Taobao (2021)Billion-item product retrieval in real time, while actively fighting irrelevant matches and train/serving mismatch.

Which lets us retire three myths in one breath. “Plug in a vector database and you’re done” — no; the hard parts are negatives, bias correction, and co-tuning retrieval with the ranker. “The index finds the true nearest neighbors” — no; it’s approximate on purpose, and teams dial the accuracy knob to hit a latency budget. “More candidates always help” — no; past what the ranker can reorder, extra candidates only add latency and noise.

14Same machine, different words: recommendations

Everything so far has used search vocabulary — query, document, relevance. Swap three words and you have a modern recommender system. This isn’t a loose analogy; it’s a literal substitution. In a feed, the two-tower model is the candidate-generation stage, and ANN over item embeddings is what fetches the few hundred things a heavy ranker then orders.

The same two towers, relabeled
In searchIn recommendations
Query tower (encodes the typed query)User tower (encodes the user: id, context, recent history)
Document towerItem tower (the video, pin, product, post)
Relevance label (a judged match)Engagement (a click, watch, purchase, save)
Retrieve → rerankCandidate generation → ranking

The scoring is identical — a dot product of a user vector and an item vector — so the same ANN index (ScaNN, HNSW, FAISS) precomputes every item embedding and returns candidates online. When you flip the demo near the top to Recommendations, nothing in the math changes; only the labels do. That’s the whole point.

What genuinely differs lives on the user side — and it’s where most of the research goes:

The exemplars are the same names from the funnel above — YouTube’s corrected two-tower retriever, Pinterest’s PinSage and PinnerSage, Meta’s Facebook-Search EBR, Alibaba’s Taobao MGDSPR — all running the identical retrieve-then-rank pattern over catalogs of millions to billions. (The main alternative, Alibaba’s tree-based TDM, drops the dot-product constraint to allow richer interaction at retrieval time — a reminder that two-tower + ANN is the dominant answer, not the only one.)

The takeaway

Web-scale retrieval looks like magic — a billion items, the right twenty, in a few milliseconds. It’s really one trick told twice. Split the model so item vectors can be built ahead of time. Index those vectors so the search can be approximate.

Everything else follows from that seam. The training shortcut, the distance measure, the compression scheme, the choice of reranker — each is a negotiation between the two halves. You can’t reason about the model without the index, or the index without the model that shaped its vectors. They were never two technologies. They’re one bargain — and that bargain is why nearly every search box and feed you touch can answer you before you finish blinking.

How this was made: the claims, papers, and figures here were assembled by a multi-agent research pass — 16 agents covering distinct facets (the model, training, the index families, real production systems), then a fact-checking stage that verified the numbers and citations and corrected two attribution errors, plus a focused pass on the recommendation-systems mapping and a clarity edit. The interactive demo above computes its exact ground truth every frame, so its recall and “missed” markers are real — but 2-D radically understates real high-dimensional difficulty. Figures tied to a specific paper come from that paper; “typical” ranges are practitioner conventions, not guarantees.

SM
Sugeerth Murugesan Staff ML Engineer / Scientist · LLM & Recommendation Systems