Liang Wang, Nan Yang, Xiaolong Huang, Binxing Jiao, Linjun Yang, Daxin Jiang, Rangan Majumder, Furu Wei (Microsoft) — arXiv:2212.03533, December 2022 · plus the frontier it started

E5 and the Modern Text-Embedder Recipe

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.

Prerequisites: dot products and softmax. Contrastive learning, in-batch negatives, pooling, and instruction conditioning are all built from zero.
10
Chapters
9
Interactive Sims
270M
Filtered Pairs
32,768
Batch Size

Chapter 0: The Commodity Layer

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:

embed : text → Rd

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.

The four products are four operations on one point cloud. Search is nearest neighbours to a query point. RAG is nearest neighbours, then string concatenation. Clustering is partition the cloud. Deduplication is find pairs closer than ε. Nobody has to retrain anything to go from one to the next. The embedder is not a feature of any of those four products; it is the substrate all four are standing on. This is what it means to call it a commodity layer — a component that is invisible when it works and catastrophic when it does not.

Follow the tensors before you follow the argument

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".

StageOutput shapeWhat 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,

u · v = ‖u‖ ‖v‖ cos θ = 1 × 1 × cos θ = cos θ

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.

Why throw away magnitude? Because magnitude in a mean-pooled transformer encodes mostly nuisance: sequence length, token frequency, and the anisotropy of the pretrained space. A 200-word passage and a 6-word query have systematically different norms even when they say the same thing. Normalising removes that confound before it can pollute the ranking — and it also bounds every score into [−1, 1], which makes the temperature in Chapter 2 a meaningful, transferable hyperparameter rather than a per-dataset fudge factor.

What "good" means, operationally

"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.

ProductGeometric operationThe relation it needsWhat breaks if the geometry is wrong
Semantic searchargmaxp cos(q, p) over the corpusp answers q — asymmetricReturns passages that look like the question instead of ones that answer it
RAGtop-k, then concatenate into the promptp answers q, plus diversity across the kSix near-identical chunks fill the context; the model confidently answers from the wrong document
Clusteringk-means / HDBSCAN on the clouda and b share a topic — symmetricClusters form around writing style or ticket template, not subject matter
Deduplicationpairs with cos > 1 − εa and b say the same thing — symmetric, very tightEither merges two genuinely different policies or misses eleven copies of one
Classification probelogistic regression on the frozen vectorthe label is linearly readableThe 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.

One vector space, four products

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.

Product:
Query angle 42°

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.

Why the embedder is the cheapest lever in the stack

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:

0.72 × 0.90 + 0.28 × 0.08 = 0.648 + 0.0224 = 0.670

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:

0.84 × 0.90 + 0.16 × 0.08 = 0.756 + 0.0128 = 0.769

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.

The asymmetry that funded this research area. Retrieval quality multiplies through the whole pipeline and costs almost nothing to serve. Generator quality only helps on the fraction of queries where retrieval already succeeded. When a cheap component sits upstream of an expensive one and gates it, improving the cheap component is the highest-leverage move available — and in late 2022, open text embedders were visibly worse than a twenty-five-year-old keyword baseline on unfamiliar domains. That gap is the hole E5 was built to fill.

Twenty years of trying to turn text into vectors

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.

EraThe representationDimensionWhat it unlockedWhere it broke
TF-IDF (1970s onward)Sparse count vector, one coordinate per vocabulary term, weighted by rarity50k–1M, mostly zerosExact term matching at web scale on a CPUTwo documents with no shared word have cosine exactly 0, however synonymous
LSA / LSI (1990)Truncated SVD of the term–document matrix100–300Latent topics; some synonymy for freeOne 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-occurrence100–300Learned 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 pairs768Genuine sentence-level similaritySymmetric only; out-of-domain retrieval lost to BM25
E5 (2022)Contextual encoder + prefixes + 270M weakly-supervised pairs384–1024Asymmetric retrieval that transfers out of domain512-token window; English-first
Decoder-LLM embedders (2024)7B decoder with pooling surgery and free-form instructions4096Geometry selected at inference by a sentenceRoughly 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:

q · p = Σt qt pt = 0    whenever   { t : qt ≠ 0 } ∩ { t : pt ≠ 0 } = ∅

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.

Read the last column downward. Each generation solved the previous generation's fatal flaw and introduced a subtler one. Sparse-and-exact gave way to dense-and-fuzzy; static words gave way to contextual ones; symmetric similarity gave way to asymmetric retrieval. The recipe in this lesson is not the end of that sequence — Chapter 8 is about how to notice which flaw you are currently living with.

The baseline that kept winning: BM25

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.

Inline concept check — answer before reading on. Why does "without using any labelled data" make the claim stronger rather than weaker?  …  Because BEIR is supposed to measure transfer. A model fine-tuned on MS MARCO that then scores well on BEIR has partly measured how much BEIR resembles MS MARCO. A model that never saw a human relevance judgement and still wins has demonstrated that the signal it learned — naturally occurring text pairs from the web — generalises. The constraint is what makes the measurement mean something.

Three things an embedder is not

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:

recallend-to-end = recallembedder × recallANN index

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

0.84 × 0.95 = 0.798

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.

The debugging order that follows. When retrieval is bad, check in this sequence: (1) is the prefix right and consistent across the whole index — Chapter 4 shows why this fails silently; (2) is the ANN index's recall against exact search above 0.98; (3) is your chunking destroying the answer before the model sees it; (4) and only then, is the embedder the wrong one. Three of those four have nothing to do with the model, and they are cheaper to check.

The recipe has exactly five slots

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.

1. Substrate
What network do you start from? BERT-base, a T5 encoder, MiniLM, or a 7B decoder-only LLM.
2. Data
What counts as a positive pair? Scraped web co-occurrence, human relevance labels, or text an LLM wrote to order.
3. Objective
InfoNCE with in-batch negatives, plus mined hard negatives, plus distillation from a cross-encoder, plus auxiliary reconstruction losses.
4. Pooling
Mean, [CLS], last token, or a trained latent-attention block. The one line that decides what survives.
5. Conditioning
Nothing, fixed prefixes, or a free-form task instruction that reshapes the geometry at inference.

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.

Why this lesson has six papers in it. Reading E5 alone teaches you one configuration. Reading E5 next to BGE, Instructor, E5-Mistral, LLM2Vec, and NV-Embed teaches you the space: five slots, several viable choices each, with published evidence about which swaps pay. That is the difference between knowing a result and being able to invent the next one. Chapter 9 lays the whole grid out and points at the empty cells.

Where we are going

Chapters 1–3 — build E5
Where 1.3 billion training pairs come from and how a filter cuts them to 270 million → why a 32,768-sample batch is the negative-mining strategy → hard negatives and cross-encoder distillation in stage two
Chapters 4–6 — the conditioning story
Why query: and passage: are load-bearing → instructions that give one model many geometries → how decoder LLMs got turned into embedders by three different surgeries
Chapters 7–9 — do it and judge it
A full prefix-conditioned InfoNCE step by hand on three pairs → how to read an MTEB leaderboard without fooling yourself → the design space you recombine to build the next one
Search and deduplication both use cosine similarity on the same embeddings. Why is that a problem rather than a convenience?

Chapter 1: CCPairs and Consistency

Contrastive 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.

The whole first half of E5 is an answer to one question: where do you get half a billion text pairs when only a few million have ever been labelled? The answer is that you stop asking humans and start noticing that the internet is already full of pairs — written by people who were not thinking about retrieval at all.

Pairs that already exist

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.

SourceWhat plays the query roleWhat plays the passage roleWhere the relation is honest, and where it lies
RedditPost title (and body)Top commentHonest when the comment answers the post. Lies constantly: jokes, tangents, "this", and comments that reply to other comments
Stack ExchangeQuestion title + bodyUpvoted answerThe cleanest source in the set — the relation is literally curated by voting, and the vocabulary gap between question and answer is real
English WikipediaEntity name + section titleThe section's passageHonest for factual lookup. Systematically biased toward encyclopedic register; "External links" sections are pure noise
Scientific papersPaper titleAbstractVery high precision, very narrow domain. Teaches technical vocabulary alignment better than anything else in the mix
Common CrawlPage titlePage passageEnormous and filthy. "Home | Acme Inc" paired with a cookie banner is a valid extraction and a worthless pair
NewsHeadlineArticle bodyHonest, 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.

What "weakly supervised" actually means, precisely

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:

P(relevant | co-occurred)  ≫  P(relevant | randomly paired)

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.

Why a wrong positive is worse than a missing one. Contrastive learning has two forces. A positive pair is pulled together; every other item in the batch is pushed away. Now consider a false positive — a post titled "Best pizza in Chicago" paired with the comment "same". The pull force drags the embedding of a food question toward the embedding of a generic agreement token. But other batches contain "same" as a negative for hundreds of unrelated queries, so those gradients push it away from everything. The two forces do not cancel into "no learning"; they fight, and the model resolves the conflict by shrinking the offending region of the space toward the mean — degrading not just that pair but the geometry around it. A missing positive costs you one gradient you never received. A false positive costs you a gradient pointing the wrong way, plus the collateral damage of the resolution.

Deriving the filter from zero

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.

The key insight, stated plainly. A model trained on noisy data encodes the consensus of that data, not its individual examples. So the model can be used to judge the individual examples — not against ground truth, which nobody has, but against what the rest of the corpus collectively believes. Pairs that agree with the consensus survive. Pairs that contradict it are, by construction, the idiosyncratic ones: the jokes, the misalignments, the cookie banners.

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.

Worked example: "top-2 out of a million" is secretly a score threshold

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:

background cosine  ~  Normal(μ = 0.15, σ = 0.06)

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:

E[# better] = 1,000,000 × P( Z > z ),   z = (s − 0.15) / 0.06

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

s* = 0.15 + 4.61 × 0.06 = 0.15 + 0.2766 = 0.4266

Now run four candidate pairs through it, by hand.

PairModel's cos(q, p)z = (s − 0.15)/0.06P(Z > z)Expected # better in 1MVerdict at k = 2
Stack Exchange Q & accepted answer0.627.832.4 × 10−150.0000000024Kept — rank 1, not close
Paper title & abstract0.455.002.87 × 10−70.29Kept — expected rank about 1.3
News headline & loosely-related body0.404.171.53 × 10−515.3Dropped — rank about 16
Reddit post & "same"0.302.506.21 × 10−36,210Dropped — 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.

Here is the punchline. The criterion "rank in the top 2 of a million" is arithmetically identical to "score above 0.4266". It is a threshold, dressed as a rank. And thresholds are brittle — so is this one brittle in k? Change k by four orders of magnitude and watch:
k (rank cutoff)Required tail Pz*Score threshold s*Change from k = 2
11 × 10−64.7530.4352+0.009
2 (E5)2 × 10−64.6110.4266
101 × 10−54.2650.4059−0.021
1001 × 10−43.7190.3731−0.054
1,0001 × 10−33.0900.3354−0.091
10,0001 × 10−22.3260.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.

A transferable lesson about rank-based criteria. Whenever you see "keep the top-k out of N", ask what score that corresponds to under the background distribution. If the background is light-tailed — Gaussian, sub-Gaussian, anything with exponential decay — the rank criterion is a threshold with a √log dial, and you should tune the pool size or the scoring model, not k. If the background is heavy-tailed, the opposite is true and k matters enormously. One line of arithmetic tells you which world you are in.

What the filter costs: purity bought with recall

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:

aligned survivors: 0.45 × P(Z > (0.4266 − 0.415)/0.10 = 0.116) = 0.45 × 0.4538 = 0.2042
weak survivors: 0.55 × P(Z > (0.4266 − 0.18)/0.09 = 2.740) = 0.55 × 0.00307 = 0.0017

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:

QuantityBefore filteringAfter filteringInterpretation
Pairs1,300M268M79% 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 pairs100%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.

The consistency-filtering funnel

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.

Rank cutoff k 2
Pool size 1,000,000

Realization: could you actually run this?

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.

Concept + realization. The idea "keep pairs the model already agrees with" is one sentence. Making it run required: caching pool embeddings in fp16 so they fit in HBM, reshaping a per-query problem into a tiled matmul, and replacing a top-k sort with a threshold count. None of that is in the paper's method section. All of it is why the method section exists.

What everyone else did with the same problem

Consistency filtering is E5's answer. It is not the only one, and the alternatives are informative about the design space.

ModelPair sourceQuality controlScale reaching contrastive training
E5 (2022)CCPairs — 6 web sourcesConsistency filter with a model trained on the noisy set270M
BGE / C-Pack (2023)C-MTP unlabelled — similar web harvesting, English and ChineseHeuristic and rule-based cleaning; an added RetroMAE pretraining stage instead of a filterRoughly 200M
GTE (2023)Multi-source web pairsSource-level sampling weights rather than pair-level filteringRoughly 800M
E5-Mistral (2024)Pairs an LLM was asked to writeQuality is controlled at generation time by the prompt, so no filter is neededAbout 500k, plus labelled data
LLM2Vec (2024)No pairs at all — a sequence paired with a differently-dropped-out copy of itselfNot applicable; positives are constructed, not foundWikipedia 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.

Inline concept check. The consistency filter uses a model trained on the noisy data to judge the noisy data. What kind of pair does it systematically fail to keep, no matter how correct that pair is?  …  A correct pair whose relation is rare. If a form of question–answer alignment appears only a few thousand times in 1.3 billion pairs, the noisy model never learned it, will not rank the passage highly, and will drop every instance. The filter enforces the majority's notion of relevance and quietly deletes minority relations — unusual domains, low-resource phrasings, non-standard registers. That is a real cost, it is invisible in the aggregate benchmark numbers, and it is one of the reasons the frontier moved toward synthetic data, where you can simply ask for the rare cases.
E5 keeps a pair only if its passage ranks in the top 2 out of a million random passages. Why does the exact choice of "2" barely matter?

Chapter 2: Batch Size Is the Miner

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.

L = − log   exp( cos(qi, pi) / τ )  /  Σj=1..N exp( cos(qi, pj) / τ )

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.

The one design decision that makes this cheap. Where do the N − 1 wrong answers come from? They are the other pairs in the same batch. Passage 7 is the correct answer for query 7 and a wrong answer for queries 1 through 6 and 8 through N. Nothing extra was fetched, embedded, or stored. These are in-batch negatives, and they are the reason contrastive learning at this scale is affordable at all.

The economics of in-batch negatives

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 NForward passes (2N)Negative comparisons N(N−1)Comparisons per forward pass
816563.5
25651265,280127.5
4,0968,19216,773,1202,047.5
32,768 (E5)65,5361,073,709,05616,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.

Why the number of negatives is the wrong thing to count

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)/τ).

Termsτ = 0.01τ = 0.05τ = 0.2
Positive0.75111
Hardest negative0.65e−10 = 0.0000454e−2 = 0.1353e−0.5 = 0.6065
Second negative0.55e−20 = 2.1×10−9e−4 = 0.0183e−1 = 0.3679
32,765 background0.1532,765 × e−60 = 2.9×10−2232,765 × e−12 = 0.201332,765 × e−3 = 1,631.4
Denominator sum1.00004541.35491,633.4
Loss = ln(sum)0.00004540.30387.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.

Temperature is a hardness dial, not a scaling constant. Dividing logits by τ before a softmax controls how peaked the distribution is, and here the distribution being peaked or flat decides which negatives own the gradient. Small τ concentrates everything on the hardest one; large τ spreads it uniformly. E5's τ = 0.01 — equivalently, a logit scale of 100 — is a deliberate choice to run in hardest-negative mode. That single number is why the rest of this chapter is about the batch as a mining pool.

How hard is the hardest negative in a batch of N?

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:

cos(u, v)  ~  Normal(0, 1/d),   so σ = 1/√d

The maximum of M independent draws from Normal(0, σ²) concentrates near σ√(2 ln M). Substituting σ = 1/√d and M = N − 1:

E[ hardest random negative ]  ≈  √( 2 ln(N − 1) / d )

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 Nln(N − 1)Hardest random negative, d = 768Relative to N = 8
81.9460.07121.00×
2565.5410.12011.69×
4,0968.3180.14722.07×
32,76810.3970.16462.31×
1,048,57613.8630.19002.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.

Where this model is wrong, and why the truth is better than the model. Real passage embeddings are not uniform on the sphere. They are clustered by topic, and a batch sampled from CCPairs contains many passages about the same subject as the query — several Stack Exchange answers about Python, several Wikipedia sections about the same city. Those are not random directions; they are near directions. So the real hardest in-batch negative is far harder than √(2 ln N / d) says. The formula is a floor: it tells you the hardness you get from luck alone, and everything above it comes from the corpus having structure. This is also why the effect of batch size is larger in practice than the table suggests, and why the next chapter goes hunting for hard negatives deliberately instead of waiting for them.

Realization: how do you put 32,768 sequences in a batch?

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:

32,768 × 1024 × 2 bytes = 67.1 MB per gathered tensor

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.

The subtlety that breaks naive implementations. A plain all-gather is not differentiable in the way you need: gradients computed on device 3 with respect to an embedding produced on device 7 must be routed back to device 7's parameters. Frameworks provide a gradient-aware gather for exactly this, and if you use the ordinary one, your model still trains — slowly, wrongly, and with a loss curve that looks almost right. It is a silent bug, and it is the single most common way a from-scratch contrastive training run quietly underperforms.

The problem large batches create

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

P(at least one) = 1 − (1 − N/270,000,000)r,   N/270M = 32,768 / 2.7×108 = 1.214 × 10−4

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.

The false-negative rate scales with batch size, and it lands entirely on head queries. Doubling the batch doubles the exposure. And because r — the number of valid answers — is enormous for common topics and tiny for specific ones, the damage concentrates on exactly the queries your users type most. At τ = 0.01 it is worse than the raw rate suggests: a false negative is by definition a high-scoring passage, which means it is likely to be the batch's hardest negative, which means it receives essentially the entire gradient. The one wrong item in the batch gets the loudest correction.

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.

Batch size versus negative hardness

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.

Batch size N 32,768
Temperature τ 0.010
Dim d:

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.

Inline concept check. Two runs, same data, same model, same number of gradient steps. Run A: batch 1,024, τ = 0.01. Run B: batch 32,768, τ = 0.2. Which learns more per step, and why is the answer not obvious?  …  Run A, in almost all cases. B has 32 times the negatives, but at τ = 0.2 its gradient is dominated by thousands of trivially easy background negatives the model already separates, so most of the signal is spent restating a solved problem. A's low temperature routes nearly all of its gradient into its own hardest negative — a genuine decision boundary. Negative count and negative usefulness are different quantities, and temperature is the exchange rate between them.
Why is it accurate to say that in E5's stage one the batch size is the negative-mining strategy?

Chapter 3: Hard Negatives and Distillation

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:

Query: "what is the boiling point of water at 2000 m elevation"

Passage A: "Water boils at 100 °C (212 °F) at standard atmospheric pressure of 101.3 kPa. The boiling point is the temperature at which vapour pressure equals ambient pressure."

Passage B: "At an elevation of 2,000 metres, atmospheric pressure falls to roughly 79.5 kPa, and water boils at about 93.4 °C. Cooking times must be extended accordingly."

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.

Hard negative mining, derived

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.

1. Retrieve
Stage-one model (or BM25) returns the top 200 passages for each training query
2. Remove the gold
Drop the labelled positive. Everything left is a candidate negative — ranked by the model's own confusion
3. Sample
Draw 7–15 negatives per query from some rank window, and train with them alongside the in-batch negatives

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.

The rank-1 "negative" is usually a positive

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 windowCandidates in windowUnlabelled positives in windowChance a sampled "negative" is actually relevant
Ranks 1–109 (gold removed)~2.528%
Ranks 1–3029413.8%
Ranks 31–20017021.2%
Ranks 100–200101~0.60.6%
Random from corpus8,800,00060.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:

false negative: e(0.72−0.75)/0.01 = e−3 = 0.04979
genuine hardest negative: e(0.65−0.75)/0.01 = e−10 = 0.0000454

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.

This is the failure mode that stalled dense retrieval for two years. Teams would add hard negatives, watch training loss improve, and watch retrieval quality get worse. The 2020 RocketQA paper diagnosed it precisely: hard negatives are only usable if you first remove the ones that are secretly positives. Their fix — use a stronger model to re-score mined negatives and discard the high-scoring ones — is the ancestor of what E5's stage two does with distillation.

Cross-encoders: accurate and unusable

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-encoderCross-encoder
InputTwo separate sequencesOne concatenated sequence
Query–passage token interactionNone — one scalar at the endEvery layer, every head, every token pair
Output(d,) vector per text(1,) scalar per pair
Corpus precomputationYes — embed once, reuse foreverImpossible — the score depends on the query
Cost per query over 1M passages1M dot products ≈ 1.5 GFLOP1M transformer forwards ≈ 5 × 1016 FLOP
Accuracy on MS MARCO rerankingGoodSubstantially 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.

Why the cross-encoder is more accurate, mechanistically. The bi-encoder must compress a passage into 768 numbers without knowing what will be asked of it. That vector has to simultaneously support "what is the boiling point at altitude", "how does pressure affect cooking", and every other query anyone might type. The cross-encoder never compresses: at layer 3 the token "2000" in the query is already attending to the token "2,000" in the passage. It is answering one question about one pair with the full model. The bi-encoder's whole value proposition — precomputation — is exactly what costs it accuracy.

Distillation: buy the accuracy, skip the cost

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

LKL = Σj Pj · log ( Pj / Qj )

and blend it with the ordinary contrastive term:

L = α · LInfoNCE + (1 − α) · LKL

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.

The elegant part: distillation denoises for free. Remember the false-negative problem — a mined "negative" that is secretly relevant. The one-hot contrastive target demands the student drive its probability to zero. The teacher, being an accurate relevance model, gives it a high score, so the KL target asks the student to keep it high. You do not need a separate denoising pass, a threshold, or a discard rule. The mechanism that transfers the teacher's precision is the same mechanism that protects mislabelled data, because both are consequences of replacing a hard label with a calibrated distribution.

Worked example: one query, four candidates, all the arithmetic

Take a single training instance. The candidate list is the gold passage plus three mined negatives, one of which is secretly relevant.

jPassageCross-encoder logit tjStudent cos(q, pj)
1Gold (labelled positive)8.20.71
2Mined negative — actually relevant, unlabelled5.10.68
3Mined negative — same topic, wrong answer4.60.55
4Random negative−1.30.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:

P = ( 0.9325 , 0.0420 , 0.0255 , 0.0000698 )

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.

Q = ( 0.95257 , 0.047425 , 1.072×10−7 , 2.1×10−26 )

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:

jPjQjln(Pj/Qj)Pj ln(Pj/Qj)
10.93250.95257−0.0213−0.0199
20.04200.047425−0.1214−0.0051
30.02551.072×10−7+12.379+0.3154
40.00006982.1×10−26+49.5+0.0035
LKL0.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:

Q′ = ( 0.6291 , 0.3453 , 0.0256 , 0.0000047 )

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.

The engineering rule this yields. A KL distillation term is only meaningful if the teacher and student distributions are on comparable scales. Reuse your contrastive τ = 0.01 as the student temperature inside the KL and the term degenerates into a pure overconfidence penalty on whatever the student has already crushed — the sign of the correction flips. Working implementations use a separate, larger temperature for the distillation softmax (or normalise the teacher's scores to match the student's spread). If you take one implementation detail from this chapter, take this one: the temperature inside your KL term is not the temperature inside your InfoNCE term.

Choosing α: what each term is actually for

With L = α LInfoNCE + (1 − α) LKL, the two extremes fail in different, predictable ways.

αWhat dominatesFailure mode
1.0 (no distillation)One-hot cross-entropyTrained to hate unlabelled positives; the sparser the labels, the worse the damage
0.7–0.9Contrastive with a soft correctionTypical working range — the contrastive term supplies the strong "gold wins" signal, KL sands off the false negatives
0.0 (pure distillation)Teacher's distribution onlyStudent 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.

Mining depth, false negatives, and what distillation rescues

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.

Window start 1
Window end 30
α (contrastive share) 0.80

Realization: the shape of a stage-two training batch

Stage one's batch was flat: N queries, N passages, one similarity matrix. Stage two's is grouped, and the shapes change accordingly.

TensorShapeMeaning
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.

Inline concept check. Your teacher is a cross-encoder trained on MS MARCO. You are distilling it into a bi-encoder that must work on legal contracts. What breaks?  …  The teacher's scores on legal text are only as trustworthy as its transfer to legal text, and a cross-encoder trained on web search queries is not obviously good at contracts. Distillation transfers the teacher's errors with the same fidelity as its knowledge — and worse, the student now has a soft target that disagrees with the labels, so α is asking you to trade a correct hard label against a confidently wrong soft one. Distillation is only a free lunch inside the teacher's competence.
Cross-encoder distillation is usually described as "transferring accuracy from a stronger model." What is the second, equally important thing it does in a dense-retrieval pipeline?

Chapter 4: query: and passage:

Here is E5's most-copied idea, in its entirety. Before encoding, prepend a literal string:

"query: how do I cancel my plan"
"passage: To end your subscription, open Billing and choose Cancel…"

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.

The impedance mismatch

A function is symmetric when swapping its arguments does not change the answer. Cosine similarity is symmetric by construction:

cos(a, b) = a · b = b · a = cos(b, a)

Now consider the relation retrieval actually needs: p answers q. Ask whether it is symmetric by trying it in both directions.

DirectionStatementTrue?
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.

The score does not have to be symmetric just because the dot product is. Write the scoring function in full:

score(a, b) = fQ(a) · fP(b)

If fQ and fP are the same function, the score is symmetric: swapping a and b gives the same number. If they are different functions, it is not: score(a, b) ≠ score(b, a) in general, because you applied a different map to each side. The asymmetry never has to live in the metric. It lives in the encoders.

Three ways to make the two encoders different

Once you know you need fQ ≠ fP, there are exactly three implementations, and the field tried them in this order.

ApproachHowParametersCan it do symmetric tasks?Used by
One shared encoder, no markerfQ = fPYes — it is the only thing it can doSentence-BERT, SimCSE
Two independent towersTwo full copies of BERT, trained jointlyNo — there is no single map to apply to both sidesDPR (2020)
One encoder + input prefixfQ(x) = f("query: " + x), fP(x) = f("passage: " + x)Yes — use the same prefix on both sidesE5, 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.

One model, two modes, selected at inference by a string. Asymmetric mode: 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.

Worked example: watching a symmetric encoder fail

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):

LabelText
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:

PairCosineWhy
cos(q1, q2)0.87Nearly identical surface form: same length, same register, same four of five words
cos(p1, p2)0.81Same genre, same author, same sentence template
cos(q1, p1)0.41The correct answer — and it scores half as high as an unrelated question does
cos(q1, p2)0.39The 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

P(correct wins) = Φ( 0.02 / 0.0849 ) = Φ(0.2357) = 0.593

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:

P(correct wins) = Φ( 0.26 / 0.0849 ) = Φ(3.064) = 0.9989

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.

Why margins matter more than scores. Notice that the absolute cosines went up too (0.41 → 0.78), and that is the number people quote. It is the less important one. A retrieval system never reports a similarity to a user; it reports an ordering. What determines ordering quality is the gap between the right answer and the best wrong answer, measured in units of score noise. This is a signal-to-noise ratio, and it is the quantity every retrieval design decision should be evaluated against.

Mechanism: how can four tokens do that?

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.

So the prefix works as a conditioning signal, not as an additive bias. It is a two-token instruction that reconfigures twelve layers of processing before any pooling happens. This is the same mechanism as prompting a language model — and it is exactly why the technique generalises: if two tokens can select between "encode this as a question" and "encode this as a document", then a longer, richer instruction can select between far more than two behaviours. Chapter 5 is that observation, taken seriously.

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 production gotcha nobody warns you about

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.

The operational rule. Treat the prefix as part of the index's schema, not as a call-site argument. Store it in the index metadata; refuse to write a vector whose prefix does not match; and version the index whenever the prefix changes. A prefix mismatch is a silent, total, partial-corpus retrieval failure, and it is the single most common way a working E5 deployment degrades after a refactor.
Prefix geometry: watching the query rotate

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.

Prefix strength 0%
Task:

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

cos(R a, R b) = (R a) · (R b) = aTRTR b = aT b = cos(a, b)

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.

Inline concept check. You are embedding a corpus for a clustering dashboard, not for search. Which prefix do you use on the documents?  …  "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.
Why does one shared encoder with query: / passage: prefixes beat two independent encoder towers, even though both make fQ ≠ fP?

Chapter 5: Instructions as Geometry

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.

The reframing. Stop thinking of an embedder as a function of one argument, e(x). Think of it as a function of two: e(x | I), where I is a task instruction. Each instruction induces a different metric on the same corpus. One set of weights, one forward pass, and a family of geometries selected at inference by a string a user can type. This is the natural conclusion of Chapter 0's observation that search, clustering, and deduplication need different relations — and it is what INSTRUCTOR and BGE, from two different labs, arrived at within months of each other.

INSTRUCTOR: one embedder, any task

Su and colleagues (arXiv:2212.09741) built INSTRUCTOR around a fixed instruction template:

"Represent the [domain] [text type] for [task objective]:"

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.

The pooling detail that makes it work

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:

e = w · c + (1 − w) · t

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:

ea · eb = w² + (1−w)² · cos(ta, tb)   and   ‖e‖² = w² + (1−w)²

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)²·cosObserved cosineShift
0.300.07437 + 0.15869 = 0.233060.3863+0.086
0.800.07437 + 0.42318 = 0.497550.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)NumeratorObserved cosine
0.300.23416 + 0.07991 = 0.314070.6275
0.800.23416 + 0.21309 = 0.447250.8936
Dynamic range0.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.

Why this happens, in one sentence. A shared additive component is common-mode signal: it lands identically in both vectors, so it contributes a constant to the numerator and the norms, inflating every cosine toward 1 and squeezing the differences that carry the information. It is exactly the reason instrumentation amplifiers reject common-mode voltage, and exactly the reason centring a dataset before computing correlations matters. INSTRUCTOR excludes the instruction from pooling to reject the common mode — and the longer and more elaborate you make your instructions, the more that decision matters.

BGE: one instruction, and only on the query

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:

"Represent this sentence for searching relevant passages: " + query

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 onlyInstruction on both sides
Documents embedded per corpusOnceOnce per instruction
Index size for T tasks over a 400M-document corpus400M vectors400M × T vectors
Cost of adding a new taskChange a string in the query pathRe-embed and re-index the entire corpus
Instruction compute costPaid on queries only — a few thousand per secondPaid 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."

The asymmetry of asymmetry. Instructions and prefixes are cheap on the side you encode at query time and ruinously expensive on the side you encode at index time. That is why nearly every deployed system puts the conditioning on the query, and why "one embedder, any task" is easier to demonstrate on a benchmark — where corpora are small and re-embedding is free — than to operate. When you read a leaderboard, check which side the instruction went on.

Worked example: what an instruction actually costs

"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.

SideTokens without instructionTokens withCompute changeHow often you pay it
Query823×2.88 — 1.8 GFLOP → 5.1 GFLOPEvery query, thousands per second
Passage200215×1.075 — a 7.5% surchargeOnce 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:

2 × 110×106 × 200 × 400×106 = 1.76 × 1019 FLOPs

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.

The cost of doc-side conditioning is not FLOPs. It is 819 GB per task and one migration pipeline per task. That is why the field converged on query-side instructions even though both-side conditioning is more expressive. It is an operations decision wearing a research decision's clothes, and it is the correct decision for almost everyone.

Where BGE's [CLS] vector comes from: RetroMAE

One 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.

Encoder side
Take a sentence, mask it lightly (about 15%), run the full 12-layer encoder, keep the [CLS] vector. One vector, shape (768,).
↓ hand it to a deliberately crippled decoder
Decoder side
A single transformer layer receives the [CLS] vector plus the same sentence masked aggressively (50–70%), and must reconstruct the original tokens.
Consequence
One layer cannot reconstruct 60%-destroyed text from context. The only available information channel is the [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.

Two teams, two answers, same question. E5 asks "what should the pooled vector be?" and answers "the mean, so no position needs special training." BGE asks the same question and answers "the [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.

Worked example: five sentences, three geometries

The claim "one model, many geometries" deserves to be made concrete. Take five short texts from a restaurant-review corpus:

TextTopicSentiment
S1"The new pasta place downtown is incredible — best carbonara I have had."ItalianPositive
S2"Terrible service at the pasta place downtown, we waited fifty minutes."ItalianNegative
S3"The new sushi bar is fantastic, the omakase is worth every penny."JapanesePositive
S4"Awful experience at the sushi bar, my order never arrived."JapaneseNegative
S5"How do I request a refund for a restaurant booking?"SupportNeutral

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:"

S1S2S3S4
S11.000.810.280.24
S20.811.000.250.31
S30.280.251.000.84
S40.240.310.841.00

Instruction B: "Represent the review for classifying sentiment:"

S1S2S3S4
S11.000.220.790.19
S20.221.000.200.83
S30.790.201.000.17
S40.190.830.171.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.

One model, many geometries

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.

The honest question: does it read the instruction, or memorise it?

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.

The practical rule that follows. Use the instruction string from the model card, verbatim, including punctuation and the trailing colon and space. Do not improve its grammar. Do not translate it. If you need a new task, test it — do not assume a semantically equivalent rewording is behaviourally equivalent. And when you evaluate an instruction-tuned model on your own task, evaluate with three phrasings: the spread between them is a direct measurement of how much of the conditioning is real.

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.

INSTRUCTOR mean-pools over the input text tokens only, excluding the instruction tokens even though they participate in self-attention. Why?

Chapter 6: Decoders Become Embedders

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.

ObstacleWhy it existsConsequence for embeddings
Causal attention maskThe pretraining objective is next-token prediction, which would be trivial if a token could see the futureToken i's state has read only tokens 1…i. Most positions have seen a fraction of the text
No pooling slotDecoders have no [CLS]; every position exists to predict the next oneThere is no position whose job is "summarise the sequence"
No contrastive stageLLMs are trained with cross-entropy on tokens, never with a similarity objectiveThe space is not organised so that dot products mean relatedness

Deriving the causal-mask problem

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 i12345678
Tokens visible12345678
Visibility vi12.5%25%37.5%50%62.5%75%87.5%100%
average visibility = (1+2+3+4+5+6+7+8) / (8 × 8) = 36 / 64 = 0.5625

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.

Mean-pooling a causal model averages states that have read, on average, half the text — and it is worse than that number suggests because the deficit is front-loaded. Position 1's final state has seen exactly one token: itself. After twelve or thirty-two layers of processing, it is still fundamentally the embedding of a single word, contributing pure lexical noise to the average. Position 2 has seen two. The first quarter of any sequence contributes states that could not possibly know what the text is about, and mean pooling weights them exactly as heavily as the last position, which knows everything.

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.

The three obstacles are one obstacle, restated three ways. A decoder-only LLM was optimised end to end for a task whose output is the next token. An embedder needs an output that is a whole-sequence summary. Everything the three papers do — flip the mask, append an EOS, train a latent pooler, add a contrastive objective — is a way of manufacturing a summary slot in an architecture that was never asked to have one.

Path 1 — E5-Mistral: change nothing, generate everything

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:

Step 1 — brainstorm
Ask a strong LLM to list retrieval tasks: "give me 20 kinds of query–document relationship a search system might need." You get task descriptions, not examples.
↓ for each task…
Step 2 — instantiate
Ask for a triple: a query, a positive document, and a hard negative — a document that looks right and is not. Vary length, difficulty, language, and register through the prompt.
Result
About 500k examples across roughly 150k unique task instructions, covering 93 languages. Hard negatives arrive pre-mined, because you asked for them.

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:

full matrix: 4096 × 4096 = 16,777,216
LoRA rank 16: 4096×16 + 16×4096 = 131,072  —  0.78%

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.

The number that should stop you. Fewer than one thousand gradient steps. E5's stage one is 270 million pairs. E5-Mistral reaches a higher MTEB score with roughly three orders of magnitude less contrastive training — because the substrate arrived already knowing what text means, and all that remained was to teach it where to put the answer. Slot 1 of the recipe absorbed the work that slots 2 and 3 used to do.

Path 2 — LLM2Vec: perform surgery on the mask

BehnamGhader and colleagues (arXiv:2404.05961) attacked the obstacle directly. If the causal mask is the problem, remove the causal mask.

1. Bidirectional attention
Replace the lower-triangular attention mask with an all-ones mask. One line of code. Every token can now see every token — and the model, which has never experienced this, gets substantially worse.
2. Masked next token prediction (MNTP)
Adapt the model to its new mask by masking tokens and predicting them — but predicting token i from the hidden state at position i − 1, not position i.
3. Unsupervised contrastive (SimCSE)
Encode the same sequence twice with independent dropout masks. The two slightly different vectors are a positive pair. No text pairs needed at all.

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.

The whisper in the LLM2Vec results. The authors observe that Mistral-7B is oddly robust to having its attention mask flipped, degrading far less than other decoder LLMs before any repair training. Their careful suggestion is that Mistral may have seen some bidirectional attention during pretraining — a prefix-LM-style objective, perhaps. It is a small remark in a results table and it is the most interesting sentence in the paper: it says the boundary between "encoder" and "decoder" is a training-recipe artefact, not an architectural law, and that some models are already standing on both sides of it.

Path 3 — NV-Embed: build a pooler worth having

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:

TensorShapeRole
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's second finding: when to switch off in-batch negatives

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

P = 1 − (1 − 1/C)N−1
TaskClasses CBatch NP(at least one false negative)Expected false negatives per anchor
Sentiment (binary)232> 0.9999915.5 of 31
Topic classification4320.999897.75 of 31
Intent classification20320.79611.55 of 31
Retrieval (CCPairs)32,7680.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.

The rule that falls out. In-batch negatives are safe exactly when the label space is enormous and sparse — retrieval over a 270-million-passage corpus, where a random other item is almost certainly irrelevant. They are actively destructive when the label space is small and dense — classification and clustering, where a random other item shares your label with probability 1/C. NV-Embed's two-stage design is not a heuristic; it is the only configuration in which both task families can be trained by the same objective without one poisoning the other.
Attention mask and pooling surgery

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.

Mask:
Pooling:
Sequence length L 10

The three paths side by side

SlotE5-MistralLLM2VecNV-Embed
SubstrateMistral-7B, causal mask keptDecoder LLM, mask flipped to bidirectionalMistral-7B, mask removed during contrastive training
DataLLM-generated triples, ~500k, plus labelled dataRaw Wikipedia; positives from dropout (SimCSE)Curated public retrieval and non-retrieval mixtures
ObjectiveInfoNCE, single stage, fewer than 1k stepsMNTP adaptation, then unsupervised contrastive, then optional supervisedTwo-stage: retrieval, then a blend with in-batch negatives disabled
PoolingAppended [EOS] tokenMean (valid again once bidirectional)Trained latent-attention layer
ConditioningFree-form task instruction, query side onlyOptional instructionFree-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.

What the seven billion parameters cost

Before you reach for a 7B embedder, price it against the 335M one honestly.

QuantityE5-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,0488,192
Index size for 400M documents819 GB3.28 TB
Query encode latency (single GPU, rough)1–3 ms10–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.

Inline concept check. LLM2Vec's SimCSE step builds positives by encoding the same text twice with different dropout. What does that objective teach, and what can it not possibly teach?  …  It teaches invariance to the model's own noise — the embedding must be stable under small internal perturbations, which flattens the anisotropic cone and spreads embeddings out over the sphere. That is genuinely valuable and it is why unsupervised LLM2Vec beats untuned baselines by a wide margin. But it cannot teach the asymmetric relation from Chapter 4, because a text and its dropout twin are the same text: there is no question-to-answer direction anywhere in the signal. Which is exactly why LLM2Vec's unsupervised number (~56.8) sits well below its supervised one (~64.8), and the gap is almost entirely retrieval.
Why does NV-Embed disable in-batch negatives in its second training stage, when Chapter 2 showed that in-batch negatives are the entire economic basis of contrastive training?

Chapter 7: InfoNCE, By Hand

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.

The batch

Three query–passage pairs, which is our entire batch. N = 3.

iQueryPassage
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.

VectorRaw outputNorm ‖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.

Stage A: the similarity matrix with no prefix

Nine dot products. Two worked in full, the rest by the same recipe.

cos(q1, p1) = 0.94868×0.60000 + 0.31623×0.80000 = 0.56921 + 0.25298 = 0.82219
cos(q2, p2) = 0.89443×0.31623 + 0.44721×0.94868 = 0.28285 + 0.42426 = 0.70711

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.82220.60000.4190p1 — correct
q2 (python)0.89440.70710.5432p1wrong
q3 (sky)0.95320.80870.6675p1wrong

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.

Hubness is a real, named pathology of embedding spaces, and here it is in three by three. When one axis of variation — register, length, formality — dominates the geometry, the items nearest the centre of that dominant axis become nearest neighbours of nearly everything. In production this shows up as a handful of documents that appear in the top-10 for thousands of unrelated queries, usually boilerplate: an FAQ index, a terms-of-service page, a "contact us" stub. If you have ever wondered why one useless document keeps polluting your search results, you have met a hub, and its cause is that the geometry is encoding the wrong relation.

Stage B: the prefix map

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

A = [ [ 5, −5 ] , [ −1, 15 ] ]

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:

A q1 = [ 5×9 − 5×3 , −1×9 + 15×3 ] = [ 45 − 15 , −9 + 45 ] = [ 30 , 36 ]
A q2 = [ 5×8 − 5×4 , −1×8 + 15×4 ] = [ 40 − 20 , −8 + 60 ] = [ 20 , 52 ]
A q3 = [ 5×7 − 5×5 , −1×7 + 15×5 ] = [ 35 − 25 , −7 + 75 ] = [ 10 , 68 ]

Normalise each:

Prefixed queryRawNormUnit 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:

cos(q′2, p2) = 0.35898×0.31623 + 0.93335×0.94868 = 0.11353 + 0.88545 = 0.99897
p1p2p3Row argmaxMargin
q′10.998660.931190.83414p1 — correct0.0675
q′20.962070.998970.96721p2 — correct0.0318
q′30.878780.984610.99930p3 — correct0.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.

Stage C: the loss, at three temperatures

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.

Li = log [ 1 + Σj ≠ i exp( (sij − sii) / τ ) ]

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:

L1 = ln(1.001177) = 0.001177

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.

L = (0.001177 + 0.06460 + 0.20720) / 3 = 0.272977 / 3 = 0.09099

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

L2 = ln( 1 + 1.365×108 + 7.6×10−8 ) = ln(1.365×108) = 18.732

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.

The identity worth memorising. When the model is wrong and τ is small, InfoNCE collapses to

Li ≈ ( maxj sij − sii ) / τ

the margin deficit measured in units of temperature. That is why τ = 0.01 produces losses in the tens rather than near ln(N): the loss is not reporting "how uncertain am I over N options", it is reporting "by how much did the wrong answer beat the right one, amplified a hundredfold". A small temperature does not merely sharpen the softmax; it converts a probability into a ruler.
LStage A = (0 + 18.732 + 28.573) / 3 = 47.305 / 3 = 15.768

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 prefixL, with prefixRatioln 3 (random guessing)
0.0115.7680.0910173×1.0986
0.053.1850.52136.1×1.0986
0.201.2460.88721.40×1.0986
1.001.08941.05151.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.

Stage D: the gradient, and where it points

Differentiate. Writing sij = qi · pj and Pij for the row softmax of s/τ:

∂Li / ∂qi = (1/τ) [ Σj Pij pj − pi ]

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 P ≈ (1, 0, 0), and the weighted average is just p1:

∂L2/∂q2 = 100 × [ (0.60000, 0.80000) − (0.31623, 0.94868) ] = 100 × (0.28377, −0.14868) = ( 28.377 , −14.868 )

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 P = (4.7×10−6, 0.18715, 0.81285). The weighted average of the passages is

Σj P3j pj = ( 0.14895 , 0.98537 )

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.

One technical caveat, because it bites people. q is constrained to the unit sphere, so the raw gradient above is not the update you apply — its component along q itself would change the vector's length, which normalisation immediately undoes. The effective update is the gradient projected onto the tangent space: g − (g · q) q. Autograd handles this automatically when the normalisation is inside the graph. It does not handle it if you normalise with 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.
The three-pair batch, live

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.

Prefix strength 100%
Temperature τ 0.010

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.

What the toy gets right and what it hides. Right: normalisation, the exact InfoNCE arithmetic, the margin-over-temperature identity, hubness, the shape of the gradient, and the fact that conditioning acts on one side of the batch. Hidden: in 2-D there is only one direction to move, so the model cannot use extra dimensions to satisfy conflicting constraints — which is exactly what makes 768 dimensions work, and exactly why a real batch is 32,768 rather than 3. The mathematics is complete; the capacity is not.
In Stage A, row 2's loss came out to exactly 18.732, and the gap between the winning wrong passage and the correct one was 0.18732. Why are those the same number up to a factor of 100?

Chapter 8: Reading MTEB Honestly

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.

What the average actually averages

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 typeDatasetsImplied weight in the averageMetric
Retrieval1526.8%nDCG@10
Classification1221.4%Accuracy of a logistic probe
Clustering1119.6%V-measure after k-means
Semantic textual similarity (STS)1017.9%Spearman correlation
Reranking47.1%MAP
Pair classification35.4%Average precision
Summarization11.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.

An accident of dataset availability became the objective function of an entire research field. This is not a criticism of MTEB's authors, who documented exactly what they did. It is a criticism of the reflex to sort by one column. When you optimise a model to climb the MTEB average, you are optimising against a weighting nobody chose, and the parts of "text embedding" with few public datasets — long-document retrieval, code, domain-specific technical corpora, multilingual low-resource pairs — are quietly worth close to nothing.

Worked example: what does a one-point average gap mean?

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.

ScenarioDistribution of the 56 pointsDatasets B winsWhat it means for a RAG pipeline
Uniform+1.00 on every dataset56 of 56Genuinely better everywhere. Rare in practice
Retrieval-concentrated+3.73 on each of the 15 retrieval sets, 0 elsewhere15 of 56, ties on 41Excellent — this is the improvement you actually wanted
STS-concentrated+5.60 on each of the 10 STS sets, 0 elsewhere10 of 56Worthless — your retrieval quality is unchanged
Adversarial+5.00 on 15 retrieval sets (+75), −0.463 on the other 41 (−18.98)15 of 56Great 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.

The reverse case is the one that costs money. Model A beats B by 1.00 on the average because A is strong on classification and STS. You deploy A for RAG. Your retrieval is worse, your end-to-end answer accuracy drops by the Chapter 0 arithmetic, and the leaderboard told you the opposite. You cannot detect this from the average. You can detect it in ten seconds from the task-type breakdown, which is published for every model and which almost nobody reads.

A real pair that makes the point

This is not hypothetical. Two models from the same lab, built on the same T5 backbone, differing only in training data:

ModelMTEB averageRetrieval sub-averageSTS sub-average
Sentence-T5-XXL (4.8B)~59.5~42.2~82.6
GTR-XXL (4.8B)~59.0~48.5~77.8
Difference0.56.34.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.

The contamination problem

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 datasetPublic training split exists?Commonly in embedder training mixtures?
MS MARCOYes, 500k pairsAlmost universally
Natural QuestionsYesVery commonly — E5 stage two uses it
HotpotQAYesCommonly
FEVERYesCommonly
QuoraYesCommonly
FiQAYesSometimes
SciFact, TRECCOVID, ArguAna, Touche, CQADupstack, NFCorpus, Climate-FEVER, DBPedia, SCIDOCSMostly noRarely

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.

What to compute instead, and it takes five minutes. Split the retrieval sub-average into the datasets the model's published training mixture includes and the ones it does not. Report both. If a model's in-domain average is far above its held-out average, it will transfer badly to your corpus, which is held-out by definition. That single split is more predictive of production behaviour than the entire headline number, and every input it needs is already public.

Is the average at least statistically stable?

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:

Var(mean) = ( 11 × 1.0² + 45 × 0.2² ) / 56² = ( 11 + 1.8 ) / 3136 = 0.004082
sd(mean) = √0.004082 = 0.064 points

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.

The average is precise. That is exactly why it is dangerous. Averaging 56 datasets suppresses noise by a factor of about √56, so the number is stable and reproducible — and stability gets mistaken for validity. The problem with the MTEB average was never that it is noisy. It is that it is a reliable measurement of a quantity you did not want: performance under a weighting determined by 2022 dataset availability, mixed across in-domain and out-of-domain retrieval. Precision without validity is the most persuasive kind of wrong number.

What MTEB cannot see at all

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 problemMeasured by MTEB?Why it matters
Documents longer than a paragraphNo — retrieval passages are shortModel rankings routinely invert between 100-token and 4,000-token documents
Your domainOnly if it resembles one of the 56Chapter 1: the consistency filter deleted rare relations from pretraining
Behaviour under your chunkingNo — chunks are handed to the modelChunking is a lossy decision made before the model sees anything
Index size and query latencyNoA 4096-dim model is a 4× storage bill that never appears on the leaderboard
Robustness to typos, casing, boilerplateNoReal queries are misspelt; real documents have navigation chrome
Degradation as the corpus growsNo — corpora are fixed and modestMore documents means more near-duplicates competing for the top slot
Mixed-language corporaOnly in the multilingual variantCode-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.

Worked example: how big does your own evaluation need to be?

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

√( 0.75 × 0.25 / 200 ) = √0.0009375 = 0.0306

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:

C(26,18) + C(26,19) + … + C(26,26)
= 1,562,275 + 657,800 + 230,230 + 65,780 + 14,950 + 2,600 + 325 + 26 + 1 = 2,533,987
pone-sided = 2,533,987 / 226 = 2,533,987 / 67,108,864 = 0.0378  →   two-sided 0.076

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.

DesignnCan it resolve a 5-point gap?
Unpaired200No — the confidence interval is ±8.5 points
Paired (McNemar)200Suggestive — p = 0.076
Paired (McNemar)400Yes — p = 0.008
Unpaired400No — the interval is still ±6.0 points
Two rules from four numbers. First, always pair: the same query set for every model, scored side by side. Pairing is worth more than doubling your sample. Second, four hundred labelled queries is the realistic entry price for distinguishing two good embedders on your own data — roughly a day of work, and it answers a question no leaderboard can.

The fourth problem: the leaderboard is a training signal

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.

Task-level versus average

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.

Concentration even
Pays for it elsewhere 0%

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.

The protocol: how to actually choose an embedder

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.

1. Read the task-type row, not the average
Building search or RAG? Sort by Retrieval. Building a topic dashboard? Sort by Clustering. The per-task columns are published for every model on the leaderboard.
2. Split in-domain from held-out
Read the model's training mixture from its paper or card. Compute the retrieval average over the datasets it did not train on. That number predicts transfer to your corpus.
3. Build a 200-query eval on your own data
Real queries with a known correct document each, scored on the same query set for every model. The arithmetic above says roughly 400 paired queries to settle a five-point recall gap; 200 will show you the effect without letting you swear to it.
4. Measure the thing you sell
End-to-end answer accuracy, not recall@10. Chapter 0's arithmetic converts one into the other, and only one of them is what your users experience.

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.

Inline concept check. Model B is +2.0 MTEB average over Model A, and +2.0 on the retrieval sub-average too. You deploy it and your recall@10 drops. What is the most likely explanation?  …  Your documents. MTEB retrieval passages are short — typically a paragraph. If your corpus is 4,000-token contracts or code files, you are measuring a regime the benchmark never touched, and models tuned on short passages routinely invert their ranking on long ones. The second most likely explanation: B's gains are concentrated in the in-domain datasets it trained on, and your corpus is out of domain for everything. Both are invisible in every published number and both take one afternoon to measure on your own data.
Two embedders have MTEB averages of 59.5 and 59.0. What is the single most important thing that comparison fails to tell you?

Chapter 9: The Design Space

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.

The five slots, filled in

Slot 1 — Substrate. What network are you starting from?

OptionUsed byWhat it buysWhat it costs
BERT-base / large (110M–335M)E5, BGE, GTEBidirectional by construction; cheap to serve; 768–1024 dimsSmall; 2018-era pretraining data; 512-token limit
MiniLM (33M)E5-smallRuns on a CPU; 384 dims means a 2× smaller indexA few MTEB points below base
T5 encoderSentence-T5, GTR, INSTRUCTORStrong multitask pretraining; scales to 4.8BEncoder-decoder overhead; the decoder half is dead weight
Decoder LLM, causal mask keptE5-MistralEnormous pretraining; instruction-following already presentCausal mask forces last-token pooling; 4096 dims; ~21× the compute
Decoder LLM, mask flippedLLM2Vec, NV-EmbedEverything above plus full visibility at every positionNeeds repair training (MNTP) after the flip

Slot 2 — Data. Where do positive pairs come from?

OptionUsed byScaleThe characteristic failure
Scraped web co-occurrence + consistency filterE5 (CCPairs)1.3B → 270MDeletes rare relations the noisy filter model never learned
Scraped web + heuristic cleaning + RetroMAE stageBGE / C-Pack~200MLower purity; compensated by a stronger pretraining stage
Human relevance labelsEveryone, in stage two~1MSparse labels → unlabelled positives mined as negatives
LLM-generated triplesE5-Mistral~500kInherits the generator's blind spots; "hard negatives" that are secretly positive
No pairs at all — dropout twinsLLM2Vec (SimCSE)Any raw corpusCannot teach an asymmetric relation; retrieval lags badly

Slot 3 — Objective. What does the loss say?

OptionUsed byWhat it adds
InfoNCE, in-batch negatives, huge batchE5 stage 1, BGE stage 2Coarse global geometry, cheaply. Batch size is the mining pool
+ mined hard negativesE5 stage 2, BGE stage 3, NV-EmbedFine local boundaries — and a false-negative problem
+ cross-encoder KL distillationE5 stage 2Teacher precision, and automatic denoising of mined negatives
Masked auto-encoding (RetroMAE)BGE stage 1Pretrains the pooling slot before contrastive training starts
MNTP (masked next-token prediction)LLM2VecRepairs a decoder after the attention mask is flipped
Two-stage, in-batch negatives disabled in stage 2NV-EmbedLets retrieval and classification share one model without poisoning each other

Slot 4 — Pooling. How do L token vectors become one?

OptionUsed byRequiresWeakness
MeanE5, LLM2Vec, INSTRUCTORBidirectional attentionWeights boilerplate as heavily as the subject noun
[CLS]BGEA pretraining stage that makes [CLS] worth readingUntrained out of the box; one position carries everything
Last token / appended [EOS]E5-MistralNothing — works under a causal maskThat state was optimised to predict the next token, not to summarise
Trained latent attentionNV-EmbedA trainable (512, d) dictionary and one cross-attentionExtra parameters; only ever demonstrated at 7B

Slot 5 — Conditioning. What tells the model which relation to encode?

OptionUsed byIndex costFlexibility
NothingSentence-BERT, SimCSEOne indexSymmetric relations only
Two fixed prefixesE5One indexTwo geometries, selected by a four-token string
One fixed instruction, query side onlyBGEOne indexOne asymmetric geometry, no re-indexing ever
Free-form instruction, both sidesINSTRUCTOROne index per instructionMost flexible on paper; most expensive to operate
Free-form instruction, query side onlyE5-Mistral, NV-EmbedOne indexThe pragmatic consensus

Three cross-cutting axes

These are orthogonal to the five slots and are usually decided late, which is why they are usually decided badly.

AxisThe choiceWhat it controls
Dimension384 / 768 / 1024 / 4096, or Matryoshka-trained so the prefix of the vector is itself usableIndex size and search latency, linearly. Matryoshka converts a one-time decision into a runtime knob
Context length512 tokens (BERT) versus 8k or 32k (LLM substrates)Whether you must chunk. Chunking is a lossy decision made before the model sees anything
Quantisationfp32 → fp16 → int8 → binaryIndex 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

Reading the grid as an invention procedure

Look back at how the papers in this lesson were actually produced. Each is a transplant.

PaperWhat it borrowedWhat it changedWhat was genuinely new
E5InfoNCE, in-batch negatives, cross-encoder distillation, two-stage trainingData scale, and a consistency filterCCPairs + the filter; the prefix convention
BGE / C-PackE5's two-stage shapeAdded RetroMAE; moved pooling to [CLS]; one query-side instructionThe full open data + benchmark + model package
INSTRUCTORGTR substrate; contrastive trainingPrefix → free-form instruction; instruction excluded from poolingMEDI, and the "any task" framing
E5-MistralE5's objective and instruction ideaSubstrate → 7B decoder; data → synthetic; dropped stage one entirelyThe two-step synthetic data recipe
LLM2VecSimCSE's objective; MNTP from masked LM literatureFlipped the attention maskShowing the flip is cheap and reversible — three steps, no pairs
NV-EmbedNearly everything aboveTwo-stage instruction tuning; in-batch negatives off in stage twoLatent-attention pooling
The procedure, stated as a procedure. Take a technique that is proven inside one slot configuration. Ask what, precisely, in that configuration the technique required. Usually the answer is "less than you assumed." Then transplant it into a different configuration and measure. Every row above is that move. It is not a lesser form of research than inventing a new loss function — it is the form that produced every model you would actually deploy.

The empty cells

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.

RecombinationWhy it should workWhy it has not been tried
Latent-attention pooling on a 335M encoderNV-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 tooIt 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 dataE5-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.3BThe 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 truncationDifferent 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 lengthMatryoshka and instruction tuning arrived from different research communities and their loss terms have never been written down together
Task-conditioned temperatureChapter 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 sidesChapter 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 bothThe shared-encoder convention makes it feel like cheating. It is not: the prefix already makes the two sides different functions
Distillation for non-retrieval tasksCross-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 themCross-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
How to tell a real empty cell from a bad idea. A real one has a mechanism argument: "technique T solved problem P; problem P also occurs in configuration C; nothing in T's derivation depended on the configuration it was born in." A bad one is a product of the grid with no mechanism — "nobody has tried [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.

Cheat sheet: what to actually use

SituationChoiceReasoning
Prototype, CPU only, small corpusA 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 documentsA 335M-class model with prefixes and MatryoshkaThe 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 corpusInstruction-conditioned, query-side onlyBoth-sides conditioning multiplies the index by the number of tasks (Chapter 5)
Clustering or deduplicationSame prefix on both sides, alwaysSymmetric relation → the induced transform must cancel (Chapter 4)
Unusual domain: code, contracts, clinical notesMeasure first; consider fine-tuning on ~10k in-domain pairsThe consistency filter deleted rare relations from the pretraining data (Chapter 1). Your domain may be one of them
Long documentsAn LLM-substrate model, or chunk deliberatelyChunking is a lossy decision made before the model sees anything; if you must chunk, overlap and keep section headings
Latency budget under 5 msSmall model + binary quantisation + a reranker on the top 50Cheap first pass, accurate second pass — the bi-encoder / cross-encoder split of Chapter 3, deployed as designed

The one paragraph to keep

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.

References

  1. Wang, L., Yang, N., Huang, X., Jiao, B., Yang, L., Jiang, D., Majumder, R., Wei, F. "Text Embeddings by Weakly-Supervised Contrastive Pre-training," 2022 — arXiv:2212.03533. The anchor paper: CCPairs, consistency filtering, the two-stage recipe, and the prefixes.
  2. Xiao, S., Liu, Z., Zhang, P., Muennighoff, N. et al. "C-Pack: Packed Resources For General Chinese Embeddings," 2023 — arXiv:2309.07597. BGE, RetroMAE-first three-stage training, and query-side-only instruction.
  3. Su, H., Shi, W., Kasai, J. et al. "One Embedder, Any Task: Instruction-Finetuned Text Embeddings," 2022 — arXiv:2212.09741. INSTRUCTOR, MEDI, and instruction-excluded pooling.
  4. Wang, L., Yang, N., Huang, X., Yang, L., Majumder, R., Wei, F. "Improving Text Embeddings with Large Language Models," 2024 — arXiv:2401.00368. E5-Mistral: synthetic data, LoRA, EOS pooling, fewer than 1k steps.
  5. BehnamGhader, P., Adlakha, V., Mosbach, M., Bahdanau, D., Chapados, N., Reddy, S. "LLM2Vec: Large Language Models Are Secretly Powerful Text Encoders," 2024 — arXiv:2404.05961. Bidirectional-attention surgery, MNTP, unsupervised SimCSE.
  6. Lee, C., Roy, R., Xu, M. et al. "NV-Embed: Improved Techniques for Training LLMs as Generalist Embedding Models," 2024 — arXiv:2405.17428. Latent-attention pooling and two-stage instruction tuning.
  7. Muennighoff, N., Tazi, N., Magne, L., Reimers, N. "MTEB: Massive Text Embedding Benchmark," 2022 — arXiv:2210.07316. The benchmark Chapter 8 takes apart.
  8. Thakur, N., Reimers, N., Rücklé, A., Srivastava, A., Gurevych, I. "BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models," 2021 — arXiv:2104.08663. Where the BM25 baseline that E5 had to beat lives.
  9. van den Oord, A., Li, Y., Vinyals, O. "Representation Learning with Contrastive Predictive Coding," 2018 — arXiv:1807.03748. The origin of the InfoNCE loss.
  10. Chen, T., Kornblith, S., Norouzi, M., Hinton, G. "A Simple Framework for Contrastive Learning of Visual Representations" (SimCLR), 2020 — arXiv:2002.05709. Where large batches and low temperature became standard.
  11. Radford, A. et al. "Learning Transferable Visual Models From Natural Language Supervision" (CLIP), 2021 — arXiv:2103.00020. The two-tower contrastive template everything here inherits.
  12. Karpukhin, V. et al. "Dense Passage Retrieval for Open-Domain Question Answering" (DPR), 2020 — arXiv:2004.04906. The two-tower design the prefix replaced.
  13. Gao, T., Yao, X., Chen, D. "SimCSE: Simple Contrastive Learning of Sentence Embeddings," 2021 — arXiv:2104.08821. Dropout as a positive-pair generator; LLM2Vec's step 3.
  14. Xiao, S., Liu, Z., Shao, Y., Cao, Z. "RetroMAE: Pre-Training Retrieval-oriented Language Models Via Masked Auto-Encoder," 2022 — arXiv:2205.12035. How BGE's [CLS] slot gets trained.
  15. Qu, Y. et al. "RocketQA: An Optimized Training Approach to Dense Passage Retrieval," 2020 — arXiv:2010.08191. The false-negative diagnosis behind Chapter 3.
  16. Xiong, L. et al. "Approximate Nearest Neighbor Negative Contrastive Learning for Dense Text Retrieval" (ANCE), 2020 — arXiv:2007.00808. Hard-negative mining with periodic index refresh.
  17. Hofstätter, S. et al. "Improving Efficient Neural Ranking Models with Cross-Architecture Knowledge Distillation," 2020 — arXiv:2010.02666. Cross-encoder to bi-encoder distillation.
  18. Hofstätter, S. et al. "Efficiently Teaching an Effective Dense Retriever with Balanced Topic Aware Sampling" (TAS-B), 2021 — arXiv:2104.06967. Batch composition as a design variable.
  19. Reimers, N., Gurevych, I. "Sentence-BERT," 2019 — arXiv:1908.10084. The symmetric baseline Chapter 4 breaks.
  20. Ni, J. et al. "Sentence-T5," 2021 — arXiv:2108.08877; and Ni, J. et al. "Large Dual Encoders Are Generalizable Retrievers" (GTR), 2021 — arXiv:2112.07899. The pair Chapter 8 uses to show what an average hides.
  21. Kusupati, A. et al. "Matryoshka Representation Learning," 2022 — arXiv:2205.13147. Truncatable embeddings.
  22. Hu, E. et al. "LoRA: Low-Rank Adaptation of Large Language Models," 2021 — arXiv:2106.09685. How E5-Mistral fine-tunes 7B without erasing it.
  23. Li, Z. et al. "Towards General Text Embeddings with Multi-stage Contrastive Learning" (GTE), 2023 — arXiv:2308.03281; Nussbaum, Z. et al. "Nomic Embed," 2024 — arXiv:2402.01613. Two more points in the same design space, with different data-mixture choices.

Connections

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 zeroVector Embeddings
Why cosine and not Euclidean, and what each metric assumesSimilarity Metrics
The loss family behind Chapter 2, derived independently of textContrastive Learning
The symmetric ancestor of every model hereSentence-BERT and SimCSE
The truncatable-embedding axis from this chapter, in fullMatryoshka Representation Learning
What happens after retrieval: the system that consumes these vectorsRAG and Vector Databases
How to evaluate an embedder on your own data, properlyEmbedding Benchmarks
Cross-domain bridge
Consistency filtering is co-training, and hubness is a database index problem
Chapter 1's filter — train a weak model on noisy data, then use it to select which noisy data to keep — is the same move as self-training in semi-supervised learning and as co-training when two views are available. The correctness argument is identical in all three: a model fitted to a billion examples encodes the consensus, so it can adjudicate individual examples it was itself trained on, provided the error is idiosyncratic rather than systematic. And Chapter 7's hub — one passage that is nearest neighbour to every query — is the same phenomenon database engineers meet as a skewed index: a key that matches most queries provides no selectivity and destroys the point of having an index at all. Both fields solved it the same way: change the space so the dominant axis is no longer the one you rank on.
"What I cannot create, I do not understand."
Take a pretrained encoder, five hundred thousand title–abstract pairs from arXiv, one 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.
Across six papers and four years, which component of the recipe changed least, and what does that tell you about where to look for the next improvement?