Every search box, every RAG pipeline, every deduplication job leans on one function: text in, vector out. This is the paper that wrote down how to build a good one — and the four papers that took the recipe apart and put it back together differently.
Four teams at your company are shipping four different products this quarter.
The search team is replacing keyword search with something that finds "how do I cancel" when the help article says "terminating your subscription." The assistant team is building retrieval-augmented generation: pull the six most relevant support articles, paste them into a prompt, let the model answer. The analytics team wants a dashboard that groups yesterday's ten thousand support tickets into themes nobody wrote down in advance. The data team needs to find near-duplicate articles in a knowledge base of 400,000 documents so the writers stop maintaining eleven copies of the same page.
Four teams. Four roadmaps. Four sets of slides. And exactly one function underneath all of them:
Take a string. Return a fixed-length list of numbers — 384 of them, or 768, or 1024, or 4096. Do it in a way that puts strings people would call related near each other, and strings people would call unrelated far apart. That is the whole job description. Every one of those four products is a different geometric question asked of the same set of points.
Abstractions like "text in, vector out" hide the part you would actually have to write. Let us walk one string all the way through a BERT-based embedder, naming every shape, because every design decision in this lesson lives at one of these arrows.
Start with the string "how do I cancel my plan".
| Stage | Output shape | What actually happens |
|---|---|---|
| Tokenizer | (L,) = (9,) | WordPiece splits into [CLS] how do i cancel my plan [SEP] — integer ids, not text, from here on |
| Embedding table | (L, 768) | Each id indexes a row of a (30522, 768) matrix; add positional and segment embeddings |
| 12 transformer blocks | (L, 768) | Shape is unchanged. What changes is that token 4 ("cancel") now contains information about tokens 0–8, because self-attention mixed them |
| Pooling | (768,) | Collapse L token vectors into one sentence vector. E5 uses the mean. BGE uses the [CLS] row. NV-Embed uses a learned attention layer. This one line is a whole chapter later. |
| L2 normalisation | (768,) | Divide by the vector's own length so every embedding lands on the unit sphere. Now the dot product is the cosine |
That last row is worth pausing on because it silently defines the metric for everything downstream. If v is a vector, its L2 norm — written ‖v‖ — is the square root of the sum of its squared components: the ordinary length of the arrow. Dividing by it gives a vector of length exactly 1. For two such unit vectors,
so the dot product, which a GPU computes as a single fused multiply-add sweep, returns exactly the cosine of the angle between the two texts. That is why a vector database can answer "most similar document" with a matrix multiply. Length has been thrown away on purpose: we decided that direction carries the meaning and magnitude does not.
"Related texts near each other" is not yet a loss function. Make it one by asking what each of the four products actually needs, and notice that they need different relations.
| Product | Geometric operation | The relation it needs | What breaks if the geometry is wrong |
|---|---|---|---|
| Semantic search | argmaxp cos(q, p) over the corpus | p answers q — asymmetric | Returns passages that look like the question instead of ones that answer it |
| RAG | top-k, then concatenate into the prompt | p answers q, plus diversity across the k | Six near-identical chunks fill the context; the model confidently answers from the wrong document |
| Clustering | k-means / HDBSCAN on the cloud | a and b share a topic — symmetric | Clusters form around writing style or ticket template, not subject matter |
| Deduplication | pairs with cos > 1 − ε | a and b say the same thing — symmetric, very tight | Either merges two genuinely different policies or misses eleven copies of one |
| Classification probe | logistic regression on the frozen vector | the label is linearly readable | The information exists in the encoder but was destroyed by pooling |
Look at column three. Search wants an asymmetric relation: "who wrote Hamlet" and a paragraph about Shakespeare are related in the way a question is related to its answer, and that relation does not read the same in both directions. Deduplication wants a symmetric relation: a and b are duplicates of each other, full stop. One model, one vector per string, one cosine — and two relations that genuinely disagree about what should be close.
That tension is not a detail. It is the reason Chapter 4 exists, the reason E5 prepends the literal strings "query: " and "passage: " to its inputs, and the reason every model after E5 kept some version of that trick.
The same 240 documents, embedded once. Switch the product and watch the operation change while the points stay exactly where they are. Nothing is retrained between modes — this is the entire economic argument for a shared embedder.
Three things worth noticing while you play. First, the query in Search mode is not a special kind of object — it is a point, embedded by the same function as everything else. Second, RAG's top-6 in a dense cluster returns six chunks that say nearly the same thing, which is the single most common real-world RAG failure and it is a geometry failure, not a prompting failure. Third, Dedup and Cluster never use a query at all: they are operations on the cloud itself, and they only work if the cloud's local structure means something.
Here is the argument that made text embedders a research priority in 2022 rather than a solved 2019 problem. Consider a RAG system. Its answer quality is bounded above by whether the right document made it into the context window. If retrieval hands the generator the wrong six passages, no prompt engineering, no larger model, and no chain-of-thought recovers the answer — the information is simply not in the room.
Put rough numbers on it. Suppose recall@6 is 72% with your current embedder, and the generator answers correctly 90% of the time when the answer is present and 8% of the time when it is not (occasionally it happens to know). End-to-end accuracy:
Now swap in an embedder that lifts recall@6 to 84% — a realistic jump from a 2021-era model to a 2023-era one on out-of-domain data. Nothing else changes:
Ten points of end-to-end accuracy, from swapping one 330M-parameter model whose inference cost is a rounding error next to the generator's. Contrast that with the alternative lever: upgrading the generator so that it answers correctly 95% of the time instead of 90% when the answer is present. That gives 0.72 × 0.95 + 0.0224 = 0.706 — less than four points, at many times the serving cost.
None of this is new. Turning documents into vectors so you can compare them with arithmetic is one of the oldest ideas in information retrieval, and each generation failed in a way that defined the next one's job.
| Era | The representation | Dimension | What it unlocked | Where it broke |
|---|---|---|---|---|
| TF-IDF (1970s onward) | Sparse count vector, one coordinate per vocabulary term, weighted by rarity | 50k–1M, mostly zeros | Exact term matching at web scale on a CPU | Two documents with no shared word have cosine exactly 0, however synonymous |
| LSA / LSI (1990) | Truncated SVD of the term–document matrix | 100–300 | Latent topics; some synonymy for free | One global linear projection; no word order, no context, no way to add a document without refactoring |
| word2vec / GloVe (2013) | One dense vector per word type, learned from co-occurrence | 100–300 | Learned synonymy; "king − man + woman" | One vector per word regardless of sense, and a sentence is only the average of its words |
| Sentence-BERT (2019) | Contextual encoder, mean-pooled, fine-tuned on inference pairs | 768 | Genuine sentence-level similarity | Symmetric only; out-of-domain retrieval lost to BM25 |
| E5 (2022) | Contextual encoder + prefixes + 270M weakly-supervised pairs | 384–1024 | Asymmetric retrieval that transfers out of domain | 512-token window; English-first |
| Decoder-LLM embedders (2024) | 7B decoder with pooling surgery and free-form instructions | 4096 | Geometry selected at inference by a sentence | Roughly 21× the compute and 4× the index |
Row one deserves the arithmetic, because it is the cleanest possible statement of what a neural embedder buys. Under TF-IDF, "how do I cancel my plan" and "to end your subscription, open Billing" share no content word at all. The dot product of two vectors that are non-zero on disjoint coordinate sets is a sum in which every term has a zero factor:
Not "small". Zero. The right answer is exactly as similar to the query as a recipe for bread. No amount of tuning fixes it, because the representation has no coordinate on which the two texts could possibly agree.
Row three has an equally clean failure. If a sentence is the average of its word vectors, then "the cat sat on the mat" and "the mat sat on the cat" are averages of the same six vectors. They are therefore the same vector, cosine 1.0000, indistinguishable. Word order carries meaning and averaging destroys it. That single observation is why the field needed contextual encoders, and it is worth remembering when someone proposes averaging embeddings as a cheap way to represent a document.
Before the neural story, name the incumbent honestly. BM25 is a scoring function from the 1990s that ranks documents by term overlap with the query, weighting rare terms higher and saturating the contribution of repeated terms. It has no parameters learned from your data, no GPU, and no notion of meaning. It cannot match "cancel" to "terminate."
And through 2022, on the BEIR benchmark — a suite of retrieval datasets deliberately chosen to be out of domain for anything trained on web QA — BM25 beat almost every neural embedder that had not been trained on that specific domain. Averaged over BEIR's datasets, BM25 scores roughly 41.7 nDCG@10, and dense models trained on MS MARCO routinely landed below it once they left MS MARCO's distribution.
That result is embarrassing in a specific, informative way. A dense model has memorised a semantic space from one corpus; a lexical model has memorised nothing. When the corpus changes, the model with no assumptions transfers better than the model with the wrong ones. The E5 paper's headline claim is precisely this: it is, in the authors' words, the first model to outperform BM25 on BEIR without using any labelled data.
Because it sits underneath everything, the embedder gets blamed for problems it does not own and credited with capabilities it does not have. Three boundaries, drawn now, save a lot of debugging later.
It is not a classifier. It has no label set and emits no probabilities. It emits a direction. To make a decision you must supply something to compare against — a set of class-name embeddings, a labelled training set for a probe, or a threshold you chose. Everything the model "knows" about your categories arrives at inference from you.
It is not a reranker. A bi-encoder compresses the query and the document independently, so it can never let a query token look at a document token. That constraint is the entire reason you can precompute the corpus, and it costs real accuracy. Chapter 3 measures the cost and shows how to buy some of it back.
It is not an index. This is the one that produces the most confusing production incidents. Finding the nearest neighbours among 400 million vectors is a separate system — HNSW, IVF-PQ, ScaNN — and those systems are approximate. They deliberately trade recall for speed. Your end-to-end recall is the product of two independent recalls:
Put numbers on it. Suppose your embedder genuinely places the right document in its true top 10 for 84% of queries, and your approximate index returns the true top 10 for 95% of queries. Then
Four points of recall vanished into a configuration file you have probably never opened. Tighten the index's search parameters until its recall against exact search is 0.99 and you recover 0.832 — a bigger improvement than most embedder upgrades deliver, at the cost of some query latency. Before you replace your model, measure your index against brute-force search on a thousand queries. It takes an hour and it is frequently the whole problem.
Everything in this lesson, and every embedder released since, is a choice in five slots. Fix the slots in your head now; the remaining nine chapters fill them in.
[CLS], last token, or a trained latent-attention block. The one line that decides what survives.E5's contribution is not any one slot. Contrastive learning is from 2018. In-batch negatives are from 2020. Prefixes are barely an idea. E5's contribution is a configuration — a specific, reproducible fill of all five slots, executed at a scale nobody outside a large lab had tried, with an ablation table showing which parts carried the weight. It is an engineering paper, and it is the most influential embedding paper of its year precisely because it is one.
query: and passage: are load-bearing → instructions that give one model many geometries → how decoder LLMs got turned into embedders by three different surgeriesContrastive learning has one non-negotiable input: pairs. Two texts that belong together, so the model has something to pull close, with everything else in the batch acting as the thing to push away. The method is simple. The bottleneck is arithmetic.
Count the world's supply of human-judged query–passage pairs as of 2022. MS MARCO passage ranking, built from real Bing queries with human relevance labels, contributes about 500,000 training pairs. Natural Questions contributes roughly 58,000 after filtering. Add NLI corpora, add TriviaQA, add HotpotQA, add every retrieval dataset anyone has released. You reach a few million pairs, and you have exhausted the entire annotated corpus of humanity.
A few million is not enough. CLIP trained on 400 million image–text pairs. The scaling behaviour of contrastive learning is unforgiving: the model learns the geometry of the pair distribution, and a small pair distribution yields a small geometry, correct in the neighbourhoods it saw and arbitrary everywhere else. That is exactly the BM25-loses-to-nobody failure from Chapter 0, restated as a data problem.
Look at how text appears on the web and the pairs are obvious once you see them. A scientific paper has a title and an abstract: the title is a compressed query, the abstract is its answer. A Stack Exchange post has a question and an accepted answer. A Reddit post has a title and its top comment. A Wikipedia article has section headings and the paragraphs beneath them. A news article has a headline and a body.
None of these were written as retrieval training data. All of them encode the same underlying relation: a short piece of text that names a topic, and a longer piece of text that elaborates on it. That is exactly the relation a search engine needs to learn.
E5 harvests six such sources and calls the result CCPairs — Colossal Clean text Pairs, named in deliberate echo of the C4 web corpus.
| Source | What plays the query role | What plays the passage role | Where the relation is honest, and where it lies |
|---|---|---|---|
| Post title (and body) | Top comment | Honest when the comment answers the post. Lies constantly: jokes, tangents, "this", and comments that reply to other comments | |
| Stack Exchange | Question title + body | Upvoted answer | The cleanest source in the set — the relation is literally curated by voting, and the vocabulary gap between question and answer is real |
| English Wikipedia | Entity name + section title | The section's passage | Honest for factual lookup. Systematically biased toward encyclopedic register; "External links" sections are pure noise |
| Scientific papers | Paper title | Abstract | Very high precision, very narrow domain. Teaches technical vocabulary alignment better than anything else in the mix |
| Common Crawl | Page title | Page passage | Enormous and filthy. "Home | Acme Inc" paired with a cookie banner is a valid extraction and a worthless pair |
| News | Headline | Article body | Honest, but headlines are optimised for clicks, so the pair sometimes teaches sensationalism rather than aboutness |
After simple heuristic cleaning — length bounds, language identification, dropping pairs with too few alphanumeric characters, deduplication — this yields roughly 1.3 billion text pairs. Three orders of magnitude more than the world's labelled supply, obtained by scraping rather than annotating.
The phrase gets used loosely, so pin it down. A weak label is a signal correlated with the thing you want but not equal to it. Here the thing we want is relevance — would a person say this passage answers this query? — and what we observe is co-occurrence: these two spans of text appeared adjacent on a web page.
The assumption is a probabilistic inequality, and stating it as one makes the risk visible:
The right-hand side is essentially zero: two random web spans are almost never relevant to each other. So the inequality is easy to satisfy, and it is all the inequality guarantees. It says nothing about how large the left side is. If 40% of Reddit title–comment pairs are jokes, the inequality still holds comfortably while 40% of your training positives are wrong.
So: 1.3 billion pairs, an unknown but large fraction of them wrong, and no budget to look at them. What can you actually do?
Option 1: hand-label a sample and train a classifier. Reasonable, and this is what most industrial pipelines do. It requires an annotation guideline, annotators, agreement measurement, and a classifier that generalises across six wildly different sources. It is a project, not a step.
Option 2: heuristics. Drop comments under five words, drop pairs with high lexical overlap, drop Common Crawl pages with navigation-heavy text. Cheap, and it catches the dumbest failures — which is exactly why it was already applied to get from raw scrape to 1.3B. Heuristics cannot see semantics: "same" is five characters, and so is "Yes, use pandas.read_parquet".
Option 3 — the one E5 takes: ask a model that has read all of it.
Here is the move, and it is worth building up slowly because it looks circular at first and is not.
Train an embedder on the full noisy 1.3B. It will be mediocre — 40% of its positives were lies. But it will not be random, and the reason is statistical: a pattern has to recur to shape a model's weights. One post where "same" answers a pizza question contributes one gradient among a billion. The pattern "a title about a topic goes with a paragraph about that topic" appears in hundreds of millions of pairs. Gradient descent over a billion examples is an averaging machine, and the average of mostly-signal plus scattered-noise is signal.
Now make "agrees with the consensus" testable. Given a pair (q, p), hide p in a pool of about one million randomly sampled passages, embed everything with the noisy model, and rank the pool by cosine similarity to q. If p comes out near the top, the model — which learned from everyone else's pairs — independently agrees that p is the right answer to q. If p is buried at rank 40,000, the model does not see the connection, and the model is speaking for a billion other examples.
Keep the pair only if p lands in the top-k. E5 sets k very small; the paper's setting is k = 2. This is consistency-based filtering, and it takes 1.3 billion pairs down to roughly 270 million — about one pair in five survives.
The rank criterion sounds sophisticated. Let us do the arithmetic and discover it is not, which is a much more useful thing to know than the criterion itself.
Fix a query q. Score it against the one-million-passage pool. Those scores are cosines between a query embedding and a million essentially unrelated passage embeddings. Each cosine is a sum of 768 coordinate products; by the central limit theorem the sum is approximately Gaussian. Empirically, mean-pooled encoders are anisotropic — their embeddings occupy a narrow cone rather than the whole sphere — so the background cosine sits well above zero. Take a concrete, realistic model:
The pair survives at rank ≤ 2 when fewer than two of the million pool passages beat p's score. The expected number that beat a score s is the pool size times the tail probability:
Set that expectation to 2 and solve for the score. We need P(Z > z) = 2 × 10−6. Reading the normal tail: P(Z > 4.61) ≈ 1.99 × 10−6. So z* = 4.61, and
Now run four candidate pairs through it, by hand.
| Pair | Model's cos(q, p) | z = (s − 0.15)/0.06 | P(Z > z) | Expected # better in 1M | Verdict at k = 2 |
|---|---|---|---|---|---|
| Stack Exchange Q & accepted answer | 0.62 | 7.83 | 2.4 × 10−15 | 0.0000000024 | Kept — rank 1, not close |
| Paper title & abstract | 0.45 | 5.00 | 2.87 × 10−7 | 0.29 | Kept — expected rank about 1.3 |
| News headline & loosely-related body | 0.40 | 4.17 | 1.53 × 10−5 | 15.3 | Dropped — rank about 16 |
| Reddit post & "same" | 0.30 | 2.50 | 6.21 × 10−3 | 6,210 | Dropped — rank about 6,211 |
Check the third row's tail by hand so you trust the table. The standard normal density at z is φ(z) = (1/√(2π)) e−z²/2. At z = 4.167: z²/2 = 8.682, e−8.682 = 1.70 × 10−4, so φ = 0.3989 × 1.70 × 10−4 = 6.78 × 10−5. The tail is well approximated by φ(z)/z for large z: 6.78 × 10−5 / 4.167 = 1.63 × 10−5, and the next correction factor (1 − 1/z²) = 0.942 brings it to 1.53 × 10−5. Times a million: 15.3 passages beat it. Rank 16. Dropped.
| k (rank cutoff) | Required tail P | z* | Score threshold s* | Change from k = 2 |
|---|---|---|---|---|
| 1 | 1 × 10−6 | 4.753 | 0.4352 | +0.009 |
| 2 (E5) | 2 × 10−6 | 4.611 | 0.4266 | — |
| 10 | 1 × 10−5 | 4.265 | 0.4059 | −0.021 |
| 100 | 1 × 10−4 | 3.719 | 0.3731 | −0.054 |
| 1,000 | 1 × 10−3 | 3.090 | 0.3354 | −0.091 |
| 10,000 | 1 × 10−2 | 2.326 | 0.2896 | −0.137 |
A ten-thousand-fold change in k moves the threshold by 0.14 cosine. The reason is that the Gaussian tail decays like e−z²/2, so inverting it gives z ≈ √(2 ln(1/P)) — the threshold grows with the square root of the logarithm of the cutoff. Square root of a logarithm is the most inert function in applied mathematics. This is why the paper can pick k = 2 without an elaborate sweep: almost any small k lands in the same place.
Twenty percent survival sounds like a lot of thrown-away data. It is. Let us quantify what was bought, using a model of pair quality chosen so that it reproduces the one number the paper publishes — 1.3B in, 270M out.
Model the 1.3 billion pairs as a mixture: 45% are genuinely aligned, with model scores distributed Normal(0.415, 0.10), and 55% are weak or wrong, with scores Normal(0.18, 0.09). (This is a modelling choice, not a paper result — E5 does not publish the score distribution. It is here so the trade-off becomes computable.) Apply the threshold s* = 0.4266:
Total survival: 0.2042 + 0.0017 = 0.2059, which is 20.6% of 1.3 billion ≈ 268 million pairs. The model is calibrated. Now read the two numbers that matter:
| Quantity | Before filtering | After filtering | Interpretation |
|---|---|---|---|
| Pairs | 1,300M | 268M | 79% of the corpus discarded |
| Purity (fraction of positives that are genuinely aligned) | 45% | 0.2042 / 0.2059 = 99.2% | The false-positive problem is essentially eliminated |
| Recall of aligned pairs | 100% | 0.2042 / 0.45 = 45.4% | More than half of the good pairs were thrown away too |
Read that last row again. The filter is not a scalpel. It discards the majority of the correct pairs in order to reach near-perfect purity. Is that the right trade?
Yes, and the argument is the asymmetry from earlier in this chapter. A discarded true positive costs one gradient you had 268 million replacements for. A retained false positive costs a gradient pointing the wrong way, plus the geometric distortion of resolving the conflict. When the two error types have wildly different costs, the optimal operating point is nowhere near the balanced one — you push hard toward the cheap error. Filtering aggressively is the same instinct as a spam filter with a low false-positive tolerance, and for the same reason.
Drag the rank cutoff k and the pool size and watch the funnel. The upper panel shows the two score populations and where the induced threshold falls; the lower panel shows survivors, purity, and recall. The default settings reproduce the paper's 1.3B → 270M. Notice how far you have to drag k before anything meaningful changes, and how much more the pool size does.
A filter you cannot afford is a thought experiment. Walk the compute.
Step 1 — embed the pool once. One million passages through BERT-base at 128 tokens each. Store as fp16: 106 × 768 × 2 bytes = 1.54 GB. That fits in the high-bandwidth memory of a single accelerator, which is the whole reason this design is affordable. The pool never changes, so this cost is paid once.
Step 2 — embed every pair. 1.3 billion queries and 1.3 billion passages, 2.6 billion forward passes. This, not the ranking, is the dominant cost, and it is unavoidable — you had to embed them to train the noisy model anyway, and you can cache.
Step 3 — rank. For each query embedding q of shape (768,), one matrix–vector product against the (1e6, 768) pool gives all million scores: 2 × 106 × 768 ≈ 1.5 GFLOP. Batch 4,096 queries at a time and it becomes a (4096, 768) @ (768, 1e6) matmul — the shape GPUs are fastest at. Across 1.3 billion queries that is about 2 × 1018 FLOPs, which on a cluster achieving 1017 FLOP/s is a matter of hours, not weeks.
Step 4 — the early exit. You do not need the rank. You need the boolean "were fewer than 2 scores greater than s(q,p)?". That is a comparison and a count, fused into the same kernel that produced the scores, with no sort anywhere. Sorting a million floats 1.3 billion times would have been the actual bottleneck; noticing you never needed the sort is the difference between a feasible pipeline and an infeasible one.
Consistency filtering is E5's answer. It is not the only one, and the alternatives are informative about the design space.
| Model | Pair source | Quality control | Scale reaching contrastive training |
|---|---|---|---|
| E5 (2022) | CCPairs — 6 web sources | Consistency filter with a model trained on the noisy set | 270M |
| BGE / C-Pack (2023) | C-MTP unlabelled — similar web harvesting, English and Chinese | Heuristic and rule-based cleaning; an added RetroMAE pretraining stage instead of a filter | Roughly 200M |
| GTE (2023) | Multi-source web pairs | Source-level sampling weights rather than pair-level filtering | Roughly 800M |
| E5-Mistral (2024) | Pairs an LLM was asked to write | Quality is controlled at generation time by the prompt, so no filter is needed | About 500k, plus labelled data |
| LLM2Vec (2024) | No pairs at all — a sequence paired with a differently-dropped-out copy of itself | Not applicable; positives are constructed, not found | Wikipedia only |
Read the last two rows next to the first. E5 spends enormous effort finding and cleaning naturally occurring pairs. E5-Mistral, from the same lab fourteen months later, skips the entire problem by having a language model write the pairs. LLM2Vec skips it in the opposite direction by manufacturing positives out of dropout noise. Chapter 6 is about why both of those became possible, and what each one gives up.
We have 270 million filtered pairs. Now: what is the loss?
Start from the question you actually want the model to answer, not from a formula. Hand the model a query and a shortlist of passages, exactly one of which goes with it. Ask: which one? That is an N-way classification problem — and it is a strange one, because the classes are not fixed. They change every step. There is no weight matrix with one row per class, because there is no fixed set of classes. The "class vectors" are just the passage embeddings, recomputed from scratch on every batch.
Once you see it that way, the loss writes itself. You want the softmax over the shortlist to put its mass on the correct passage, so you use cross-entropy, and the logits are similarities.
Symbol by symbol. qi is the L2-normalised embedding of the i-th query in the batch. pi is the embedding of its true partner passage. pj ranges over every passage in the batch, including the correct one. τ (tau) is the temperature, a positive scalar that divides every similarity before the exponential — we will spend half this chapter on it. N is the batch size.
This loss has a name from the representation-learning literature: InfoNCE, for Noise-Contrastive Estimation of mutual information, introduced by van den Oord and colleagues in 2018. The name describes its origin — it lower-bounds the mutual information between q and p — rather than what it does day to day, which is: pick the right one out of N.
Put numbers on "free." A batch of N pairs requires 2N forward passes — N queries and N passages. From those 2N forward passes you obtain N × (N − 1) query–negative comparisons, because every query is compared against every non-matching passage. The comparison itself is a dot product, essentially free next to a transformer forward pass.
| Batch size N | Forward passes (2N) | Negative comparisons N(N−1) | Comparisons per forward pass |
|---|---|---|---|
| 8 | 16 | 56 | 3.5 |
| 256 | 512 | 65,280 | 127.5 |
| 4,096 | 8,192 | 16,773,120 | 2,047.5 |
| 32,768 (E5) | 65,536 | 1,073,709,056 | 16,383.5 |
Check the last row: 32,768 × 32,767 = 1,073,709,056, just over a billion query–negative comparisons, from 65,536 transformer forward passes. Divide: 16,383.5 comparisons purchased per forward pass. The ratio grows linearly in N, which is why every lab that could afford a bigger batch took one.
But "more comparisons" is not automatically "more learning", and that is where the interesting part starts.
Here is the claim this chapter is built to earn: at the temperatures these models actually use, the loss is dominated by a single negative — the hardest one in the batch. The other 32,766 contribute almost nothing. So batch size does not matter because it gives you more negatives. It matters because it gives you a bigger pool to draw the hardest negative from.
Prove it with arithmetic. Take a partially trained model and a realistic batch. The correct passage scores cos = 0.75. There is one genuinely confusable passage at 0.65, one mildly confusable at 0.55, and 32,765 unrelated ones sitting near the background level of 0.15. Compute the softmax denominator, term by term, at three temperatures. Every term is measured relative to the positive, that is, exp((s − 0.75)/τ).
| Term | s | τ = 0.01 | τ = 0.05 | τ = 0.2 |
|---|---|---|---|---|
| Positive | 0.75 | 1 | 1 | 1 |
| Hardest negative | 0.65 | e−10 = 0.0000454 | e−2 = 0.1353 | e−0.5 = 0.6065 |
| Second negative | 0.55 | e−20 = 2.1×10−9 | e−4 = 0.0183 | e−1 = 0.3679 |
| 32,765 background | 0.15 | 32,765 × e−60 = 2.9×10−22 | 32,765 × e−12 = 0.2013 | 32,765 × e−3 = 1,631.4 |
| Denominator sum | — | 1.0000454 | 1.3549 | 1,633.4 |
| Loss = ln(sum) | — | 0.0000454 | 0.3038 | 7.398 |
Now read the columns as three different worlds.
At τ = 0.01 — E5's setting — the entire negative mass is 4.54 × 10−5, and 99.995% of it belongs to the single hardest negative. The 32,765 background passages contribute 2.9 × 10−22, which is not a small number, it is an absent number: it vanishes below the resolution of the accumulator and does not exist in the computation at all. The loss behaves exactly like a triplet loss against one carefully chosen adversary.
At τ = 0.05 the picture flips in a genuinely surprising way. The hardest negative contributes 0.1353. The crowd of 32,765 easy negatives contributes 0.2013 — more. Each one is negligible; there are enough of them that negligible times thirty-two thousand is the dominant term. At this temperature, batch size matters for the reason people usually assume: volume.
At τ = 0.2 the crowd contributes 1,631 out of a denominator of 1,633. The hard negative is 0.037% of the signal. The model is being told, with overwhelming emphasis, "you are not any of these thousands of random things", which it already knew. The loss is 7.4 and mostly measures batch size, not model quality.
If the loss cares about the maximum over the batch, then the question "should I use a bigger batch?" becomes the question "how does the maximum of N − 1 similarity scores grow with N?" That is a question extreme-value statistics answers in closed form, and the answer is discouraging in an instructive way.
Take a fixed query embedding u on the unit sphere in d dimensions, and a passage embedding v drawn uniformly at random from that sphere. Their cosine is u · v, a sum of d coordinate products. It has mean 0 by symmetry, and its variance is exactly 1/d. For d in the hundreds, the central limit theorem makes it approximately Gaussian:
The maximum of M independent draws from Normal(0, σ²) concentrates near σ√(2 ln M). Substituting σ = 1/√d and M = N − 1:
Evaluate it for d = 768, doing one by hand so the rest are believable. At N = 32,768: ln(32,767) = 10.397, so 2 × 10.397 = 20.794, divided by 768 gives 0.027076, and the square root is 0.1646.
| Batch N | ln(N − 1) | Hardest random negative, d = 768 | Relative to N = 8 |
|---|---|---|---|
| 8 | 1.946 | 0.0712 | 1.00× |
| 256 | 5.541 | 0.1201 | 1.69× |
| 4,096 | 8.318 | 0.1472 | 2.07× |
| 32,768 | 10.397 | 0.1646 | 2.31× |
| 1,048,576 | 13.863 | 0.1900 | 2.67× |
Read the last column with care. Multiplying the batch by 4,096 makes the hardest negative only 2.31 times harder. The scaling is √(ln N) — the square root of a logarithm again, the same inert function that made k irrelevant in Chapter 1. To keep making progress you must keep multiplying the batch, and the returns keep shrinking. Every lab discovered this the expensive way.
You do not. Not on one device, and not in the way "batch size" usually means. Three things have to be true at once, and each requires a specific engineering trick.
Problem 1: the similarity matrix. The loss needs all N² pairwise cosines. At N = 32,768 in fp32 that is 32,768² × 4 bytes = 4.295 GB for the forward matrix, and the backward pass wants its gradient, another 4.295 GB. Nearly 9 GB before a single transformer activation has been stored.
Problem 2: the activations. 65,536 sequences of a few hundred tokens through a 12- or 24-layer transformer, with all intermediate activations retained for backpropagation, is off by orders of magnitude from any accelerator's memory. The standard fix is gradient checkpointing: store only the layer boundaries and recompute the interiors during the backward pass, trading roughly 30% extra compute for a large memory reduction.
Problem 3: the negatives live on other machines. Split the batch across G devices and each device holds N/G pairs. Its local queries can only see N/G − 1 local negatives — you have not built a batch of 32,768, you have built G independent batches of N/G, which by the table above is dramatically weaker.
The fix for problem 3 is the one worth internalising, because it is what "large-batch contrastive training" actually means in code. After each device computes its local embeddings, an all-gather collects every device's embeddings onto every device. Now each device holds the full (N, d) matrix of passage embeddings and can score its local queries against all of them.
The communication cost is startlingly small. At N = 32,768 and d = 1024 in fp16:
Sixty-seven megabytes per step, over an interconnect built for hundreds of gigabytes per second. This is the asymmetry that makes the whole design work: embeddings are tiny compared with activations. You cannot ship activations between devices, but you can ship the 1024 numbers each sequence collapses to, and the 1024 numbers are all the loss ever needed.
Every in-batch negative is assumed to be a negative. It is an assumption, and at a batch of 32,768 drawn from 270 million pairs, it will sometimes be false. A false negative is a passage in the batch that would actually be a correct answer to the query but is being pushed away anyway.
Estimate the rate. Suppose a query has r genuinely valid passages somewhere in the 270M-pair corpus. The chance that a random batch of size N contains at least one of them is
Work two cases.
A specific technical query with r = 3 valid passages: 1 − (1 − 1.214×10−4)3 = 1 − 0.999636 = 0.00036. Roughly one batch in 2,700. Ignorable.
A common query like "what is machine learning", where r = 1,000 passages in a web-scale corpus would serve: 1 − (1 − 1.214×10−4)1000. Since (1 − x)n ≈ e−nx for small x, this is 1 − e−0.1214 = 1 − 0.8857 = 0.114. Eleven percent of the time, this query is trained to push away a passage that answers it.
E5's stage one lives with this. It uses in-batch negatives only — no mined hard negatives at all — precisely because mined negatives make the false-negative problem dramatically worse. Chapter 3 is about the second stage, where hard negatives are introduced deliberately, and about the machinery required to survive them.
Top: the hardest-random-negative curve, √(2 ln(N−1)/d), with your batch marked. Bottom: the softmax denominator split into positive, hard negatives, and background crowd, at your temperature. Drag τ up from 0.01 and watch the crowd bar appear out of nothing — that is the moment the loss stops being hardest-negative mining and starts being volume.
Push d to 4096 — the hidden size of a 7B decoder LLM — and watch the hardest random negative collapse. At d = 4096 and N = 32,768 it is √(20.794/4096) = 0.0713, the same hardness a 768-dimensional model gets from a batch of eight. High-dimensional spaces are emptier, random vectors are more nearly orthogonal, and luck stops producing hard negatives at all. That is a preview of why the 7B-parameter embedders in Chapter 6 lean on curated and synthesised hard negatives rather than on batch size.
Stage one produced a model that is very good at a task nobody needs: telling a passage about cooking apart from a passage about Kubernetes. Random in-batch negatives are, overwhelmingly, from a different topic than the query. Train on a billion of those comparisons and you get an excellent topic detector.
Real retrieval failures do not look like that. They look like this:
Both passages are about boiling water. Both share vocabulary with the query. A topic detector rates them nearly equally. Only B answers the question, and the difference lives in one clause. No batch of 32,768 random web passages will ever put A and B side by side and force the model to choose — the odds of drawing a passage that specific are essentially zero.
So you have to go and find them.
The definition writes itself once you know what you are missing. A hard negative is a passage that is not the correct answer to a query but that the current model scores highly anyway. It is, by construction, a mistake the model is currently making.
And there is an obvious place to find them: run the model. For each training query, retrieve the top few hundred passages from the corpus with the stage-one model. Remove the known positive. What remains is a ranked list of the model's most confident errors, sorted by how confident it is.
Step 3 hides the entire difficulty. Which rank window? Rank 1 is the hardest negative available, so surely take rank 1?
No — and the reason is the most important practical fact in dense retrieval training.
Retrieval datasets are sparsely labelled. MS MARCO, the workhorse of the field, was built by showing annotators the top Bing results for a real query and asking them to mark passages that contained the answer. On average roughly one passage per query ends up labelled relevant. But the corpus contains 8.8 million passages, and for a query like "how long does it take to boil an egg" there are certainly dozens of passages that answer it correctly. They are unlabelled, not irrelevant.
So when you retrieve the top 200 and call everything except the single labelled passage a negative, you are lying, and you are lying most at the top of the list — because a good retriever puts the genuinely relevant unlabelled passages there.
Quantify it. Suppose a typical MS MARCO query has about 7 truly relevant passages in the corpus, of which 1 is labelled. A decent stage-one retriever will rank most of the other 6 highly — say 4 of them land in the top 30 and 2 more in ranks 31–200.
| Sampling window | Candidates in window | Unlabelled positives in window | Chance a sampled "negative" is actually relevant |
|---|---|---|---|
| Ranks 1–10 | 9 (gold removed) | ~2.5 | 28% |
| Ranks 1–30 | 29 | 4 | 13.8% |
| Ranks 31–200 | 170 | 2 | 1.2% |
| Ranks 100–200 | 101 | ~0.6 | 0.6% |
| Random from corpus | 8,800,000 | 6 | 0.00007% |
Now recall Chapter 2's temperature arithmetic and see how bad 13.8% really is. Take a positive at cos 0.75, a false negative (secretly relevant) at 0.72, and the hardest genuine negative at 0.65, all at τ = 0.01. Relative softmax weights against the positive:
Ratio: 0.04979 / 0.0000454 = 1,097. The mislabelled passage receives about a thousand times more gradient than the hardest correct negative in the batch. You did not add a little noise to your training signal. You handed the loudest voice in the room to the one example that is wrong.
To denoise negatives you need a judge better than the model being trained. The field has one, and it has been sitting there since 2019.
Everything so far has been a bi-encoder: q goes through the network alone, p goes through the network alone, and the only interaction between them is one dot product at the very end. Call that the late-interaction constraint. It exists so that you can embed the corpus once, offline, and answer queries with a matrix multiply.
A cross-encoder throws that constraint away. It concatenates the two texts into one sequence — [CLS] query [SEP] passage [SEP] — runs the whole transformer over the pair, and reads a relevance score off the [CLS] position through a small linear head.
| Bi-encoder | Cross-encoder | |
|---|---|---|
| Input | Two separate sequences | One concatenated sequence |
| Query–passage token interaction | None — one scalar at the end | Every layer, every head, every token pair |
| Output | (d,) vector per text | (1,) scalar per pair |
| Corpus precomputation | Yes — embed once, reuse forever | Impossible — the score depends on the query |
| Cost per query over 1M passages | 1M dot products ≈ 1.5 GFLOP | 1M transformer forwards ≈ 5 × 1016 FLOP |
| Accuracy on MS MARCO reranking | Good | Substantially better |
Do the last-but-one row's arithmetic, because the gap is the entire reason both architectures exist. A BERT-base forward pass over a 256-token pair is roughly 2 × 110M parameters × 256 tokens ≈ 5.6 × 1010 FLOPs. Over a million passages that is 5.6 × 1016 FLOPs, which at an achieved 1014 FLOP/s takes about 560 seconds per query. The bi-encoder's million dot products take about 15 microseconds. The ratio is roughly 3 × 107.
So we have a slow, accurate teacher and a fast, less accurate student. Knowledge distillation is the standard move: instead of training the student against the ground-truth label, train it to reproduce the teacher's distribution.
Concretely, for one query with a candidate list of M passages: run the cross-encoder on all M pairs to get teacher scores t1..M; softmax them into a target distribution P; softmax the student's cosines into Q; minimise the Kullback–Leibler divergence
and blend it with the ordinary contrastive term:
E5's stage two is exactly this: fine-tune on a mixture of labelled datasets (natural language inference pairs, MS MARCO passage ranking, and Natural Questions) using mined hard negatives, with a linear interpolation of the cross-entropy contrastive loss and a KL term against a cross-encoder teacher.
Take a single training instance. The candidate list is the gold passage plus three mined negatives, one of which is secretly relevant.
| j | Passage | Cross-encoder logit tj | Student cos(q, pj) |
|---|---|---|---|
| 1 | Gold (labelled positive) | 8.2 | 0.71 |
| 2 | Mined negative — actually relevant, unlabelled | 5.1 | 0.68 |
| 3 | Mined negative — same topic, wrong answer | 4.6 | 0.55 |
| 4 | Random negative | −1.3 | 0.12 |
Teacher distribution. Exponentiate: e8.2 = 3,640.95, e5.1 = 163.99, e4.6 = 99.48, e−1.3 = 0.2725. Sum = 3,904.69. Divide:
Read that before continuing. The teacher is confident the gold wins, and it assigns the secretly-relevant passage 4.2% — a hundred times more than it gives the random one. The one-hot label would have assigned it 0. That 4.2% is the entire denoising signal, and it exists because the teacher is a relevance model rather than a lookup of what an annotator happened to click.
Student distribution at τ = 0.01. Logits are cosines divided by 0.01: (71, 68, 55, 12). Take everything relative to the largest: e0 = 1, e−3 = 0.049787, e−16 = 1.125×10−7, e−59 ≈ 2.2×10−26. Sum = 1.049787.
The contrastive loss with a one-hot target on the gold is just −ln Q1 = −ln(0.95257) = 0.0486. By that measure the student is nearly perfect and there is almost nothing to learn.
The KL term, computed term by term:
| j | Pj | Qj | ln(Pj/Qj) | Pj ln(Pj/Qj) |
|---|---|---|---|---|
| 1 | 0.9325 | 0.95257 | −0.0213 | −0.0199 |
| 2 | 0.0420 | 0.047425 | −0.1214 | −0.0051 |
| 3 | 0.0255 | 1.072×10−7 | +12.379 | +0.3154 |
| 4 | 0.0000698 | 2.1×10−26 | +49.5 | +0.0035 |
| LKL | 0.2938 | |||
Two things just happened that are worth more than the numbers.
First, the two losses disagree about whether the model is fine. Cross-entropy says 0.049 — nothing to see. KL says 0.294, six times larger, and 93% of it comes from row 3: the student has crushed a same-topic passage to a probability of 10−7 when the teacher thinks it deserves 2.6%. The student is not underconfident; it is catastrophically overconfident, and only the soft target can see it.
Second, that overconfidence is a direct consequence of τ = 0.01. Redo the student distribution at τ = 0.05: logits (14.2, 13.6, 11.0, 2.4), relative exponentials 1, e−0.6 = 0.54881, e−3.2 = 0.040762, e−11.8 = 7.49×10−6, sum 1.58958:
Recompute the KL: 0.9325 × ln(0.9325/0.6291) = 0.9325 × 0.3935 = +0.3670; 0.0420 × ln(0.0420/0.3453) = 0.0420 × (−2.1066) = −0.0885; 0.0255 × ln(0.0255/0.0256) = −0.0002; 0.0000698 × ln(14.8) = +0.0002. Total 0.2785.
Almost the same magnitude — and pointing in the opposite direction. At τ = 0.05 the dominant term is row 1: the student now thinks gold and the secretly-relevant passage are nearly tied (0.629 versus 0.345) while the teacher is sure the gold wins, so KL pushes the student to separate them more. At τ = 0.01 the dominant term was row 3 and KL pushed the student to separate less.
With L = α LInfoNCE + (1 − α) LKL, the two extremes fail in different, predictable ways.
| α | What dominates | Failure mode |
|---|---|---|
| 1.0 (no distillation) | One-hot cross-entropy | Trained to hate unlabelled positives; the sparser the labels, the worse the damage |
| 0.7–0.9 | Contrastive with a soft correction | Typical working range — the contrastive term supplies the strong "gold wins" signal, KL sands off the false negatives |
| 0.0 (pure distillation) | Teacher's distribution only | Student is capped by the teacher and inherits its biases; nothing anchors it to the actual labels, so teacher errors are reproduced faithfully |
There is a subtler reason not to set α = 0. The teacher only ever saw the candidate list you gave it. It has no opinion about the other 32,767 in-batch passages, so a pure-KL objective supplies no signal at all against them — you would silently lose the entire in-batch negative mechanism from Chapter 2. The contrastive term is what keeps the global geometry honest; the KL term refines the local ordering inside each candidate list.
Top: the ranked candidate list, with truly-relevant passages marked. Drag the sampling window and watch how many secret positives you scoop up. Bottom: the resulting gradient budget — how much of the negative push lands on genuine negatives versus on passages that deserved to be kept. Turn on the cross-encoder teacher and watch the misdirected share collapse.
Stage one's batch was flat: N queries, N passages, one similarity matrix. Stage two's is grouped, and the shapes change accordingly.
| Tensor | Shape | Meaning |
|---|---|---|
queries | (G, Lq) | G query groups per device, tokenised |
passages | (G, 1 + H, Lp) | Per group: 1 gold plus H mined hard negatives. Typically H = 7 |
teacher_scores | (G, 1 + H) | Precomputed offline, once, and cached — the cross-encoder never runs during training |
q_emb | (G, d) | After encoding and normalisation |
p_emb | (G × (1+H), d) | Flattened — this is the trick in the next row |
scores | (G, G × (1+H)) | Every query against every passage in the batch, so another group's hard negatives become this group's in-batch negatives |
That last row is the quiet efficiency of the design. With G = 64 groups and H = 7, each query sees 64 × 8 = 512 passages: its own 8, plus 504 others that were mined as hard negatives for different queries. Those borrowed negatives are topically diverse but individually difficult, which is a better negative distribution than either pure random or pure own-query mining would give.
And note where the cross-encoder is: entirely offline. You score the mined candidates once, write the numbers to disk beside the training data, and never load the teacher into GPU memory during training at all. A distillation setup that runs the teacher online costs several times the training throughput for no benefit when the candidate lists are fixed in advance.
Here is E5's most-copied idea, in its entirety. Before encoding, prepend a literal string:
Two words and two colons. No new parameters, no architecture change, four tokens of extra compute. It looks like a formatting convention someone added for tidiness. It is load-bearing, and removing it breaks the model.
This chapter is about why — and the argument starts somewhere that seems unrelated: with the fact that cosine similarity is symmetric and relevance is not.
A function is symmetric when swapping its arguments does not change the answer. Cosine similarity is symmetric by construction:
Now consider the relation retrieval actually needs: p answers q. Ask whether it is symmetric by trying it in both directions.
| Direction | Statement | True? |
|---|---|---|
| q → p | "To end your subscription, open Billing…" answers "how do I cancel my plan" | Yes |
| p → q | "how do I cancel my plan" answers "To end your subscription, open Billing…" | Nonsense — the second is not a question |
The relation is asymmetric, and it is asymmetric in a structural way, not a superficial one. A query is short, underspecified, ungrammatical, and often a fragment. A passage is long, self-contained, well-formed, and contains ninety percent material the query never mentioned. They are different kinds of object, and the relation between them runs one way.
So: we need to model an asymmetric relation with a symmetric kernel. That sounds impossible, and the resolution is worth stating carefully because it is the whole trick.
Once you know you need fQ ≠ fP, there are exactly three implementations, and the field tried them in this order.
| Approach | How | Parameters | Can it do symmetric tasks? | Used by |
|---|---|---|---|---|
| One shared encoder, no marker | fQ = fP | 1× | Yes — it is the only thing it can do | Sentence-BERT, SimCSE |
| Two independent towers | Two full copies of BERT, trained jointly | 2× | No — there is no single map to apply to both sides | DPR (2020) |
| One encoder + input prefix | fQ(x) = f("query: " + x), fP(x) = f("passage: " + x) | 1× | Yes — use the same prefix on both sides | E5, and nearly everything after |
The third column explains why nobody uses two towers any more: it doubles the model for one capability. The fourth column explains why the prefix beat two towers, and this is the argument most summaries of E5 miss.
A two-tower model has permanently committed to asymmetry. Ask it to score sentence-similarity — "the cat sat on the mat" versus "a cat is sitting on a mat", where the correct relation is symmetric — and you must arbitrarily assign one sentence to the query tower and one to the passage tower. The score you get depends on a coin flip. A prefixed model simply uses "query: " on both sides, recovering a genuinely symmetric function from the same weights. This is not a hack; it is what the E5 model cards instruct you to do for symmetric tasks.
query: on one side, passage: on the other. Symmetric mode: query: on both. The choice costs nothing at training time — both modes are exercised because the training mixture contains both retrieval data and symmetric NLI data — and nothing at inference time. Chapter 5 takes this idea and turns the dial all the way up.Abstract arguments about symmetry are easy to nod along to. Do the numbers.
Three texts, and a symmetric encoder trained on paraphrase and inference data (which is what Sentence-BERT-style models are):
| Label | Text |
|---|---|
| q1 | "who wrote hamlet" |
| q2 | "who wrote macbeth" |
| p1 | "Hamlet is a tragedy written by William Shakespeare around 1600. It is his longest play." |
| p2 | "Macbeth is a tragedy by William Shakespeare, first performed in 1606, concerning ambition and prophecy." |
A paraphrase-trained symmetric encoder produces, roughly:
| Pair | Cosine | Why |
|---|---|---|
| cos(q1, q2) | 0.87 | Nearly identical surface form: same length, same register, same four of five words |
| cos(p1, p2) | 0.81 | Same genre, same author, same sentence template |
| cos(q1, p1) | 0.41 | The correct answer — and it scores half as high as an unrelated question does |
| cos(q1, p2) | 0.39 | The wrong answer, essentially tied with the right one |
Retrieval works by ranking, so what matters is the last two rows: the margin between the correct passage and its closest distractor is 0.41 − 0.39 = 0.02.
Is 0.02 enough? That depends on how noisy the scores are. Real embedding scores wobble by a few hundredths depending on phrasing, chunk boundaries, and truncation — call the per-score standard deviation 0.06. The difference of two such scores has standard deviation 0.06√2 = 0.0849. The probability the correct passage ranks first is
Fifty-nine percent. On a two-way choice. The model is barely better than a coin flip on a question a child could answer, and it is not because it fails to understand the text — look at row one, it understands the text beautifully. It has learned paraphrase geometry, and paraphrase geometry says two questions about Shakespeare plays are nearly the same thing while a question and its answer are quite different things. Which, as a statement about English, is true.
Now the prefixed model. Because it saw query:-marked short fragments paired with passage:-marked long documents through 270 million examples, it has learned a map that takes question-shaped text and points it at answer-shaped text:
| Pair (with prefixes) | Cosine |
|---|---|
cos(query: q1, passage: p1) | 0.78 |
cos(query: q1, passage: p2) | 0.52 |
cos(query: q1, query: q2) | 0.83 — symmetric mode still works |
Margin: 0.78 − 0.52 = 0.26, thirteen times larger. Rerun the probability:
From 59.3% to 99.89%. The error rate went from 40.7% to 0.11%, a factor of 370. Nothing about the model's language understanding changed. What changed is which relation the geometry encodes, and that was selected by four tokens of input.
Time to be suspicious. A mean-pooled embedding is the average of L token vectors. Adding two prefix tokens changes the average by a small amount. How does a small change to an average produce a 0.37 shift in cosine?
There are exactly two channels, and separating them is instructive.
Channel A — the direct contribution to the mean. WordPiece splits "query: " into two tokens, query and :. If the query body is 32 tokens, the pooled vector is now an average over 34 instead of 32, and the two prefix tokens own 2/34 = 5.9% of it. For a 200-token passage, "passage: " owns 2/202 = 1.0%.
Channel B — attention-mediated modulation. Inside every one of the twelve transformer layers, every content token attends to the prefix tokens. The value vectors flowing into "cancel" at layer 5 include a contribution routed from the query token. So the prefix does not merely get averaged in at the end; it changes what all the other token vectors are.
Which channel does the work? Rule one out by arithmetic. Suppose Channel A were the whole story: the prefix just adds a fixed vector c, scaled by about 0.06, to the query embedding and a fixed c′ scaled by 0.01 to the passage embedding. Then for unit vectors, a perturbation of relative size ε can move a cosine by at most about ε — roughly 0.06. We observed 0.37. Channel A is off by a factor of six and cannot be the explanation.
One more thing follows from Channel B being the dominant one: the prefix must be in the model's vocabulary and seen during training. It is not a magic token; it is a word whose embedding was shaped by 270 million gradient steps in which it reliably preceded question-shaped text. Invent a new prefix at inference — "search_query: " — and you get an untrained conditioning signal that the model has no reason to interpret correctly.
The prefix is not optional and it is not symmetric across your corpus. Both of those facts have caused real outages.
Failure 1: dropping the prefix. The E5 model cards state plainly that omitting the prefixes degrades quality substantially. It is easy to omit: your embedding library takes a list of strings, your prefix lives in a config file, and someone writes a batch backfill script that skips it. Nothing errors. Cosines are still in a plausible range. Recall quietly drops.
Failure 2 — the worse one: mixing prefixes within one index. Suppose half your corpus was ingested with "passage: " and, after a refactor, the other half with "query: ". The prefix is a strong conditioning signal, so it shifts the entire embedding cluster. Cross-prefix cosines are systematically lower than within-prefix cosines — the two halves of your index sit on nearly disjoint sub-manifolds.
Put numbers on it. If within-prefix neighbours score around 0.78 and cross-prefix neighbours around 0.55, then for any query:-embedded search, every document in the passage: half is penalised by roughly 0.23 relative to the other half. With a background score spread of 0.06, that is nearly four standard deviations. The affected half of your corpus is not merely down-ranked. It is unreachable, and there is no error, no exception, and no log line. The only symptom is that certain documents stopped being found.
Two questions and two passages on the unit circle. With no prefix, the questions huddle together and the passages huddle together — paraphrase geometry. Drag the prefix strength and watch query: rotate each question toward the passage that answers it. The live 2×2 score table below shows the retrieval margin and the induced accuracy. Switch to a symmetric task and watch the prefix become a liability.
Run the symmetric task with the prefix at full strength and the accuracy readout falls. That is not a bug in the simulation; it is the honest consequence of the design. A rotation that helps questions find answers actively hurts when the correct relation is "these two sentences mean the same thing." Which is precisely why E5 tells you to use query: on both sides for those tasks — applying the same rotation to both arguments leaves their angle unchanged.
That last sentence is worth making formal, because it is the cleanest statement of why one encoder plus a prefix beats two towers. If the prefix acts approximately as a rotation R, then in symmetric mode
because a rotation matrix satisfies RTR = I. The prefix cancels out exactly when both sides carry it. A two-tower model cannot do this: its two encoders are different functions with no such algebraic relationship, so there is no way to make it symmetric on demand.
"query: ", on everything. Clustering asks a symmetric question — do these two documents belong together — so you want the mode where both arguments carry the same conditioning and the induced rotation cancels. Using "passage: " on everything would work equally well by the same argument; what you must not do is mix them. The prefix names the relation you are asking about, not the type of the text.query: / passage: prefixes beat two independent encoder towers, even though both make fQ ≠ fP?Chapter 4 established something small and strange: four tokens of input select between two different similarity geometries computed by identical weights. If that is true, an obvious question follows, and two teams asked it independently in late 2022.
Why only two?
If "query: " means "encode this so it points at its answer", then nothing stops you writing "Represent this scientific paper title for finding papers that cite it" and expecting a third geometry. The prefix stops being a type tag and becomes an instruction: a sentence describing the relation you want the embedding to be good at.
Su and colleagues (arXiv:2212.09741) built INSTRUCTOR around a fixed instruction template:
Filled in, that becomes strings like "Represent the Wikipedia question for retrieving supporting documents:" or "Represent the Amazon review for classifying sentiment:" or "Represent the news article for clustering by topic:". The template makes the instruction space structured rather than free-form, which matters for a reason we will get to.
Training data is the interesting part. INSTRUCTOR is fine-tuned on MEDI — a collection assembled from roughly 330 datasets, each paired with a hand-written instruction. The datasets span retrieval, classification, clustering, and similarity, and the point of the mixture is that no single task dominates, so the model must actually read the instruction to know which behaviour to produce. The base model is GTR, a T5-encoder retriever. Evaluated on 70 tasks, 66 of which were unseen during training, it improved over the prior best by about 3.4% on average.
INSTRUCTOR mean-pools over the input text tokens only. The instruction tokens participate in self-attention — they condition every layer, exactly the Channel B mechanism from Chapter 4 — but they are excluded from the average that produces the final vector.
That sounds fussy. It is not; it is arithmetic, and the arithmetic is severe enough to be worth doing.
Suppose you pooled over everything. Write the instruction's mean token vector as c and the text's mean token vector as t, both unit length. With m instruction tokens and L text tokens, the pooled vector is a weighted sum with weights w = m/(m+L) and 1 − w:
Now take two texts a and b that carry the same instruction, so they share the same c. Assume for clarity that c is orthogonal to both text vectors. Then:
Work a short instruction: m = 12 tokens, L = 32 tokens, so w = 12/44 = 0.2727 and 1 − w = 0.7273. Then w² = 0.07437, (1−w)² = 0.52897, and ‖e‖² = 0.60334.
| True text similarity cos(ta, tb) | Numerator w² + (1−w)²·cos | Observed cosine | Shift |
|---|---|---|---|
| 0.30 | 0.07437 + 0.15869 = 0.23306 | 0.3863 | +0.086 |
| 0.80 | 0.07437 + 0.42318 = 0.49755 | 0.8247 | +0.025 |
| Dynamic range (0.80 case minus 0.30 case) | 0.500 → 0.438 | ||
A 12% loss of dynamic range from a short instruction. Now a long one: m = 30, L = 32, so w = 0.4839, w² = 0.23416, (1−w)² = 0.26636, ‖e‖² = 0.50052.
| True cos(ta, tb) | Numerator | Observed cosine |
|---|---|---|
| 0.30 | 0.23416 + 0.07991 = 0.31407 | 0.6275 |
| 0.80 | 0.23416 + 0.21309 = 0.44725 | 0.8936 |
| Dynamic range | 0.500 → 0.266 | |
Forty-seven percent of the usable range destroyed, and every score compressed into the top of the scale — the classic symptom of a badly-pooled embedder, where everything looks 0.8-similar to everything.
Xiao and colleagues' C-Pack release (arXiv:2309.07597) — models, benchmark, and data together — took a deliberately narrower position. BGE uses a single fixed instruction, and applies it only to the query side:
Documents get nothing. No instruction, no prefix, raw text.
At first glance this looks like a weaker version of INSTRUCTOR. It is a different optimisation target, and the reason is a production constraint that a benchmark cannot see.
| Instruction on the query only | Instruction on both sides | |
|---|---|---|
| Documents embedded per corpus | Once | Once per instruction |
| Index size for T tasks over a 400M-document corpus | 400M vectors | 400M × T vectors |
| Cost of adding a new task | Change a string in the query path | Re-embed and re-index the entire corpus |
| Instruction compute cost | Paid on queries only — a few thousand per second | Paid on documents — hundreds of millions, once per task |
Read row two. A corpus of 400 million documents at 1024 dimensions in fp16 is 400M × 1024 × 2 = 819 GB. Multiply by T tasks and you are shipping a rack per task. Query-side-only instruction keeps the index task-agnostic, which is the difference between "conditioning is a feature" and "conditioning is a capacity plan."
"Index size multiplies by the number of tasks" is the headline. Do the arithmetic on both sides, because the surprising part is which resource actually binds.
Take a 15-token instruction, an 8-token query, and a 200-token passage, on a BERT-base-class encoder. A transformer forward pass costs roughly 2 × (parameters) × (tokens) FLOPs, so at 110M parameters that is 2.2 × 108 FLOPs per token.
| Side | Tokens without instruction | Tokens with | Compute change | How often you pay it |
|---|---|---|---|---|
| Query | 8 | 23 | ×2.88 — 1.8 GFLOP → 5.1 GFLOP | Every query, thousands per second |
| Passage | 200 | 215 | ×1.075 — a 7.5% surcharge | Once per document per task |
The query side nearly triples, which sounds alarming and is not: 5.1 GFLOP is well under a millisecond on any accelerator, and query volume is small compared with corpus volume. The passage side rises by only 7.5% per document, which sounds negligible. Both intuitions are wrong about what matters.
Compute the full corpus pass for 400 million documents:
At an achieved 1015 FLOP/s that is about 4.9 GPU-hours — hours on a small fleet. Re-embedding an entire 400-million-document corpus is affordable. So compute is not the constraint, and every summary that says "instructions on documents are too expensive to compute" has the argument backwards.
The constraint is the other two numbers. Storage: 400M × 1024 dims × 2 bytes = 819 GB per index, times T tasks. And operations: each re-index is a migration — build, validate, dual-write, cut over, keep the old one until you are sure — and you now run T of those pipelines, each of which can drift out of sync with the others in exactly the silent way Chapter 4 describes.
[CLS] vector comes from: RetroMAEOne more BGE decision is worth unpacking, because it is a different answer to a question E5 answered with prefixes: how do you make the pooling slot carry the sentence?
E5 mean-pools, which is a safe default: no single position has to be special, because every position contributes. BGE pools the [CLS] position — a single token that was never, in ordinary BERT pretraining, trained to summarise anything (its original job was next-sentence prediction, which BERT variants later dropped entirely). Pooling an untrained slot is a bad idea, so BGE trains it first, with RetroMAE.
[CLS] vector. One vector, shape (768,).[CLS] vector plus the same sentence masked aggressively (50–70%), and must reconstruct the original tokens.[CLS] vector — so gradient descent is forced to pack the sentence into it.The design is an information bottleneck argued into existence by handicapping the decoder. Make the decoder strong and it will cheat by using surrounding tokens; make it one layer and mask most of the input, and the bottleneck becomes the only path. The result is a [CLS] position that already behaves like a sentence summary before contrastive training begins, which is why BGE can afford to pool it.
[CLS] slot, and I will spend a whole pretraining stage teaching it to be worth pooling." Neither is wrong. Chapter 6 shows a third answer — train a small attention module whose entire job is pooling — and Chapter 9 puts all four in a grid.The claim "one model, many geometries" deserves to be made concrete. Take five short texts from a restaurant-review corpus:
| Text | Topic | Sentiment | |
|---|---|---|---|
| S1 | "The new pasta place downtown is incredible — best carbonara I have had." | Italian | Positive |
| S2 | "Terrible service at the pasta place downtown, we waited fifty minutes." | Italian | Negative |
| S3 | "The new sushi bar is fantastic, the omakase is worth every penny." | Japanese | Positive |
| S4 | "Awful experience at the sushi bar, my order never arrived." | Japanese | Negative |
| S5 | "How do I request a refund for a restaurant booking?" | Support | Neutral |
Three instructions, three cosine tables. These are representative magnitudes, not measurements from a specific checkpoint — the point is the pattern of which pairs win.
Instruction A: "Represent the review for clustering by cuisine type:"
| S1 | S2 | S3 | S4 | |
|---|---|---|---|---|
| S1 | 1.00 | 0.81 | 0.28 | 0.24 |
| S2 | 0.81 | 1.00 | 0.25 | 0.31 |
| S3 | 0.28 | 0.25 | 1.00 | 0.84 |
| S4 | 0.24 | 0.31 | 0.84 | 1.00 |
Instruction B: "Represent the review for classifying sentiment:"
| S1 | S2 | S3 | S4 | |
|---|---|---|---|---|
| S1 | 1.00 | 0.22 | 0.79 | 0.19 |
| S2 | 0.22 | 1.00 | 0.20 | 0.83 |
| S3 | 0.79 | 0.20 | 1.00 | 0.17 |
| S4 | 0.19 | 0.83 | 0.17 | 1.00 |
Look at the pair (S1, S2): 0.81 under instruction A, 0.22 under instruction B. Same two sentences, same weights, same forward pass architecture — a similarity that moves by 0.59 depending on a sentence of English prepended to the input. And (S1, S3) moves the opposite way, 0.28 to 0.79. The nearest-neighbour graph is completely rewired.
This is what "many geometries" means operationally. If you run k-means on the instruction-A vectors you get cuisine clusters. Run it on the instruction-B vectors and you get sentiment clusters. You did not train two models, and you did not train a classifier — you asked the same model a different question.
The five sentences above, laid out by the geometry each instruction induces. Switch instructions and watch the points rearrange while the texts stay identical. The panel on the right shows each sentence's nearest neighbour and the live pooling diagram — toggle whether instruction tokens are included in the mean and watch the dynamic-range readout collapse.
Time to be sceptical about the thing we just spent a chapter admiring.
There are two stories consistent with the evidence above. In the compositional story, the model genuinely parses the instruction and configures itself accordingly, so a novel instruction it has never seen produces the appropriate novel geometry. In the lookup story, the model has learned a few dozen instruction templates as effectively opaque task identifiers, and a novel instruction is snapped to the nearest memorised one — or produces garbage.
The evidence is mixed and leans uncomfortably toward the second. Instruction-tuned embedders are measurably sensitive to paraphrases of their instructions: rewording "Represent the news article for clustering by topic:" as "Group these news stories by subject:" can move scores materially, which is not what a model that understood the instruction would do. The structured template INSTRUCTOR uses — domain, text type, objective — is partly a response to this: it keeps novel instructions inside the grammar the model was trained on, so the lookup has something close to snap to.
None of this makes instruction conditioning a bad idea. It makes it a partially delivered idea, and knowing which part is delivered is the difference between deploying it well and being surprised in production. Chapter 6 shows what happens when the substrate is a model that genuinely does read instructions for a living.
By 2024 an obvious embarrassment had appeared. The best language understanding on the planet lived in decoder-only LLMs — seven billion parameters, trained on trillions of tokens, capable of following instructions in ninety languages. And the best text embedders were still 110-million-parameter BERTs trained on three billion words in 2018.
Everyone could see the arbitrage. Three papers in five months took three different routes to it, and comparing them is the single most efficient way to understand what an embedder actually requires.
Because you cannot simply use an LLM as an embedder. Three things are in the way.
| Obstacle | Why it exists | Consequence for embeddings |
|---|---|---|
| Causal attention mask | The pretraining objective is next-token prediction, which would be trivial if a token could see the future | Token i's state has read only tokens 1…i. Most positions have seen a fraction of the text |
| No pooling slot | Decoders have no [CLS]; every position exists to predict the next one | There is no position whose job is "summarise the sequence" |
| No contrastive stage | LLMs are trained with cross-entropy on tokens, never with a similarity objective | The space is not organised so that dot products mean relatedness |
This obstacle is the deep one, so build it from zero.
In a bidirectional encoder like BERT, self-attention at every layer lets every position attend to every other. After twelve layers, the final state at position 3 has integrated information from all L tokens. Every position is a complete-information summary written from a different vantage point. Pool them however you like — mean, max, first — and you are averaging complete summaries.
In a causal decoder, position i can attend only to positions 1 through i. That constraint is not a bug; it is what makes next-token prediction a well-posed task. But it means the final states are not comparable objects.
Define the visibility of position i as the fraction of the sequence its final state has read: vi = i / L. Now compute what mean pooling actually averages, for L = 8:
| Position i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| Tokens visible | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| Visibility vi | 12.5% | 25% | 37.5% | 50% | 62.5% | 75% | 87.5% | 100% |
For a general L the sum is L(L+1)/2, so the average visibility is (L+1)/(2L), which tends to exactly one half. At L = 512: 131,328 / 262,144 = 0.5010.
So mean pooling is out. What about the last position, the only one with full visibility?
It works, and it has its own defect. In a causal LM the state at position L is read by the language-model head to produce a distribution over token L+1. Gradient descent has spent trillions of tokens shaping that state to answer "what word comes next?", which is a local, syntactic, high-frequency question. If a passage ends with "…and the", the final state is overwhelmingly about grammar. It is not a summary; it is a prediction.
Wang and colleagues (arXiv:2401.00368) took the most audacious position: leave the architecture almost alone, skip the contrastive pretraining stage entirely, and fix the data problem by having a language model write the data.
The pooling fix is minimal. Append an explicit [EOS] token to every input and pool that position. The [EOS] has full visibility, and because it is the terminal token, its "next token" is nothing — so contrastive fine-tuning is free to repurpose that state as a summary without fighting a strong pretrained prior about what should follow.
The data fix is the paper's real contribution, and it is a two-step prompting recipe:
Compare that with Chapters 1 and 3. E5 scraped 1.3 billion pairs and filtered them to 270 million to get clean positives; then it mined hard negatives from a corpus and distilled a cross-encoder to denoise them. E5-Mistral asks for a clean positive and a hard negative in the same API call. The two hardest problems in the first half of this lesson become prompt engineering.
The training fix is LoRA. Mistral-7B is fine-tuned with Low-Rank Adaptation at rank 16 for fewer than a thousand steps. LoRA replaces a full weight update with a low-rank one: instead of learning a whole new W of shape (4096, 4096), learn B of shape (4096, 16) and A of shape (16, 4096) and use W + BA. Count the parameters:
Why does this matter here specifically? Because the whole premise is that the LLM already knows things. A full fine-tune on 500k synthetic examples would happily overwrite trillions of tokens of pretraining with the idiosyncrasies of one generated dataset. LoRA's low-rank constraint makes catastrophic overwriting geometrically difficult — the update simply does not have the capacity to rewrite the model.
BehnamGhader and colleagues (arXiv:2404.05961) attacked the obstacle directly. If the causal mask is the problem, remove the causal mask.
Step 2's off-by-one is the kind of detail that separates a paper that works from one that does not. The pretrained language-model head was trained on exactly one mapping: hidden state at position i − 1 → token i. Ask it instead to map position i to token i and you are using a calibrated head for an uncalibrated purpose, and it has to relearn its entire input convention. By keeping the shift, MNTP reuses the pretrained head as-is and spends its gradient budget on the only thing that actually changed — how the attention layers behave without a causal mask.
Step 3 is worth dwelling on because it eliminates the entire first half of this lesson. SimCSE constructs a positive pair out of nothing: run the same sentence through the network twice, with dropout active, and the two resulting vectors differ slightly. Train them to be close, and everything else in the batch to be far. There is no CCPairs, no consistency filter, no annotation. The only requirement is a corpus of raw text — LLM2Vec uses Wikipedia.
Because mask flipping restored full visibility to every position, LLM2Vec can go back to mean pooling, which is now a legitimate operation: it averages complete summaries again, exactly as in BERT.
Lee and colleagues at NVIDIA (arXiv:2405.17428) accepted both prior moves — remove the causal mask, use a strong decoder — and asked the remaining question: if neither mean nor last-token pooling is right, what is?
Their answer is a latent attention layer, and it is the cleanest idea in this chapter. Introduce a trainable array of latent vectors — think of it as a learned dictionary of concepts, of shape (512, 4096). Then run one cross-attention step in which the sequence's hidden states are the queries and the latent dictionary supplies the keys and values:
| Tensor | Shape | Role |
|---|---|---|
H — last-layer hidden states | (L, 4096) | Queries. Each token asks the dictionary a question |
Latents — trainable | (512, 4096) | Keys and values. A learned basis, shared across all inputs |
O = softmax(HKT/√d) V | (L, 4096) | Each token re-expressed as a mixture of dictionary entries |
| MLP, then mean over L | (4096,) | The final embedding |
Why is this better than plain mean pooling? Because plain mean pooling weights every token equally — the subject noun, the definite article, the trailing punctuation, the boilerplate footer. Latent attention lets each token first project itself onto a learned basis of 512 concept directions, so what gets averaged is not raw token states but dictionary coefficients. Uninformative tokens have nothing to retrieve and contribute near-uniform, low-magnitude mixtures; informative tokens light up specific latents. The averaging happens in a space where "unimportant" has a representation.
And why is it better than last-token pooling? Because last-token pooling routes an entire document through the state of one position — a bottleneck of 4096 numbers that also has to satisfy whatever the language model wanted that position to be. Latent attention reads all L positions.
Note the cost is linear in L: one cross-attention against a fixed 512-entry dictionary, not a quadratic self-attention. Pooling stays cheap.
NV-Embed trains in two instruction-tuned stages. Stage one is retrieval, with in-batch negatives plus mined hard negatives — the Chapter 2 and 3 recipe. Stage two blends in non-retrieval tasks (classification, clustering, semantic similarity) and disables in-batch negatives.
That sounds like an arbitrary knob. Derive it and it becomes forced.
Return to Chapter 2's false-negative calculation, but for a classification dataset with C classes. Two examples of the same class are, for the purposes of a classification embedding, positives — you want them close. In-batch negatives will treat them as negatives. The chance that a batch of N contains at least one same-class item as the anchor is
| Task | Classes C | Batch N | P(at least one false negative) | Expected false negatives per anchor |
|---|---|---|---|---|
| Sentiment (binary) | 2 | 32 | > 0.99999 | 15.5 of 31 |
| Topic classification | 4 | 32 | 0.99989 | 7.75 of 31 |
| Intent classification | 20 | 32 | 0.7961 | 1.55 of 31 |
| Retrieval (CCPairs) | — | 32,768 | 0.114 for a head query | < 0.2 |
Check the binary row: 1 − 0.531 = 1 − 4.66 × 10−10. Every single batch, half the "negatives" are same-class items you are actively teaching the model to separate. This is not a small amount of noise. It is a second, opposing objective running concurrently with the one you wanted.
Left: the attention mask, causal or bidirectional, with each position's visibility shown as a bar. Right: the three pooling schemes reading from those states. Flip to causal and watch the mean-pooling readout report the fraction of the sequence its average state has actually seen — the number the derivation above predicts.
| Slot | E5-Mistral | LLM2Vec | NV-Embed |
|---|---|---|---|
| Substrate | Mistral-7B, causal mask kept | Decoder LLM, mask flipped to bidirectional | Mistral-7B, mask removed during contrastive training |
| Data | LLM-generated triples, ~500k, plus labelled data | Raw Wikipedia; positives from dropout (SimCSE) | Curated public retrieval and non-retrieval mixtures |
| Objective | InfoNCE, single stage, fewer than 1k steps | MNTP adaptation, then unsupervised contrastive, then optional supervised | Two-stage: retrieval, then a blend with in-batch negatives disabled |
| Pooling | Appended [EOS] token | Mean (valid again once bidirectional) | Trained latent-attention layer |
| Conditioning | Free-form task instruction, query side only | Optional instruction | Free-form task instruction |
| Headline MTEB (as reported) | ~66.6 | ~56.8 unsupervised, ~64.8 supervised | ~69.3 — first place at release |
Read down the rows and the point of this lesson appears. Three teams, one substrate family, and every one of them made a different choice in every slot. There is no single recipe. There is a design space, and each of these papers is one point in it with an ablation table attached.
Before you reach for a 7B embedder, price it against the 335M one honestly.
| Quantity | E5-large (335M, d = 1024) | 7B embedder (d = 4096) | Ratio |
|---|---|---|---|
| Encode FLOPs per 256-token document | ~1.7 × 1011 | ~3.6 × 1012 | ~21× |
| Index bytes per vector (fp16) | 2,048 | 8,192 | 4× |
| Index size for 400M documents | 819 GB | 3.28 TB | 4× |
| Query encode latency (single GPU, rough) | 1–3 ms | 10–30 ms | ~10× |
| MTEB average | ~62 | ~66–69 | +4 to +7 points |
Twenty times the encoding compute and four times the storage for four to seven points of benchmark average. Whether that trade is correct is a product question, not a research question, and it depends entirely on whether your bottleneck is retrieval quality (Chapter 0's leverage argument says yes, often) or serving cost.
The dimension half of the problem has a standard mitigation worth knowing: Matryoshka Representation Learning (arXiv:2205.13147) trains a model so that the first 256 or 512 coordinates of its embedding are themselves a usable embedding, by adding loss terms on truncated prefixes. You store the short vector for the first-pass search and the full vector for reranking. It converts an all-or-nothing dimension choice into a runtime knob, and most 2024-onward embedders ship with it.
Six chapters of machinery. This chapter runs one complete training step through all of it — normalisation, prefix conditioning, the similarity matrix, the temperature, the loss, and the gradient — with every number computed by hand. Nothing is skipped and nothing is approximated except where the arithmetic says so.
We work in two dimensions. Real E5 uses 768 or 1024; two is what fits on paper and on a circle you can look at. One consequence to hold on to: two dimensions is a crowded place. Six unit vectors in 2-D cannot get far from each other, so converged cosines will look implausibly high, around 0.99. In 768 dimensions the same relative structure appears at absolute cosines around 0.7–0.8. What transfers is the margin — the gap between the right answer and the best wrong one — not the absolute value.
Three query–passage pairs, which is our entire batch. N = 3.
| i | Query | Passage |
|---|---|---|
| 1 | "how long to boil an egg" | "A soft-boiled egg needs about six minutes in gently boiling water." |
| 2 | "how to sort a list in python" | "Use list.sort() to sort in place, or sorted() to return a new list." |
| 3 | "why is the sky blue" | "Rayleigh scattering sends short blue wavelengths in every direction across the sky." |
The encoder produces these raw, unnormalised 2-D outputs. Read the pattern before the arithmetic: the three queries all point mostly along dimension 1, and the three passages all point mostly along dimension 2. That is not about topic. It is register — short interrogative fragments versus long declarative sentences — and it is exactly the surface-form geometry Chapter 4 warned about.
| Vector | Raw output | Norm ‖v‖ | Unit vector |
|---|---|---|---|
| q1 | [9, 3] | √(81+9) = √90 = 9.48683 | (0.94868, 0.31623) |
| q2 | [8, 4] | √(64+16) = √80 = 8.94427 | (0.89443, 0.44721) |
| q3 | [7, 5] | √(49+25) = √74 = 8.60233 | (0.81373, 0.58124) |
| p1 | [6, 8] | √(36+64) = √100 = 10 | (0.60000, 0.80000) |
| p2 | [3, 9] | √(9+81) = √90 = 9.48683 | (0.31623, 0.94868) |
| p3 | [1, 9] | √(1+81) = √82 = 9.05539 | (0.11043, 0.99381) |
Check one normalisation by hand so the rest are trustworthy. For q1 = [9, 3]: 9² + 3² = 81 + 9 = 90, and √90 = 9.48683. Then 9 / 9.48683 = 0.94868 and 3 / 9.48683 = 0.31623. Confirm it landed on the unit circle: 0.94868² + 0.31623² = 0.90000 + 0.10000 = 1.00000. Good.
Nine dot products. Two worked in full, the rest by the same recipe.
The full matrix. Rows are queries, columns are passages, and the diagonal is what we want to win:
| p1 (egg) | p2 (python) | p3 (sky) | Row argmax | |
|---|---|---|---|---|
| q1 (egg) | 0.8222 | 0.6000 | 0.4190 | p1 — correct |
| q2 (python) | 0.8944 | 0.7071 | 0.5432 | p1 — wrong |
| q3 (sky) | 0.9532 | 0.8087 | 0.6675 | p1 — wrong |
Accuracy: 1 out of 3, and it is worse than that number sounds. Every query picks p1. Not because p1 is about eggs, but because p1 is the passage whose direction sits closest to the whole query cluster. It is a hub.
Chapter 4 argued that "query: " works by reconfiguring the encoder's processing, and that its effect is approximately a linear map applied to the query representation. In this toy we write that map down explicitly. Suppose training has produced
applied to the raw query output, before normalisation. Read what it does: it shrinks dimension 1 (the register axis), amplifies dimension 2, and mixes them. Apply it:
Normalise each:
| Prefixed query | Raw | Norm | Unit vector |
|---|---|---|---|
| q′1 | [30, 36] | √(900+1296) = √2196 = 46.8615 | (0.64017, 0.76820) |
| q′2 | [20, 52] | √(400+2704) = √3104 = 55.7136 | (0.35898, 0.93335) |
| q′3 | [10, 68] | √(100+4624) = √4724 = 68.7313 | (0.14549, 0.98936) |
Notice what the map achieved geometrically. The unprefixed queries were bunched between 18° and 36° — a spread of 17°. The prefixed ones sit between 50° and 82° — a spread of 31°, in the same neighbourhood as the passages. The prefix did two things: it moved the queries into passage territory, and it spread them out. The second is as important as the first, because a spread of 17° cannot possibly distinguish three passages spread over 30°.
New similarity matrix. One worked in full:
| p1 | p2 | p3 | Row argmax | Margin | |
|---|---|---|---|---|---|
| q′1 | 0.99866 | 0.93119 | 0.83414 | p1 — correct | 0.0675 |
| q′2 | 0.96207 | 0.99897 | 0.96721 | p2 — correct | 0.0318 |
| q′3 | 0.87878 | 0.98461 | 0.99930 | p3 — correct | 0.0147 |
Three out of three. The encoder weights did not change — the passage embeddings are byte-for-byte the same numbers as in Stage A. All that changed is a linear map applied to the query side, which is what conditioning on a prefix buys you.
Recall the per-row loss from Chapter 2, written in the form that is easiest to compute: take every similarity relative to the positive, divide by τ, exponentiate, sum, and take the log.
That form is worth a sentence of its own: the loss depends only on differences from the positive. Absolute similarity is irrelevant. This is why comparing cosine values across models is meaningless and comparing margins is not.
Stage B (prefixed), τ = 0.01, row 1. Differences: 0.93119 − 0.99866 = −0.06747, and 0.83414 − 0.99866 = −0.16452. Divide by 0.01: −6.747 and −16.452. Exponentiate: e−6.747 = 0.001177, e−16.452 = 7.2×10−8. Sum with the 1:
Row 2. Differences −0.03690 and −0.03176; divided by τ, −3.690 and −3.176; exponentials 0.024960 and 0.041782. L2 = ln(1.066742) = 0.06460.
Row 3. Differences −0.12052 and −0.01469; divided, −12.052 and −1.469; exponentials 5.8×10−6 and 0.230234. L3 = ln(1.230240) = 0.20720.
Now the same computation on Stage A, the unprefixed matrix, and this is where the temperature earns its chapter.
Row 1's positive is already the row maximum, so every difference is negative and the loss is essentially zero: e−22.22 + e−40.32 = 2.2×10−10, giving L1 = 2.2×10−10.
Row 2's positive is p2 at 0.70711, but p1 scores 0.89443 — higher. The difference is positive: 0.89443 − 0.70711 = +0.18732, divided by τ gives +18.732, and e18.732 = 1.365×108. The denominator is dominated by that single term, so
Look at that number and then look back at the difference: 18.732 is exactly 0.18732 / 0.01. Row 3 does the same thing: its deficit is 0.95323 − 0.66750 = 0.28573, and L3 = 28.573.
Stage A: 15.768. Stage B: 0.09099. A factor of 173, from a linear map on one side of the batch. And now sweep the temperature to see how much of that gap is real and how much is the ruler:
| τ | L, no prefix | L, with prefix | Ratio | ln 3 (random guessing) |
|---|---|---|---|---|
| 0.01 | 15.768 | 0.0910 | 173× | 1.0986 |
| 0.05 | 3.185 | 0.5213 | 6.1× | 1.0986 |
| 0.20 | 1.246 | 0.8872 | 1.40× | 1.0986 |
| 1.00 | 1.0894 | 1.0515 | 1.04× | 1.0986 |
At τ = 1 both models sit at roughly ln 3 and the loss cannot tell them apart — a model that gets 1 of 3 right and a model that gets 3 of 3 right differ by four percent. At τ = 0.01 they differ by a factor of 173. The temperature is not a detail of the loss; it decides whether the loss can see the thing you care about.
Trace one row to feel the mechanism. Stage A row 2 at τ = 1: the exponentials are e0.18732 = 1.2060 for the wrong winner, 1 for the positive, and e−0.1639 = 0.8488 for the third, summing to 3.0548 — the wrong answer holds 39.5% of the mass and the right one 32.7%. The model is losing, and the loss reports 1.117, barely distinguishable from the 1.099 of a model that has learned nothing. Divide the same three differences by 0.01 instead and the wrong answer holds 99.9999993% of the mass. Same embeddings. Same ordering. Completely different gradient.
Differentiate. Writing sij = qi · pj and Pij for the row softmax of si·/τ:
In words: the gradient is the softmax-weighted average of all passages, minus the correct passage, scaled by 1/τ. Gradient descent moves q in the opposite direction — toward the positive, away from whatever the model is currently betting on.
Compute it for Stage A, row 2, the badly wrong one. The row softmax at τ = 0.01 is dominated by p1: relative weights are 1 for p1, e−18.732 = 7.3×10−9 for p2, and e−35.12 for p3. So P2· ≈ (1, 0, 0), and the weighted average is just p1:
Magnitude: √(28.377² + 14.868²) = √(805.25 + 221.06) = √1026.31 = 32.04. The descent direction is (−28.377, +14.868): shrink dimension 1, grow dimension 2. That is precisely "become less query-shaped and more passage-shaped", and it is what accumulates, over 270 million examples, into the prefix map of Stage B.
Now Stage B, row 3, the least confident of the three correct rows. Its softmax is P3· = (4.7×10−6, 0.18715, 0.81285). The weighted average of the passages is
Subtract p3 = (0.11043, 0.99381) and scale by 100: (3.852, −0.845), magnitude 3.94. Eight times smaller than Stage A's, and pointing almost tangentially — the model is nearly right and is being nudged, not shoved.
torch.no_grad() or normalise offline, which is a real and silent bug: your model trains on a gradient with a spurious radial component and converges to the wrong place.Left: all six unit vectors on the circle, with the prefix map applied at your chosen strength (0% reproduces Stage A exactly, 100% reproduces Stage B). Right: the live 3×3 similarity matrix, the row softmax at your temperature, and the per-row loss. Press Train to run gradient descent on the query side and watch the diagonal take over.
Set the prefix to 0% and press Train. The queries crawl out of their register cluster and rotate toward their own passages — you are watching, in miniature, the thing that 270 million pairs and a 32,768-wide batch do over two weeks on a GPU fleet. Then set the temperature to 1.0 and press Train again: the same gradients, a hundred times weaker, and the diagonal never quite wins.
Every model in this lesson is compared to every other one by a single number. That number governs which embedder ends up in your stack, which papers get written, and which research directions get funded. It deserves an hour of scepticism.
MTEB — the Massive Text Embedding Benchmark (Muennighoff et al., arXiv:2210.07316) — was a genuine service to the field. Before it, every embedding paper evaluated on a different handful of datasets, chosen after the fact. MTEB fixed a suite, published a leaderboard, and made results comparable. That is a real contribution and this chapter is not an attack on it.
It is an attack on reading only the first column.
The English MTEB suite is 56 datasets across seven task types. The headline number is the unweighted mean over the 56 datasets — not over the seven task types. That distinction is the whole chapter.
| Task type | Datasets | Implied weight in the average | Metric |
|---|---|---|---|
| Retrieval | 15 | 26.8% | nDCG@10 |
| Classification | 12 | 21.4% | Accuracy of a logistic probe |
| Clustering | 11 | 19.6% | V-measure after k-means |
| Semantic textual similarity (STS) | 10 | 17.9% | Spearman correlation |
| Reranking | 4 | 7.1% | MAP |
| Pair classification | 3 | 5.4% | Average precision |
| Summarization | 1 | 1.8% | Spearman correlation |
Read the third column as an editorial statement, because that is how the field uses it: retrieval matters fifteen times as much as summarization. Now ask where that ratio came from. Not from a survey of what embeddings are used for. Not from a weighting decision anyone defended in the paper. It came from how many suitable public datasets happened to exist for each task type in 2022.
Model B beats Model A by 1.00 on the MTEB average. What has actually been observed?
The average is a sum over 56 datasets divided by 56, so a 1.00 average gap is exactly 56 points of total dataset-level movement. That total can be distributed in wildly different ways, and the distributions are not equivalent for your purposes.
| Scenario | Distribution of the 56 points | Datasets B wins | What it means for a RAG pipeline |
|---|---|---|---|
| Uniform | +1.00 on every dataset | 56 of 56 | Genuinely better everywhere. Rare in practice |
| Retrieval-concentrated | +3.73 on each of the 15 retrieval sets, 0 elsewhere | 15 of 56, ties on 41 | Excellent — this is the improvement you actually wanted |
| STS-concentrated | +5.60 on each of the 10 STS sets, 0 elsewhere | 10 of 56 | Worthless — your retrieval quality is unchanged |
| Adversarial | +5.00 on 15 retrieval sets (+75), −0.463 on the other 41 (−18.98) | 15 of 56 | Great for RAG, and B loses 73% of the benchmark |
Check the adversarial row: 15 × 5.00 = 75.0 points gained; 41 × 0.463 = 18.98 points lost; net 56.02 points, divided by 56 = +1.0004 average. Model B climbs the leaderboard by a full point while being worse than A on forty-one of the fifty-six datasets. Both statements are true simultaneously, and one column can only show you one of them.
This is not hypothetical. Two models from the same lab, built on the same T5 backbone, differing only in training data:
| Model | MTEB average | Retrieval sub-average | STS sub-average |
|---|---|---|---|
| Sentence-T5-XXL (4.8B) | ~59.5 | ~42.2 | ~82.6 |
| GTR-XXL (4.8B) | ~59.0 | ~48.5 | ~77.8 |
| Difference | 0.5 | 6.3 | 4.8 |
Half a point apart on the headline. Six and a third points apart on retrieval. If you are building search or RAG, GTR-XXL is not marginally better, it is in a different class — and the leaderboard reports them as tied. The reason is visible in their training: Sentence-T5 was trained largely on symmetric community-QA and inference pairs, GTR on retrieval data. They learned two different relations, exactly as Chapter 4 predicted, and the average dissolves that distinction into a rounding error.
MTEB's retrieval section is essentially BEIR, and BEIR was designed as a zero-shot benchmark: score well without having trained on these domains. That framing is now largely false, for a mundane reason. Several BEIR datasets have public training splits, and nearly everyone trains on them.
| MTEB retrieval dataset | Public training split exists? | Commonly in embedder training mixtures? |
|---|---|---|
| MS MARCO | Yes, 500k pairs | Almost universally |
| Natural Questions | Yes | Very commonly — E5 stage two uses it |
| HotpotQA | Yes | Commonly |
| FEVER | Yes | Commonly |
| Quora | Yes | Commonly |
| FiQA | Yes | Sometimes |
| SciFact, TRECCOVID, ArguAna, Touche, CQADupstack, NFCorpus, Climate-FEVER, DBPedia, SCIDOCS | Mostly no | Rarely |
So a "zero-shot retrieval average" over 15 datasets is, for a typical modern model, an average over roughly 5 in-domain datasets and 10 genuinely held-out ones. Those two groups measure different things: the first measures fit, the second measures transfer. Averaging them produces a number that measures neither.
A common complaint is that leaderboard gaps are "just noise." Let us check, because the honest answer is more interesting than the complaint.
Different task types have different amounts of run-to-run variance. Classification uses a logistic probe with a fixed split — nearly deterministic. Retrieval and STS are deterministic given the embeddings. Clustering is not: it runs k-means, whose initialisation is random, and V-measure genuinely moves between seeds. Assign plausible per-dataset standard deviations and propagate them:
So the average is reproducible to about ±0.13 at two standard deviations. A 0.5-point gap is real. A 0.05-point gap — and leaderboards do report two decimal places — is not.
Distribution across task types is the failure people can at least detect from published numbers. These are the ones no published number contains.
| Property of your actual problem | Measured by MTEB? | Why it matters |
|---|---|---|
| Documents longer than a paragraph | No — retrieval passages are short | Model rankings routinely invert between 100-token and 4,000-token documents |
| Your domain | Only if it resembles one of the 56 | Chapter 1: the consistency filter deleted rare relations from pretraining |
| Behaviour under your chunking | No — chunks are handed to the model | Chunking is a lossy decision made before the model sees anything |
| Index size and query latency | No | A 4096-dim model is a 4× storage bill that never appears on the leaderboard |
| Robustness to typos, casing, boilerplate | No | Real queries are misspelt; real documents have navigation chrome |
| Degradation as the corpus grows | No — corpora are fixed and modest | More documents means more near-duplicates competing for the top slot |
| Mixed-language corpora | Only in the multilingual variant | Code-switched and translated documents behave nothing like either monolingual case |
Look at row one and row four together. The two properties most likely to decide whether a model works for you — how it handles your document length, and what it costs to store — are both entirely absent. That is not a flaw anyone can fix by adding datasets; a benchmark measures what it measures.
Everyone agrees you should evaluate on your own data. Almost nobody computes whether their evaluation can detect the difference they are looking for. Do it once, properly, and the answer is memorable.
Say recall@10 is around 0.75 and you want to detect a 5-point difference between two models.
Unpaired — different query samples for each model, or the same sample treated as independent. The standard error of one proportion at n = 200 is
The difference of two independent estimates has standard error 0.0306√2 = 0.0433, so the 95% confidence interval on the gap is ±0.085. You can only resolve differences larger than 8.5 points. Your 5-point effect is invisible.
Paired — both models on the same 200 queries. Now the query's intrinsic difficulty cancels, and only the queries where the two models disagree carry information. This is McNemar's test. Suppose B succeeds where A fails on b = 18 queries, and A succeeds where B fails on c = 8. The observed gap is (18 − 8) / 200 = +5.0 points, exactly the effect we wanted to detect.
Under the null hypothesis that the models are equally good, b is Binomial(26, 0.5) — 26 discordant queries, each a coin flip. Compute the exact tail by summing binomial coefficients:
Suggestive, not conclusive. So the honest verdict on 200 paired queries is: it will show you a 5-point effect and it will not let you swear to it.
What would settle it? Double to 400 queries with the same discordance rate: b = 36, c = 16, 52 discordant. The null mean is 26 with standard deviation √(52 × 0.25) = 3.606, so the continuity-corrected z is (36 − 0.5 − 26) / 3.606 = 2.634, giving a two-sided p of 0.008. Four hundred paired queries is conclusive.
| Design | n | Can it resolve a 5-point gap? |
|---|---|---|
| Unpaired | 200 | No — the confidence interval is ±8.5 points |
| Paired (McNemar) | 200 | Suggestive — p = 0.076 |
| Paired (McNemar) | 400 | Yes — p = 0.008 |
| Unpaired | 400 | No — the interval is still ±6.0 points |
MTEB is public, fixed, and famous. Nobody needs to train on its test sets for it to be overfit. Selection is enough: you try eight data mixtures, four pooling strategies, and three instruction templates, and you keep the one with the best MTEB score. No test label ever touched a gradient, and you have still fitted the benchmark — through your own decisions.
Quantify the pressure. If you evaluate 100 candidate configurations and pick the best, you are taking a maximum over 100 draws. With a per-configuration standard deviation of 0.064, the expected inflation of the winner is roughly 0.064 × √(2 ln 100) = 0.064 × 3.03 = 0.19 points of pure selection luck — small on its own, but exactly the size of the gaps that reorder the top of the leaderboard. And that is the optimistic estimate: real configuration choices differ by far more than noise, so the selection is fitting genuine benchmark-specific structure, not just noise.
Fifty-six datasets, grouped by task type. Model B is exactly +1.00 average over Model A in every configuration — drag the concentration slider to move the same one point between "spread evenly" and "all in retrieval", and drag the trade-off slider to make B pay for its retrieval win elsewhere. Watch the win count, the retrieval sub-average, and the RAG end-to-end estimate move while the headline number never budges.
Push both sliders to the right. The headline stays at +1.00, the retrieval sub-average climbs past +5, and the number of datasets B wins falls to 15 of 56. Two models, one number, four completely different products.
None of the above means "ignore the benchmark." It means use it for what it is good at — a broad first-pass filter — and then do the ten minutes of work that determines your outcome.
Step 3 is where the worked example above pays off. Paired evaluation is not a refinement of unpaired evaluation; it is the difference between an experiment that can answer your question and one that cannot, at the same labelling cost. And the sample size is not a matter of taste — it is four lines of binomial arithmetic that tell you, before you label anything, whether the study you are about to run is capable of detecting the effect you are looking for.
Nine chapters, six papers, one recurring structure. This chapter lays the structure out flat, because the point of reading six papers instead of one is to stop seeing recipes and start seeing a space with coordinates.
Every text embedder in this lesson is a choice in five slots plus three cross-cutting axes. Here is the whole grid.
Slot 1 — Substrate. What network are you starting from?
| Option | Used by | What it buys | What it costs |
|---|---|---|---|
| BERT-base / large (110M–335M) | E5, BGE, GTE | Bidirectional by construction; cheap to serve; 768–1024 dims | Small; 2018-era pretraining data; 512-token limit |
| MiniLM (33M) | E5-small | Runs on a CPU; 384 dims means a 2× smaller index | A few MTEB points below base |
| T5 encoder | Sentence-T5, GTR, INSTRUCTOR | Strong multitask pretraining; scales to 4.8B | Encoder-decoder overhead; the decoder half is dead weight |
| Decoder LLM, causal mask kept | E5-Mistral | Enormous pretraining; instruction-following already present | Causal mask forces last-token pooling; 4096 dims; ~21× the compute |
| Decoder LLM, mask flipped | LLM2Vec, NV-Embed | Everything above plus full visibility at every position | Needs repair training (MNTP) after the flip |
Slot 2 — Data. Where do positive pairs come from?
| Option | Used by | Scale | The characteristic failure |
|---|---|---|---|
| Scraped web co-occurrence + consistency filter | E5 (CCPairs) | 1.3B → 270M | Deletes rare relations the noisy filter model never learned |
| Scraped web + heuristic cleaning + RetroMAE stage | BGE / C-Pack | ~200M | Lower purity; compensated by a stronger pretraining stage |
| Human relevance labels | Everyone, in stage two | ~1M | Sparse labels → unlabelled positives mined as negatives |
| LLM-generated triples | E5-Mistral | ~500k | Inherits the generator's blind spots; "hard negatives" that are secretly positive |
| No pairs at all — dropout twins | LLM2Vec (SimCSE) | Any raw corpus | Cannot teach an asymmetric relation; retrieval lags badly |
Slot 3 — Objective. What does the loss say?
| Option | Used by | What it adds |
|---|---|---|
| InfoNCE, in-batch negatives, huge batch | E5 stage 1, BGE stage 2 | Coarse global geometry, cheaply. Batch size is the mining pool |
| + mined hard negatives | E5 stage 2, BGE stage 3, NV-Embed | Fine local boundaries — and a false-negative problem |
| + cross-encoder KL distillation | E5 stage 2 | Teacher precision, and automatic denoising of mined negatives |
| Masked auto-encoding (RetroMAE) | BGE stage 1 | Pretrains the pooling slot before contrastive training starts |
| MNTP (masked next-token prediction) | LLM2Vec | Repairs a decoder after the attention mask is flipped |
| Two-stage, in-batch negatives disabled in stage 2 | NV-Embed | Lets retrieval and classification share one model without poisoning each other |
Slot 4 — Pooling. How do L token vectors become one?
| Option | Used by | Requires | Weakness |
|---|---|---|---|
| Mean | E5, LLM2Vec, INSTRUCTOR | Bidirectional attention | Weights boilerplate as heavily as the subject noun |
[CLS] | BGE | A pretraining stage that makes [CLS] worth reading | Untrained out of the box; one position carries everything |
Last token / appended [EOS] | E5-Mistral | Nothing — works under a causal mask | That state was optimised to predict the next token, not to summarise |
| Trained latent attention | NV-Embed | A trainable (512, d) dictionary and one cross-attention | Extra parameters; only ever demonstrated at 7B |
Slot 5 — Conditioning. What tells the model which relation to encode?
| Option | Used by | Index cost | Flexibility |
|---|---|---|---|
| Nothing | Sentence-BERT, SimCSE | One index | Symmetric relations only |
| Two fixed prefixes | E5 | One index | Two geometries, selected by a four-token string |
| One fixed instruction, query side only | BGE | One index | One asymmetric geometry, no re-indexing ever |
| Free-form instruction, both sides | INSTRUCTOR | One index per instruction | Most flexible on paper; most expensive to operate |
| Free-form instruction, query side only | E5-Mistral, NV-Embed | One index | The pragmatic consensus |
These are orthogonal to the five slots and are usually decided late, which is why they are usually decided badly.
| Axis | The choice | What it controls |
|---|---|---|
| Dimension | 384 / 768 / 1024 / 4096, or Matryoshka-trained so the prefix of the vector is itself usable | Index size and search latency, linearly. Matryoshka converts a one-time decision into a runtime knob |
| Context length | 512 tokens (BERT) versus 8k or 32k (LLM substrates) | Whether you must chunk. Chunking is a lossy decision made before the model sees anything |
| Quantisation | fp32 → fp16 → int8 → binary | Index bytes, again linearly. Binary embeddings cut storage 32× and cost a few points of recall — often the right trade for a first-pass retriever followed by a reranker |
Look back at how the papers in this lesson were actually produced. Each is a transplant.
| Paper | What it borrowed | What it changed | What was genuinely new |
|---|---|---|---|
| E5 | InfoNCE, in-batch negatives, cross-encoder distillation, two-stage training | Data scale, and a consistency filter | CCPairs + the filter; the prefix convention |
| BGE / C-Pack | E5's two-stage shape | Added RetroMAE; moved pooling to [CLS]; one query-side instruction | The full open data + benchmark + model package |
| INSTRUCTOR | GTR substrate; contrastive training | Prefix → free-form instruction; instruction excluded from pooling | MEDI, and the "any task" framing |
| E5-Mistral | E5's objective and instruction idea | Substrate → 7B decoder; data → synthetic; dropped stage one entirely | The two-step synthetic data recipe |
| LLM2Vec | SimCSE's objective; MNTP from masked LM literature | Flipped the attention mask | Showing the flip is cheap and reversible — three steps, no pairs |
| NV-Embed | Nearly everything above | Two-stage instruction tuning; in-batch negatives off in stage two | Latent-attention pooling |
Now use the grid the way it is meant to be used. Here are six combinations that the six papers between them never tried, with the reason each is plausible and the reason nobody has done it.
| Recombination | Why it should work | Why it has not been tried |
|---|---|---|
| Latent-attention pooling on a 335M encoder | NV-Embed's pooler is a (512, d) dictionary and one cross-attention. Nothing about it needs 7B parameters. Chapter 6 argued mean pooling wastes capacity on boilerplate — that waste exists at 335M too | It was introduced inside a frontier-scale system, so it inherited that system's framing. Cheap to test: one module, one training run |
| Consistency filtering applied to synthetic data | E5-Mistral's characteristic failure is a generated "hard negative" that is secretly a positive. That is precisely the error a consistency filter detects, and running it on 500k examples costs minutes rather than the days E5 needed for 1.3B | The two ideas come from the same lab fourteen months apart and were framed as alternatives — scrape-and-filter versus generate — rather than as composable steps |
| Instruction-conditioned Matryoshka truncation | Different relations need different capacity. Binary sentiment needs far fewer dimensions than open-domain retrieval. If the instruction selects the geometry, it could also select the useful prefix length | Matryoshka and instruction tuning arrived from different research communities and their loss terms have never been written down together |
| Task-conditioned temperature | Chapter 6 showed classification needs in-batch negatives switched off. But "off" is the limit of raising τ — a large τ spreads gradient over the crowd and stops the same-class neighbour from dominating. A per-task τ is a smooth version of a binary switch | τ is treated as a global hyperparameter by inheritance from SimCLR, where there was only one task |
| Asymmetric pooling — different poolers for the two sides | Chapter 4's entire argument is that queries and passages are different kinds of object. Queries are short and need sharpness; passages are long and need stability. Yet every model applies an identical pooler to both | The shared-encoder convention makes it feel like cheating. It is not: the prefix already makes the two sides different functions |
| Distillation for non-retrieval tasks | Cross-encoder distillation solved the false-negative problem for retrieval. Classification has the same problem, far worse (Chapter 6: 7.75 false negatives per anchor at C = 4), and NV-Embed's answer was to discard the negatives entirely. A classification teacher supplying soft targets would keep them | Cross-encoders are a retrieval-community tool; the classification half of the mixture is usually treated as a fixed dataset rather than something to be re-scored |
[CLS] pooling with synthetic data" is a sentence, not a hypothesis. Every row above states its mechanism in the middle column. If you cannot fill that column, you have found a combination, not an idea.| Situation | Choice | Reasoning |
|---|---|---|
| Prototype, CPU only, small corpus | A small BERT-class embedder (~33M, 384 dims) | A 384-dim index is a quarter the size of a 1024-dim one, and the quality gap barely matters below a million documents |
| Production RAG, English, millions of documents | A 335M-class model with prefixes and Matryoshka | The retrieval sub-average is what matters (Chapter 8), and the last five points of MTEB average cost 20× the compute |
| Many task types on one corpus | Instruction-conditioned, query-side only | Both-sides conditioning multiplies the index by the number of tasks (Chapter 5) |
| Clustering or deduplication | Same prefix on both sides, always | Symmetric relation → the induced transform must cancel (Chapter 4) |
| Unusual domain: code, contracts, clinical notes | Measure first; consider fine-tuning on ~10k in-domain pairs | The consistency filter deleted rare relations from the pretraining data (Chapter 1). Your domain may be one of them |
| Long documents | An LLM-substrate model, or chunk deliberately | Chunking is a lossy decision made before the model sees anything; if you must chunk, overlap and keep section headings |
| Latency budget under 5 ms | Small model + binary quantisation + a reranker on the top 50 | Cheap first pass, accurate second pass — the bi-encoder / cross-encoder split of Chapter 3, deployed as designed |
A text embedder is a function from a string to a direction. Everything in this lesson is an answer to two questions about that function: which pairs of strings should point the same way, and how do you get enough examples of that to matter. E5 answered the second with a scraper and a filter; E5-Mistral answered it with a prompt. E5 answered the first with two prefixes; INSTRUCTOR answered it with a sentence. The loss barely changed across six papers and four years. The data and the conditioning changed completely. That is the shape of progress in this field, and it is a useful thing to know before you go looking for a better loss.
[CLS] slot gets trained.This lesson sits in the middle of a chain. Below it are the primitives; above it are the systems that consume the vectors.
| Go here next if you want… | Lesson |
|---|---|
| The primitive itself: what a vector representation of text is, from zero | Vector Embeddings |
| Why cosine and not Euclidean, and what each metric assumes | Similarity Metrics |
| The loss family behind Chapter 2, derived independently of text | Contrastive Learning |
| The symmetric ancestor of every model here | Sentence-BERT and SimCSE |
| The truncatable-embedding axis from this chapter, in full | Matryoshka Representation Learning |
| What happens after retrieval: the system that consumes these vectors | RAG and Vector Databases |
| How to evaluate an embedder on your own data, properly | Embedding Benchmarks |
nn.functional.normalize, and the four-line InfoNCE from Chapter 2. Prepend "query: " and "passage: ". Train for an afternoon on one GPU. You will have an embedder that beats keyword search on your own corpus — and the 270 million and the 32,768 will stop being numbers you read.