LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnTransformer Deep DivesSentence Embeddings & Contrastive Loss
📐HardEmbeddings & Vector Search

Sentence Embeddings & Contrastive Loss

Learn how contrastive losses train sentence embeddings, why hard negatives matter, and how retrieval systems combine bi-encoders, rerankers, and dimension tradeoffs.

44 min read
Learning path
Step 95 of 196 in the full curriculum
Capstone: Reproducible ML StudyEmbedding Similarity & Quantization

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A developer asks "How do I rotate an API key?" Two passages look plausible. One says "Create a replacement key, deploy it, then revoke the old key." Another discusses a billing export token. They're topic-adjacent, but only one answers the question. A keyword-heavy search can still send the developer to the wrong guide.

Start with a retrieval contract: for this query, the rotation guide must rank above the lookalike. A sentence embedding maps each string to one fixed-width vector. Contrastive learning trains that geometry so an intended match lands nearer the query than a confusing candidate. The reproducible-study capstone already treated a training-loss drop as a claim you have to test. Apply the same habit here: a pooling choice, a hard-negative trick, or a leaderboard average is a hypothesis about ranking, and held-out retrieval is the test.

QueryPassage that should rank near itTempting non-match
"How do I rotate an API key?""Create a replacement key, deploy it, then revoke the old key.""A billing export token expired yesterday."

The vector only proposes which text gets considered. Authorization still has to run before any passage becomes answer evidence, the same gate the document-QA capstone already shipped.

Keep this ranked triplet in view: the rotation guide should score above the billing-token passage, and both should remain below an authorization boundary if the caller lacks access. A lower contrastive loss without that held-out ordering can mean the batch got easier rather than the retriever got better. Start by watching pooling erase word order; that gives contrastive training a concrete geometry to repair.

From word embeddings to sentence embeddings

Word embeddings gave individual tokens numerical coordinates. Retrieval has to compare variable-length queries and passages with one reusable vector. A sentence encoder performs that compression while trying to preserve the distinctions its task needs from nearby texts.

Averaging context-free word vectors is a useful baseline because it's fast and transparent. It also isolates two jobs that are easy to conflate: mean pooling collapses token states into one vector, while the training objective teaches that vector what relevance means.

Mean pooling inside a trained sentence encoder can work well. An untrained average has no reason to rank the rotation guide above a topic neighbor, so contrastive learning has to supply that ranking pressure.

Why averaging and raw [CLS] fail

Mean pooling of word embeddings

Before reaching for a larger encoder, predict what a commutative operation will do to "compiler calls linker" and "linker calls compiler." This tiny baseline uses a local vector table, so you can run it and see exactly what averaging throws away:

mean_pool.py
1word_vectors: dict[str, tuple[float, float, float]] = { 2 "compiler": (0.9, 0.1, 0.0), 3 "calls": (0.8, 0.2, 0.1), 4 "linker": (0.7, 0.1, 0.2), 5 "build": (0.1, 0.9, 0.2), 6 "failed": (0.2, 0.8, 0.1), 7} 8 9def mean_pool( 10 sentence: str, 11 vectors: dict[str, tuple[float, float, float]], 12) -> tuple[float, float, float]: 13 tokens = [token.lower() for token in sentence.split()] 14 token_vectors = [vectors[token] for token in tokens if token in vectors] 15 16 if not token_vectors: 17 width = len(next(iter(vectors.values()))) 18 return tuple(0.0 for _ in range(width)) 19 20 return tuple( 21 sum(vector[dim] for vector in token_vectors) / len(token_vectors) 22 for dim in range(len(token_vectors[0])) 23 ) 24 25pooled_a = mean_pool("compiler calls linker", word_vectors) 26pooled_b = mean_pool("linker calls compiler", word_vectors) 27 28print("A:", tuple(round(value, 3) for value in pooled_a)) 29print("B:", tuple(round(value, 3) for value in pooled_b)) 30print("Same vector:", pooled_a == pooled_b)
Output
1A: (0.8, 0.133, 0.1) 2B: (0.8, 0.133, 0.1) 3Same vector: True

The final line exposes the failure: both sentences produce the same vector because averaging ignores order.

Why can mean pooling make "compiler calls linker" and "linker calls compiler" identical?

Answer

Mean pooling adds the same token vectors and divides by the same count. Addition doesn't preserve order, so both sentences get the same average even though they reverse which build component calls the other.

What averaging throws away

Reversing two tokens is the clearest collision. The same operation also loses other signals a retrieval boundary may need:

  • Word order: "compiler calls linker" and "linker calls compiler" get the same vector because addition is commutative (A+B=B+AA+B = B+AA+B=B+A).
  • Common-word dilution: Frequent words and boilerplate can wash out rare, informative tokens.
  • No context: A static vector for "key" can't tell an API token from KMS key material.

[CLS] token from BERT

Taking the [CLS] (Classification) token from BERT (Bidirectional Encoder Representations from Transformers) and treating it as a sentence vector is a tempting shortcut. BERT's original pair architecture feeds both texts through one Transformer for a joint task. Its [CLS] state isn't trained by default so that independently encoded sentences can be ranked by cosine similarity.

That joint path also has the wrong serving shape for a large corpus. Reimers and Gurevych estimated about 50 million pair inferences, or roughly 65 hours on a V100, to find the nearest pair in 10,000 sentences. SBERT (Sentence-BERT) instead encodes each sentence once, then compares the cached vectors with cosine similarity.[1]Reference 1Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.https://arxiv.org/abs/1908.10084

The same paper measured how the shortcuts behave. Across seven semantic textual similarity (STS) tasks, raw [CLS] averaged 29.19 Spearman rank correlation with human ratings, while mean-pooling BERT token states averaged 54.81. Both trailed averaged GloVe (Global Vectors) embeddings. A pretrained task token is not automatically a nearest-neighbor relevance score.[1]Reference 1Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.https://arxiv.org/abs/1908.10084

The anisotropy problem

The score gap also points to a geometry problem. Contextual token representations aren't isotropic in every layer: their directions aren't spread evenly through the available space.[2]Reference 2How Contextual are Contextualized Word Representations? Comparing the Geometry of BERT, ELMo, and GPT-2 Embeddings.https://arxiv.org/abs/1909.00512 This anisotropy can leave an untuned sentence encoder with poorly discriminative neighborhoods.

Many API-doc passages can point toward one generic "technical docs" direction. Their cosine scores then look high even when they answer different questions. Contrastive objectives reshape that geometry by rewarding aligned positives while penalizing competing candidates.

Two-dimensional intuition for embedding anisotropy and contrastive geometry. Before training, API-key, KMS-key, and billing-doc vectors occupy a narrow 12-degree cone; an unrelated KMS-key vector only 6 degrees from an API-key query has cosine 0.995. After contrastive training, API-key positives remain 6 degrees apart with cosine 0.995, while the KMS-key cluster is 120 degrees from the API-key query with cosine negative 0.500. Billing docs form a third separated direction. The angles are an illustrative geometric example, not benchmark measurements.
Raw contextual vectors can bunch into one generic direction, while contrastive fine-tuning creates separable semantic neighborhoods that make nearest-neighbor search meaningful.

Contrastive learning for sentence embeddings

The core idea

Now turn the retrieval failure into a training target. For each query, name a passage that should be nearby and candidates that should not. Contrastive training reshapes the space so that ordering becomes part of the representation instead of a hope at serving time.

Cosine similarity measures the angle between two vectors and ignores their length. For two unit vectors, it's their dot product: +1 is the same direction, 0 is orthogonal, and a negative value is opposing.

Those values aren't a meaning score by themselves. Only an evaluated embedding model makes cosine ranking useful. The next lesson studies that scoring contract in detail.

SimCSE (Simple Contrastive Learning of Sentence Embeddings) showed how small the construction can be: pass the same sentence through the encoder twice, let dropout supply two noisy views, and treat those views as a positive pair.[3]Reference 3SimCSE: Simple Contrastive Learning of Sentence Embeddings.https://arxiv.org/abs/2104.08821 The model gets a positive without a second labeled sentence.

Wang and Isola give names for the geometry this objective should produce. Alignment asks whether positives are close. Uniformity asks whether normalized representations avoid crowding into a small region of the hypersphere.[4]Reference 4Understanding Contrastive Representation Learning through Alignment and Uniformity on the Hyperspherehttps://arxiv.org/abs/2005.10242

E5 later trained single-vector text embeddings contrastively from a large weakly supervised pair corpus.[5]Reference 5Text Embeddings by Weakly-Supervised Contrastive Pre-training.https://arxiv.org/abs/2212.03533 That same pattern, rather than a pooling rule alone, is what gives a vector space a retrieval contract.

The batch-ranking objective used below is InfoNCE: for one anchor, the true match should outrank every other candidate in the batch.

Contrastive ranking chart for one anchor query, how do I rotate an API key. The relevant passage, Replace old API token, ranks first with cosine similarity 0.91. The lexical hard negative, Rotate KMS key material, scores 0.42, giving a positive margin of 0.49 and strong downward training pressure. The unrelated easy negative, GPU driver matrix, scores 0.11, giving a margin of 0.80 and weak downward pressure. Training raises the positive score and focuses most negative pressure on the confusing hard negative.
Contrastive training turns a batch into a ranking problem: the true match should outrank every in-batch negative for the same anchor query.
Diagram showing Approved query-passage pairs, Shared encoder one vector per text, Similarity matrix positive on diagonal, and InfoNCE loss rank true passage first.
Approved query-passage pairs, Shared encoder one vector per text, Similarity matrix positive on diagonal, and InfoNCE loss rank true passage first.

InfoNCE loss

You want the matching API-doc passage close to the query and every non-match farther away. InfoNCE writes that ranking problem as classification: for this query, which passage in the batch is the right one? Before calculating, predict the answer for q1q_1q1​: p1p_1p1​ should win because 0.90 is above 0.20.

Walk through a batch of two query-passage pairs. After you normalize the embeddings, cosine similarities are just dot products:

QueryPositiveSimilarity
q1q_1q1​p1p_1p1​0.90
q1q_1q1​p2p_2p2​0.20
q2q_2q2​p1p_1p1​0.15
q2q_2q2​p2p_2p2​0.85

For query q1q_1q1​, the true match is p1p_1p1​ (similarity 0.90). The other passage in the batch, p2p_2p2​, acts as an in-batch negative (similarity 0.20). InfoNCE turns that ordering into probability: it wants the model to make p1p_1p1​ more likely than p2p_2p2​.

For this worked row, choose a sharp temperature τ=0.05\tau = 0.05τ=0.05 and compute the loss contribution for q1q_1q1​ step by step:

  1. Scale the similarities into logits: positive logit = 0.90/0.05=18.00.90 / 0.05 = 18.00.90/0.05=18.0, negative logit = 0.20/0.05=4.00.20 / 0.05 = 4.00.20/0.05=4.0
  2. Exponentiate (this turns logits into unnormalized probabilities): exp⁡(18.0)≈65,659,969\exp(18.0) \approx 65{,}659{,}969exp(18.0)≈65,659,969, exp⁡(4.0)≈54.6\exp(4.0) \approx 54.6exp(4.0)≈54.6
  3. Normalize with softmax into a probability for the positive: 65,659,969/(65,659,969+54.6)≈0.9999991765{,}659{,}969 / (65{,}659{,}969 + 54.6) \approx 0.9999991765,659,969/(65,659,969+54.6)≈0.99999917
  4. Take negative log: −log⁡(0.99999917)≈0.00000083-\log(0.99999917) \approx 0.00000083−log(0.99999917)≈0.00000083 (tiny loss; the model is already very confident)

If the model were wrong (q1q_1q1​ similarity to p1p_1p1​ only 0.20, to p2p_2p2​ 0.90), the positive probability would drop to about 8.3×10−78.3 \times 10^{-7}8.3×10−7 and the loss would jump to roughly 14 nats. The optimizer would receive a strong gradient pushing the correct pair closer.

Exact InfoNCE row calculation for a two-pair batch. The cosine similarity matrix is q1 to p1 0.90, q1 to p2 0.20, q2 to p1 0.15, and q2 to p2 0.85, with diagonal positives. For q1 at temperature 0.05, similarities 0.90 and 0.20 become logits 18 and 4. Subtracting the maximum gives stable logits 0 and negative 14; exponentials are 1 and about 8.3 times 10 to the negative 7; softmax probabilities are 0.99999917 and 0.00000083. Negative log positive probability is about 0.00000083. Swapping the scores makes the loss about 14.
InfoNCE reads each similarity row as a multiple-choice question where the diagonal passage is the correct answer and off-diagonal passages are in-batch negatives.

In the q1q_1q1​ example, why is the loss tiny when the positive similarity is 0.90 and the negative similarity is 0.20?

Answer

After temperature scaling, the positive logit is much larger than the negative logit. Softmax assigns almost all probability to the true match, so −log⁡(ppositive)-\log(p_\text{positive})−log(ppositive​) is near zero.

The standard contrastive loss for a batch of NNN positive pairs:[6]Reference 6Representation Learning with Contrastive Predictive Coding.https://arxiv.org/abs/1807.03748

L=−1N∑i=1Nlog⁡exp⁡(sim(zi,zi+)/τ)∑j=1Nexp⁡(sim(zi,zj+)/τ)\mathcal{L} = -\frac{1}{N} \sum_{i=1}^{N} \log \frac{\exp(\text{sim}(z_i, z_i^+) / \tau)}{\sum_{j=1}^{N} \exp(\text{sim}(z_i, z_j^+) / \tau)}L=−N1​∑i=1N​log∑j=1N​exp(sim(zi​,zj+​)/τ)exp(sim(zi​,zi+​)/τ)​

Reading the formula

For each example iii, the numerator gives the true match zi+z_i^+zi+​ its score. The denominator adds that positive to every candidate in the batch, turning the row into a multiple-choice question.

Temperature τ\tauτ controls how sharply those logits separate. The loss therefore asks for one concrete change: make the true pair more likely than every alternative in the batch.

Where:

  • zi,zi+z_i, z_i^+zi​,zi+​ are embeddings of a positive pair (e.g., query and relevant document)
  • τ\tauτ is the temperature parameter
  • All non-matching examples in that denominator act as in-batch negatives

This copy-runnable implementation keeps the same calculation visible instead of hiding the matrix math behind a framework. Production training code would vectorize this in PyTorch or another tensor library. The loop below makes the denominator explicit:

info_nce.py
1from math import exp, log, sqrt 2 3def normalize(vector: list[float]) -> list[float]: 4 norm = sqrt(sum(value * value for value in vector)) 5 return [value / norm for value in vector] 6 7def dot(left: list[float], right: list[float]) -> float: 8 return sum(a * b for a, b in zip(left, right)) 9 10def logsumexp(values: list[float]) -> float: 11 peak = max(values) 12 return peak + log(sum(exp(value - peak) for value in values)) 13 14def row_cross_entropy(logits: list[float], correct: int) -> float: 15 return logsumexp(logits) - logits[correct] 16 17def info_nce_loss( 18 query_vectors: list[list[float]], 19 positive_vectors: list[list[float]], 20 temperature: float = 0.2, 21) -> float: 22 queries = [normalize(vector) for vector in query_vectors] 23 positives = [normalize(vector) for vector in positive_vectors] 24 losses: list[float] = [] 25 26 for row, query in enumerate(queries): 27 logits = [dot(query, candidate) / temperature for candidate in positives] 28 losses.append(row_cross_entropy(logits, row)) 29 30 return sum(losses) / len(losses) 31 32query_vectors = [[1.0, 0.0], [0.0, 1.0]] 33positive_vectors = [[0.95, 0.05], [0.10, 0.90]] 34 35loss = info_nce_loss(query_vectors, positive_vectors) 36score_pos = dot(normalize(query_vectors[0]), normalize(positive_vectors[0])) 37score_neg = dot(normalize(query_vectors[0]), normalize(positive_vectors[1])) 38extreme_loss = row_cross_entropy([1000.0, 986.0], correct=0) 39 40print("loss:", round(loss, 4)) 41print("q1 positive score:", round(score_pos, 4)) 42print("q1 negative score:", round(score_neg, 4)) 43print("stable large-logit loss:", f"{extreme_loss:.8f}")
Output
1loss: 0.0104 2q1 positive score: 0.9986 3q1 negative score: 0.1104 4stable large-logit loss: 0.00000083

The equation is often expanded into raw exponentials when calculating a small example on paper. Code should compute the same expression with log-sum-exp or a framework cross-entropy operation, so large logits don't overflow.

Dual (symmetric) InfoNCE

The formula above is query → candidate only: each query row treats other positives in the batch as negatives. Many dual-encoder and multimodal recipes also run the reverse direction and average both:

Ldual=12(Lq→d+Ld→q)\mathcal{L}_{\text{dual}} = \frac{1}{2}\left(\mathcal{L}_{q \to d} + \mathcal{L}_{d \to q}\right)Ldual​=21​(Lq→d​+Ld→q​)

On the same N×NN \times NN×N similarity matrix Sij=sim(ziq,zjd)/τS_{ij} = \text{sim}(z_i^{q}, z_j^{d}) / \tauSij​=sim(ziq​,zjd​)/τ:

  • Row CE (Lq→d\mathcal{L}_{q \to d}Lq→d​): for each query iii, the correct column is iii (query ranks its matched document).
  • Column CE (Ld→q\mathcal{L}_{d \to q}Ld→q​): for each document jjj, the correct row is jjj (document ranks its matched query).

CLIP uses this symmetric contract for image↔text.[7]Reference 7Learning Transferable Visual Models From Natural Language Supervision.https://arxiv.org/abs/2103.00020 Keep the one-sided form when towers or serving are intentionally asymmetric: a query encoder that never appears as a retrieval key, or a setup that only ranks documents given queries. In that case, train the direction you serve and evaluate that direction with held-out retrieval alongside training loss.

Triplet loss

InfoNCE compares one anchor with a candidate pool. A second contrastive objective, triplet loss, focuses on one anchor-positive-negative triplet instead, making the required distance gap explicit:

L=max⁡(0,d(a,p)−d(a,n)+m)\mathcal{L} = \max(0, d(a, p) - d(a, n) + m)L=max(0,d(a,p)−d(a,n)+m)

Where:

  • d(⋅,⋅)d(\cdot, \cdot)d(⋅,⋅) is the Euclidean distance between embeddings
  • mmm is a margin hyperparameter
  • aaa is the anchor, ppp is the positive, nnn is the negative

The loss enforces that the anchor must be closer to the positive than to the negative by at least margin mmm: d(a,p)+m≤d(a,n)d(a, p) + m \leq d(a, n)d(a,p)+m≤d(a,n). If this constraint is already satisfied, the loss is zero. The margin prevents the model from wasting capacity pushing already-distant negatives even farther away.

Worked example: computing triplet loss by hand

Consider three sentences about API-key rotation. Before doing the arithmetic, predict the outcome: with the positive at distance 0.2 and the negative at 0.5, a margin of 0.1 should leave no work, while a larger margin could still leave a gap to close:

RoleSentence
Anchor (aaa)"How do I rotate an API key?"
Positive (ppp)"Where can I replace an old API token?"
Negative (nnn)"How do I rotate a KMS encryption key?"

After encoding, suppose the distances are:

  • d(a,p)=0.2d(a, p) = 0.2d(a,p)=0.2 (the paraphrase is close)
  • d(a,n)=0.5d(a, n) = 0.5d(a,n)=0.5 (the hard negative is farther, but not by much)

With margin m=0.1m = 0.1m=0.1, plug into the formula:

0.2−0.5+0.1=−0.20.2 - 0.5 + 0.1 = -0.20.2−0.5+0.1=−0.2

max⁡(0,−0.2)=0\max(0, -0.2) = 0max(0,−0.2)=0

The loss is zero because the model already satisfies the margin constraint: the positive is closer than the negative by more than 0.1. Now imagine a bad model where d(a,p)=0.5d(a, p) = 0.5d(a,p)=0.5 and d(a,n)=0.3d(a, n) = 0.3d(a,n)=0.3 (the negative is closer than the positive):

0.5−0.3+0.1=0.30.5 - 0.3 + 0.1 = 0.30.5−0.3+0.1=0.3

max⁡(0,0.3)=0.3\max(0, 0.3) = 0.3max(0,0.3)=0.3

A non-zero loss tells the optimizer to push the anchor and positive together while pushing the negative away until the gap exceeds the margin.

For triplet loss, what does zero loss mean?

Answer

It means the positive is already closer to the anchor than the negative by at least the margin. The model doesn't need to spend more gradient on that triplet.

Key differences from InfoNCE

  • Triplet loss compares a chosen negative against a margin, so mining determines most of its learning signal.
  • InfoNCE compares each anchor with a candidate pool. Batches supply negatives cheaply, but they can also contain false negatives.
  • Neither objective is an automatic win. Choose data construction deliberately and evaluate held-out retrieval failures, not training loss alone.

Temperature parameter τ

Temperature controls the "sharpness" of the softmax distribution over similarity scores. Hold the scores fixed and change only τ\tauτ to see what the optimizer is being asked to emphasize.

For the worked similarity gap, 0.90−0.20=0.700.90 - 0.20 = 0.700.90−0.20=0.70, changing temperature changes the positive probability:

τP(positive)P(\text{positive})P(positive) for this rowWhat to inspect
0.011.0000001.0000001.000000Saturates quickly; a false negative receives extreme pressure.
0.050.9999990.9999990.999999Very sharp separation for this easy row.
0.100.9990890.9990890.999089Still confident, with less sharpness.
1.000.6681880.6681880.668188Much flatter signal.

There's no universal best temperature. Tune it against held-out retrieval failures and implement the loss stably. Low temperature amplifies mislabeled or false negatives. Overflow is an implementation bug that stable log-softmax or cross-entropy avoids. E5's contrastive pre-training used τ=0.01\tau = 0.01τ=0.01 by default, even sharper than the 0.05 walkthrough above.[5]Reference 5Text Embeddings by Weakly-Supervised Contrastive Pre-training.https://arxiv.org/abs/2212.03533

Curve of positive probability versus temperature for a fixed similarity gap of 0.70, from positive 0.90 minus negative 0.20. Probability stays near 1 until temperature rises, then falls to 0.999089 at 0.10, 0.802183 at 0.50, and 0.668188 at 1.00. Low temperature saturates the easy row; high temperature flattens the softmax.
Temperature is a sharpness knob on a fixed 0.70 similarity gap. Low τ saturates an easy row; high τ flattens the softmax so the true match and the negative share more probability.
temperature.py
1from math import exp 2 3def probability_of_positive( 4 positive_similarity: float, 5 negative_similarity: float, 6 temperature: float, 7) -> float: 8 scaled_gap = (positive_similarity - negative_similarity) / temperature 9 return 1.0 / (1.0 + exp(-scaled_gap)) 10 11for temperature in (0.01, 0.05, 0.10, 1.00): 12 probability = probability_of_positive(0.90, 0.20, temperature) 13 print(f"tau={temperature:.2f}: P(positive)={probability:.6f}")
Output
1tau=0.01: P(positive)=1.000000 2tau=0.05: P(positive)=0.999999 3tau=0.10: P(positive)=0.999089 4tau=1.00: P(positive)=0.668188

What happens if temperature is too high in contrastive learning?

Answer

The softmax becomes too flat. True matches and negatives receive similar probabilities, so the model gets weak pressure to separate relevant passages from confusing non-matches.


Where the pairs come from

A contrastive objective has no way to decide what a pair means. You need texts that should match, texts that shouldn't, and enough variety that the model can't succeed by memorizing topic words. Supervised NLI pairs and SimCSE's dropout views make two useful constructions concrete.

Supervised: fine-tuning on NLI data

Natural Language Inference (NLI) labels whether a hypothesis follows from a premise (entailment), conflicts with it (contradiction), or does neither (neutral). Entailment is directional. It isn't a promise that two texts are interchangeable.

SBERT trained Siamese and triplet architectures with NLI supervision and evaluated sentence similarity.[1]Reference 1Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.https://arxiv.org/abs/1908.10084 Those labels give the loss a useful boundary. You still need retrieval evaluation before treating two technical passages as substitutes.

Supervised SimCSE uses entailment as a positive and the corresponding contradiction as a hard negative.[3]Reference 3SimCSE: Simple Contrastive Learning of Sentence Embeddings.https://arxiv.org/abs/2104.08821 That pairing makes the distinction explicit, but it still needs retrieval evaluation before two technical passages can be treated as substitutes.

The labels say which sentences should meet; SBERT's architecture makes that comparison cheap. It runs both sentences through the same encoder with shared weights, pools each into a single vector, then trains on those pooled embeddings.[1]Reference 1Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.https://arxiv.org/abs/1908.10084

At inference you keep only the single-sentence path. That shared-weight Siamese network makes precomputing document embeddings and nearest-neighbor search practical.

Self-supervised: SimCSE

No labeled NLI data? SimCSE changes where the positive comes from.[3]Reference 3SimCSE: Simple Contrastive Learning of Sentence Embeddings.https://arxiv.org/abs/2104.08821 Pass the same sentence through the encoder twice with different dropout masks. Dropout zeros different units each time, so you get two slightly different vectors for the same sentence.

Those views are positives; every other sentence in the batch is a negative. The construction turns an unlabeled batch into a ranking problem, with the same false-negative risk we will audit below.

On the paper's STS suite, unsupervised SimCSE with BERT-base reached 76.3 average Spearman correlation. The supervised NLI variant reached 81.6. Dropout, normally just regularization, becomes the data augmentation. Removing it, or reusing the same mask for both views, collapses the representation.[3]Reference 3SimCSE: Simple Contrastive Learning of Sentence Embeddings.https://arxiv.org/abs/2104.08821

Data augmentation

Once dropout makes a safe positive view, other augmentations become hypotheses about meaning preservation:

  • Dropout masks (SimCSE): different mask patterns per forward pass
  • Verified paraphrases or back-translations: use only after checking that technical scope and exceptions survive
  • Domain pairs: mine resolved duplicate questions that cite the same approved docs passage
  • Reranking with cross-encoders: score candidate paraphrases before accepting them as positives

Avoid casual word deletion or insertion for technical docs: dropping "not," a version condition, or an authorization requirement changes the rule while incorrectly labeling the pair positive.

In-batch negatives in practice

The batch can supply a negative pool without another labeling pass. For a batch of NNN positive pairs, each anchor has one matching positive, and the other N−1N-1N−1 candidates act as negatives. Dense Passage Retrieval trained dual encoders this way for open-domain QA.[8]Reference 8Dense Passage Retrieval for Open-Domain Question Answering.https://arxiv.org/abs/2004.04906

Larger batches increase the chance of informative competitors, but they also increase the chance of false negatives: another row may cite the same relevant docs passage while the loss treats it as wrong. Distributed training commonly gathers embeddings across GPUs before computing this loss. Plain gradient accumulation doesn't create more in-batch negatives unless the implementation explicitly reuses embeddings across microbatches.

audit_false_negatives.py
1batch = [ 2 {"query": "How do I rotate an API key?", "doc_id": "api-key-rotation-v3"}, 3 {"query": "How do I replace an old API token?", "doc_id": "api-key-rotation-v3"}, 4 {"query": "How do service account tokens expire?", "doc_id": "service-token-lifecycle-v2"}, 5] 6 7false_negatives = [] 8for anchor_index, anchor in enumerate(batch): 9 for candidate_index, candidate in enumerate(batch): 10 if anchor_index == candidate_index: 11 continue 12 if candidate["doc_id"] == anchor["doc_id"]: 13 false_negatives.append( 14 f"row {anchor_index} treats row {candidate_index} as negative" 15 ) 16 17print("false negatives:", false_negatives) 18print("action: deduplicate shared-doc positives before InfoNCE")
Output
1false negatives: ['row 0 treats row 1 as negative', 'row 1 treats row 0 as negative'] 2action: deduplicate shared-doc positives before InfoNCE

Hard negative mining

Why hard negatives matter

Suppose the model already rejects "GPU driver compatibility" for an API-key query. That example contributes little new signal. Hard negatives such as a KMS-key guide share the broad vocabulary but answer a different intent, forcing the encoder to learn the boundary that matters.

Negative typeAnchorCandidateWhy it matters
Easy negative"How do I rotate an API key?""GPU driver compatibility matrix"Different topic; the model learns this separation almost immediately
Hard negative"How do I rotate an API key?""How do I rotate a KMS encryption key?"Same keywords, different intent; forces fine-grained learning
Hard-negative mining audit for the query How do I rotate an API key. Four candidates are compared by known relevance label, lexical token overlap, deterministic cross-encoder score, and mining decision. The true matching API-token passage has overlap 3 and score 0.92 but is protected as a positive. The service-account-key passage has overlap 5 and score 0.76 and becomes hard negative rank 1. The KMS-key passage has overlap 2 and score 0.68 and becomes hard negative rank 2. The GPU-driver passage has zero overlap and score 0.05 and is dropped.
Useful mined negatives can share words with the anchor while answering a different intent. Explicit relevance labels keep true positives out of the negative set.

Mining strategies

Mining is how you keep that boundary in the training stream. Each source trades labeling cost, freshness, and the risk of promoting a true match to the negative set.

1. In-batch negatives

Use other examples in the batch. This is simple and scales with batch size, but it stops helping when rows are unrelated or accidentally share the same relevant document.

2. BM25 negatives

Use a lexical search algorithm like BM25 (Best Matching 25) to find documents that share words with the query but answer a different question:

text
1Query: "How do I rotate an API key?" 2Hard negative: "Rotate KMS encryption key material" # shares "rotate" and "key" but answers a different question 3Easy negative: "GPU driver compatibility matrix"

3. Cross-encoder-assisted mining

Start with lexical or dense retrieval, then keep known non-matches that still receive a high cross-encoder score. A label, document identity, or human review must establish that a candidate is wrong; a model score isn't ground truth by itself. The mining loop below makes that control flow deterministic: lexical overlap stands in for BM25, a small scorer stands in for the cross-encoder, and explicit relevance labels protect true positives.

mine_hard_negatives.py
1def tokens(text: str) -> set[str]: 2 return {part.strip("?.!,").lower() for part in text.split()} 3 4def lexical_overlap(query: str, candidate: str) -> int: 5 return len(tokens(query) & tokens(candidate)) 6 7def cross_encoder_score(query: str, candidate: str) -> float: 8 query_terms = tokens(query) 9 candidate_terms = tokens(candidate) 10 11 if "api" in query_terms and "api" in candidate_terms: 12 return 0.92 13 if "service" in candidate_terms and "key" in candidate_terms: 14 return 0.76 15 if "kms" in candidate_terms and "key" in candidate_terms: 16 return 0.68 17 return 0.05 18 19def mine_hard_negatives( 20 query: str, 21 corpus: list[dict[str, str | bool]], 22 top_k: int = 2, 23) -> list[str]: 24 known_non_matches = [ 25 row 26 for row in corpus 27 if not row["relevant"] 28 and lexical_overlap(query, str(row["text"])) > 0 29 ] 30 ranked = sorted( 31 known_non_matches, 32 key=lambda row: cross_encoder_score(query, str(row["text"])), 33 reverse=True, 34 ) 35 return [str(row["text"]) for row in ranked[:top_k]] 36 37query = "How do I rotate an API key?" 38corpus = [ 39 {"text": "Where can I replace an old API token?", "relevant": True}, 40 {"text": "Rotate KMS encryption key material", "relevant": False}, 41 {"text": "How do I rotate a service account signing key?", "relevant": False}, 42 {"text": "GPU driver compatibility matrix", "relevant": False}, 43] 44 45print(mine_hard_negatives(query, corpus))
Output
1['How do I rotate a service account signing key?', 'Rotate KMS encryption key material']

4. Iterative mining

Re-mine hard negatives periodically using the improved model. As broad errors disappear, the refreshed pool exposes finer confusions, but every round still needs false-negative checks.

Hard negatives in the InfoNCE denominator

Mining changes the loss only after those candidates appear in the softmax. Production trainers usually do one of two things:

  1. Expand the candidate pool: for each query, score positive + in-batch negatives + K mined hard negatives and run InfoNCE over that longer list.
  2. Pack hard negatives into the batch: attach mined passages as extra rows (or a fixed negative bank) so they land in other queries' denominators.

Either way, a mined hard negative is just another term in ∑jexp⁡(sim(zi,zj)/τ)\sum_j \exp(\text{sim}(z_i, z_j) / \tau)∑j​exp(sim(zi​,zj​)/τ). The toy below keeps the positive and one in-batch negative, then adds a mined hard negative so you can see the extra denominator mass raise the loss:

hard_negatives_in_denominator.py
1from math import exp, log 2 3def row_cross_entropy(logits: list[float], correct: int) -> float: 4 peak = max(logits) 5 log_z = peak + log(sum(exp(v - peak) for v in logits)) 6 return log_z - logits[correct] 7 8# Cosine similarities for one query at temperature 0.05 9temperature = 0.05 10positive = 0.90 11in_batch_negative = 0.20 12mined_hard_negative = 0.72 13 14easy_logits = [positive / temperature, in_batch_negative / temperature] 15hard_logits = easy_logits + [mined_hard_negative / temperature] 16 17print("loss without mined hard neg:", round(row_cross_entropy(easy_logits, 0), 6)) 18print("loss with mined hard neg:", round(row_cross_entropy(hard_logits, 0), 6))
Output
1loss without mined hard neg: 1e-06 2loss with mined hard neg: 0.026958

The positive score didn't change. The mined candidate (still wrong, but much closer) added competing probability mass, so the loss rises and gradients focus on that fine-grained confusion. Guard against false negatives when packing mined rows: a true relevant passage labeled as hard-negative teaches the model to push real matches away.


Bi-encoder vs cross-encoder

The loss trains a vector space. Serving still has to decide when the query and the document are allowed to interact. That placement decision, not the brand name of the encoder, sets latency and index size.

Three retrieval architectures compared by when query and document tokens interact. A bi-encoder encodes query tokens how, rotate, api, key and document tokens create, replace, key independently into one vector each, then scores with a single dot product. A cross-encoder concatenates the same tokens so attention mixes both texts and produces one score, with no reusable document vector. Late interaction stores one vector per document token, encodes the query once, and sums each query token maximum similarity over document tokens.
The architectures differ by when query and document tokens meet: never until a single vector score, inside full attention, or at token-level MaxSim after both sides are encoded independently.
ArchitectureWhat you storeQuery-time Transformer workInteractionTypical job
Bi-encoder1 vector per document1 query encode, then ANNq⋅dq \cdot dq⋅d or cosineFirst-stage corpus retrieval
Cross-encoderRaw documentsKKK joint passes for shortlist size KKKFull attention over [q;d][q; d][q;d]Rerank a bounded shortlist
Late interaction (ColBERT)1 vector per document token1 query encode, then token MaxSim∑imax⁡jqi⋅dj\sum_i \max_j q_i \cdot d_j∑i​maxj​qi​⋅dj​Token-level retrieval with a larger index

Bi-encoder (dual encoder)

A bi-encoder encodes the query and document independently, then compares them with dot product or cosine similarity. Documents can be pre-encoded and indexed. At query time you encode the query once, then use an approximate nearest neighbor (ANN) search index.

That speed comes from keeping the texts apart. Without cross-attention, the single-vector score can miss phrase order, negation, and fine token alignment.

Cross-encoder

A cross-encoder concatenates query and document and runs them through one Transformer. Full attention can model phrase-level interactions, and on a suitable shortlist that often improves precision.

The same interaction makes it expensive: inference runs for every (query, document) pair. Scoring a corpus of NNN documents therefore means O(N)O(N)O(N) Transformer passes per query, which is too slow for large-scale search.

Late interaction: ColBERT

ColBERT (Contextualized Late Interaction over BERT)[9]Reference 9ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT.https://arxiv.org/abs/2004.12832 uses late interaction as a middle ground between bi-encoder and cross-encoder:

Exact ColBERT MaxSim calculation for query tokens api and key against document tokens rotate, api, and key. The similarity rows are 0.12, 0.93, 0.08 and 0.19, 0.07, 0.91. MaxSim selects api-to-api 0.93 and key-to-key 0.91, then sums them to a final relevance score of 1.84.
ColBERT stores token vectors for documents, then scores a query by taking each query token's best document-token match and summing those MaxSim values.

Instead of a single embedding per document, ColBERT stores per-token embeddings and computes relevance using MaxSim: for each query token, find the maximum similarity to any document token, then sum.

Advantages

ColBERT retains token-level matching signals while documents can still be pre-encoded.

Disadvantages

The price is a much larger index: it stores one vector per token instead of one per document.

This is a budget tradeoff, not a universal ranking. A bi-encoder serves first-stage candidate generation at corpus scale and must be tuned for recall. A cross-encoder can improve precision for a bounded candidate set. Late interaction spends more index space to keep token-level evidence.

maxsim.py
1similarities = { 2 "api": {"rotate": 0.12, "api": 0.93, "key": 0.08}, 3 "key": {"rotate": 0.19, "api": 0.07, "key": 0.91}, 4} 5 6best_by_query_token = { 7 query_token: max(document_scores.values()) 8 for query_token, document_scores in similarities.items() 9} 10score = sum(best_by_query_token.values()) 11 12print("best token scores:", best_by_query_token) 13print("MaxSim score:", round(score, 2))
Output
1best token scores: {'api': 0.93, 'key': 0.91} 2MaxSim score: 1.84

A production reranking pattern

Two-stage ranking for the query How do I rotate an API key. Four documents are ranked by bi-encoder score: KMS key rotation 0.84, API key rotation 0.81, replace old API token 0.78, and GPU driver notes 0.10. Candidate k equals 3, so GPU driver notes is excluded. Cross-encoder reranking changes the shortlist order to API key rotation 0.96, replace old API token 0.91, and KMS key rotation 0.21. Top k equals 2, so the two API-key passages are returned.
The bi-encoder admits a lexical lookalike because it shares rotate and key. Cross-encoder attention is reserved for that shortlist, where it demotes the KMS passage and keeps the two API-key matches.

Serving stacks usually combine the two. A reranker spends the expensive cross-encoder pass only on candidates the bi-encoder admitted, so the first stage owns recall and the second stage improves precision. If the correct passage never enters the shortlist, reranking can't recover it.

The small function below makes that boundary visible with deterministic scores. The bi-encoder admits a KMS lookalike; cross-encoder attention then demotes it and puts the two true API-key passages on top.

rerank.py
1documents = [ 2 { 3 "id": "kms-key-rotation", 4 "text": "How do I rotate a KMS encryption key?", 5 "bi_score": 0.84, 6 "cross_score": 0.21, 7 }, 8 { 9 "id": "api-key-rotation", 10 "text": "API key rotation runbook", 11 "bi_score": 0.81, 12 "cross_score": 0.96, 13 }, 14 { 15 "id": "replace-old-api-token", 16 "text": "Create a replacement key, deploy it, then revoke the old key.", 17 "bi_score": 0.78, 18 "cross_score": 0.91, 19 }, 20 { 21 "id": "gpu-driver-notes", 22 "text": "GPU driver compatibility notes", 23 "bi_score": 0.10, 24 "cross_score": 0.05, 25 }, 26] 27 28def search_with_rerank( 29 query: str, 30 corpus: list[dict[str, str | float]], 31 candidate_k: int = 3, 32 top_k: int = 2, 33) -> list[str]: 34 candidates = sorted(corpus, key=lambda doc: float(doc["bi_score"]), reverse=True)[ 35 :candidate_k 36 ] 37 reranked = sorted( 38 candidates, 39 key=lambda doc: float(doc["cross_score"]), 40 reverse=True, 41 ) 42 return [str(doc["id"]) for doc in reranked[:top_k]] 43 44results = search_with_rerank("How do I rotate an API key?", documents) 45print(results)
Output
1['api-key-rotation', 'replace-old-api-token']

Instruction-tuned embeddings

The problem with task ambiguity

"Label" means different things depending on the job: clustering groups issue-label documents, retrieval finds axis-label troubleshooting, and classification asks whether text is about a bug, docs, or security. If an embedding call sees only the text, one model has to serve all three notions of similarity with the same signal.

Task-specific prefixes and instructions

Some embedding families expose task prefixes or lightweight instructions that steer the encoder toward retrieval, clustering, or classification. E5 is a simple example: it prepends query: and passage: during contrastive pre-training, and that split mattered for retrieval tasks where the corpus contains paraphrases of the query.[5]Reference 5Text Embeddings by Weakly-Supervised Contrastive Pre-training.https://arxiv.org/abs/2212.03533

INSTRUCTOR-style models go further and condition the embedding on an explicit task instruction concatenated with the text.[10]Reference 10One Embedder, Any Task: Instruction-Finetuned Text Embeddings.https://arxiv.org/abs/2212.09741 The format is model-specific. A prefix that helps one family can hurt another, so follow its training or model card.

This example keeps the families separate on purpose:

task_prefixes.py
1def format_e5_pair(query: str, passage: str) -> tuple[str, str]: 2 """E5 prepends role prefixes to query and passage strings.""" 3 return f"query: {query}", f"passage: {passage}" 4 5def format_instructor_input(instruction: str, text: str) -> str: 6 """INSTRUCTOR concatenates a task instruction with the text.""" 7 return f"{instruction} {text}" 8 9e5_query, e5_passage = format_e5_pair( 10 "how do I rotate an API key?", 11 "Create a replacement key, deploy it, then revoke the old key.", 12) 13 14instructor_example = format_instructor_input( 15 "Represent the developer question for retrieving approved API docs:", 16 "how do I rotate an API key?", 17) 18 19assert e5_query.startswith("query: ") 20assert e5_passage.startswith("passage: ") 21assert instructor_example.startswith("Represent") 22assert instructor_example.endswith("how do I rotate an API key?")

This doesn't mean one prefix solves every task. It means some embedding families expect an extra conditioning signal. Use the format documented for that specific model family, then benchmark it on your own retrieval, clustering, or classification workload.


Matryoshka representation learning (MRL)

The idea

Suppose the index budget says "keep the first 128 coordinates." An ordinary 768-dimensional embedding gives no reason to expect that prefix to preserve its nearest neighbors.

Matryoshka representation learning changes training so selected embedding prefix widths remain useful on their own. A full 768-dimensional embedding, for example, can be trained together with 128- and 32-dimensional prefixes. You then choose among trained and evaluated widths based on the storage-quality budget.

Train embeddings so that selected prefix widths preserve useful representations under their own losses:[11]Reference 11Matryoshka Representation Learning.https://arxiv.org/abs/2205.13147

Matryoshka prefix training shown with one 768-dimensional embedding divided into 24 blocks of 32 dimensions. Training losses are attached at prefixes 32, 128, and 768, whose relative float storage is 4.17 percent, 16.67 percent, and 100 percent. A separate exact runnable toy computes InfoNCE losses 0.0088 at dimension 2, 0.0147 at dimension 4, and 0.0225 at dimension 6, then averages them to 0.0153. The toy losses are objective terms, not retrieval-quality scores.
Matryoshka training applies contrastive losses at multiple prefix sizes so smaller dimensions remain usable instead of becoming arbitrary truncations.

For a contrastively trained embedding model, the loss can be computed at several predefined truncation points simultaneously. The runnable toy below slices full embeddings down to smaller prefixes, calculates the same InfoNCE objective at each prefix, and averages the losses. Toy dimensions keep the arithmetic small; extra training pressure keeps each selected prefix useful:

matryoshka_loss.py
1from math import exp, log, sqrt 2 3def normalize(vector: list[float]) -> list[float]: 4 norm = sqrt(sum(value * value for value in vector)) 5 return [value / norm for value in vector] 6 7def dot(left: list[float], right: list[float]) -> float: 8 return sum(a * b for a, b in zip(left, right)) 9 10def logsumexp(values: list[float]) -> float: 11 peak = max(values) 12 return peak + log(sum(exp(value - peak) for value in values)) 13 14def info_nce_loss( 15 query_vectors: list[list[float]], 16 positive_vectors: list[list[float]], 17 temperature: float = 0.2, 18) -> float: 19 queries = [normalize(vector) for vector in query_vectors] 20 positives = [normalize(vector) for vector in positive_vectors] 21 losses: list[float] = [] 22 23 for row, query in enumerate(queries): 24 logits = [dot(query, candidate) / temperature for candidate in positives] 25 losses.append(logsumexp(logits) - logits[row]) 26 27 return sum(losses) / len(losses) 28 29def matryoshka_loss( 30 embeddings_a: list[list[float]], 31 embeddings_b: list[list[float]], 32 dims: tuple[int, int, int] = (2, 4, 6), 33) -> float: 34 losses = [] 35 36 for dim in dims: 37 truncated_a = [row[:dim] for row in embeddings_a] 38 truncated_b = [row[:dim] for row in embeddings_b] 39 losses.append(info_nce_loss(truncated_a, truncated_b)) 40 41 return sum(losses) / len(losses) 42 43queries = [[1.0, 0.0, 0.9, 0.1, 0.5, 0.2], [0.0, 1.0, 0.1, 0.9, 0.2, 0.5]] 44positives = [[0.95, 0.05, 0.85, 0.15, 0.45, 0.25], [0.05, 0.95, 0.15, 0.85, 0.25, 0.45]] 45 46loss = matryoshka_loss(queries, positives) 47print(round(loss, 4))
Output
10.0153

Why it matters

BenefitWhy it matters
Flexible deploymentUse the full width when it wins your evaluation, or a smaller trained prefix when storage is constrained.
No retrainingOne model can serve several dimensionality budgets.
Graceful degradationPerformance should drop smoothly as dimensions shrink, but you still need to benchmark each cutoff.
Deployment constraintShorten only at dimensions a chosen model documents or you validate; arbitrary slicing isn't guaranteed to preserve rankings.

Evaluation: STS and MTEB

Semantic textual similarity (STS)

STS is a useful first check: do embedding similarities preserve a human notion of relatedness? Before broad benchmarks like MTEB existed, Semantic Textual Similarity (STS) was a common way to ask that question. Datasets like STS-B (STS Benchmark) provide sentence pairs rated by human annotators:

text
1"A CI build failed" / "A test run failed" => 4.5 2"A CI build failed" / "A password was reset" => 1.2

To evaluate a model, you compute the cosine similarity for every pair using the model's embeddings, and then calculate the Spearman rank correlation between the model's similarity scores and the human ratings. A high correlation means the model's embedding space aligns well with human judgment.

MTEB (Massive text embedding benchmark)

As models improved, optimizing only for STS stopped being enough. A model that's excellent at pairwise relatedness can still fail at retrieval or clustering. The Massive Text Embedding Benchmark (MTEB) was introduced to make those task differences visible.[12]Reference 12MTEB: Massive Text Embedding Benchmark.https://arxiv.org/abs/2210.07316

The original paper evaluated 33 models on 58 datasets grouped into 8 task categories and found that no one model dominated every category. Much of the retrieval slice comes from BEIR.[13]Reference 13BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models.https://arxiv.org/abs/2104.08663

The public leaderboard has grown since 2023, so treat the table below as the original snapshot, not today's catalog.[14]Reference 14MTEB Leaderboardhttps://huggingface.co/spaces/mteb/leaderboard

Task# DatasetsExample
Classification12Sentiment, topic
Clustering11Document clustering
Pair Classification3Paraphrase detection
Reranking4Passage reranking
Retrieval15Question-passage retrieval
STS10Semantic similarity
Summarization1Summary similarity
BitextMining2Parallel sentence mining
Sentence-embedding evaluation scope comparison. STS evaluates sentence pairs by comparing cosine-similarity rankings with human ratings from 0 to 5 using Spearman rank correlation. The original MTEB benchmark spans 58 datasets across eight categories: retrieval 15, classification 12, clustering 11, STS 10, reranking 4, pair classification 3, bitext mining 2, and summarization 1. Public benchmark breadth still needs workload-specific retrieval, language, latency, and storage validation.
STS checks one relationship: cosine ranking versus human pair ratings. Original MTEB added seven other task types. Use that breadth to compare models, then still measure your own API-doc retrieval, latency, and storage.

Choosing a model in practice

Use a leaderboard average to screen candidates, not to make the deployment decision. Start with the operational questions that define your own retrieval contract:

  • Does the model expect plain text, query/passage prefixes, or explicit instructions?
  • Can you shorten the embedding width safely, or are you locked into the full dimensionality?
  • How well does it handle your language mix, domain jargon, and query length distribution?
  • What are the latency, throughput, and memory costs once you batch and index it at production scale?
  • Do you still need BM25 or a cross-encoder reranker to hit Recall@K and NDCG (normalized discounted cumulative gain) targets?

Small leaderboard deltas can't compensate for a mismatched retrieval architecture, weak negatives, poor chunking, or a missing reranking stage. Evaluate the whole retrieval path instead of swapping model names in isolation. The original MTEB finding still sets the boundary: no one model dominated every task category.[12]Reference 12MTEB: Massive Text Embedding Benchmark.https://arxiv.org/abs/2210.07316 Check the current leaderboard task list, then measure API-doc retrieval, reranking, languages, latency, and storage on your own workload before treating an average as a purchase decision.[14]Reference 14MTEB Leaderboardhttps://huggingface.co/spaces/mteb/leaderboard

Carry the evidence boundary into retrieval evaluation

api-doc-retriever-v1 can use sentence embeddings to propose API-doc passages, but vector proximity isn't authorization. A release test should verify both retrieval quality and that unapproved text never becomes answer evidence:

docs_retrieval_release_gate.py
1approved_evidence = {"api-key-rotation-v3", "service-token-lifecycle-v2"} 2retrieval_cases = [ 3 { 4 "query": "How do I rotate an API key?", 5 "expected": "api-key-rotation-v3", 6 "candidates": ["private-incident-note-44", "api-key-rotation-v3"], 7 }, 8 { 9 "query": "How do service account tokens expire?", 10 "expected": "service-token-lifecycle-v2", 11 "candidates": ["service-token-lifecycle-v2", "draft-runbook-12"], 12 }, 13] 14attack_candidates = ["private-incident-note-44"] 15 16def approved_candidate(candidates: list[str]) -> str | None: 17 return next((doc for doc in candidates if doc in approved_evidence), None) 18 19served = [approved_candidate(case["candidates"]) for case in retrieval_cases] 20hits = sum( 21 evidence == case["expected"] 22 for evidence, case in zip(served, retrieval_cases) 23) 24attack_evidence = approved_candidate(attack_candidates) 25 26print("approved evidence recall@2:", f"{hits / len(retrieval_cases):.0%}") 27print("served evidence:", served) 28print("private-note attack evidence:", attack_evidence)
Output
1approved evidence recall@2: 100% 2served evidence: ['api-key-rotation-v3', 'service-token-lifecycle-v2'] 3private-note attack evidence: None

Key libraries and tools

Building embedding-based systems requires the right tooling:

ToolWhat it gives you
Sentence-Transformers (sentence-transformers)Pretrained sentence embedding models, pooling modules, and contrastive training losses. The library started as the SBERT code release.[1]Reference 1Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.https://arxiv.org/abs/1908.10084
FAISS (Facebook AI Similarity Search)Efficient similarity search and clustering for dense vectors, including inverted-file and product-quantization indexes.[15]Reference 15Billion-scale similarity search with GPUs.https://arxiv.org/abs/1702.08734

Mastery check

Key concepts

  • alignment and uniformity in embedding space
  • InfoNCE numerator, denominator, and temperature
  • hard negatives vs easy negatives
  • bi-encoder vs cross-encoder vs late interaction
  • reranking as recall first, then precision
  • Matryoshka prefix training for safe dimension cuts

Evaluation rubric

  • Foundational: Derives the InfoNCE objective and explains what the numerator, denominator, and temperature do.
  • Intermediate: Explains why raw BERT [CLS] embeddings fail for semantic search without sentence-level contrastive fine-tuning.
  • Intermediate: Explains why hard negatives matter more than random negatives once the model already separates broad topics.
  • Advanced: Compares bi-encoders, cross-encoders, and late-interaction models by latency, indexability, accuracy, and storage.
  • Advanced: Explains ColBERT's MaxSim scoring and why it keeps more token-level signal than a single document vector.
  • Advanced: Explains Matryoshka embeddings and when shorter prefixes are worth the storage-accuracy tradeoff.
  • Advanced: Designs a two-stage production retrieval pipeline with recall and latency budgets defended quantitatively.

Follow-up questions

Your cross-encoder gives excellent scores when you manually include the correct passage, but production search still misses that passage. Which stage is failing?

Answer

The first-stage retriever is failing on recall. If the correct passage never reaches the shortlist, reranking quality doesn't matter because the cross-encoder never sees the right candidate.

Why do hard negatives improve embedding quality more than random unrelated negatives?

Answer

Hard negatives share words or topic with the anchor while still answering a different intent. They force the model to learn fine semantic boundaries instead of only broad topic separation.

When can you safely shorten a sentence embedding from 768 dimensions to 128?

Answer

When the model was trained for prefix-aware truncation, such as Matryoshka-style training, or when the provider explicitly documents a shorter output mode. Otherwise you must benchmark because naive truncation can destroy retrieval quality.

When is a cross-encoder the right tool even if a bi-encoder already exists?

Answer

When final precision matters and the candidate set is already small. A cross-encoder is too slow for first-pass retrieval over a large corpus, but it's strong as a reranker after a bi-encoder narrows the search space.

Why does ColBERT usually recover more relevance detail than a standard bi-encoder, and what price does it pay?

Answer

ColBERT keeps token-level document vectors and scores each query token against the best matching document token, so it preserves more fine-grained matching. Documents store many token vectors instead of one vector, so the index becomes much larger.

Common pitfalls

Raw [CLS] is treated like a search-ready sentence embedding

  • Symptom: Nearly every query-document pair gets suspiciously similar cosine scores.
  • Cause: Raw BERT [CLS] vectors weren't tuned for semantic retrieval and can inherit poorly discriminative anisotropic geometry.
  • Fix: Start from a sentence embedding model or fine-tune with a contrastive objective before building nearest-neighbor search.

Negatives stay too easy

  • Symptom: Training loss falls, but recall on realistic queries barely moves.
  • Cause: Random negatives stop teaching once the model separates unrelated topics.
  • Fix: Mine BM25 or cross-encoder negatives that share words with the anchor but answer a different intent.

The reranker is asked to save missing recall

  • Symptom: The reranker looks good in pairwise inspection, yet the right document is often absent in production results.
  • Cause: The correct passage never entered the shortlist.
  • Fix: Tune first-stage Recall@K separately, then widen candidate budget before blaming the reranker.

Dimensions are shortened blindly

  • Symptom: Index storage drops as expected, but retrieval quality falls off a cliff.
  • Cause: A standard embedding vector was truncated without prefix-aware training or provider support.
  • Fix: Use Matryoshka-trained or provider-documented shortening controls and benchmark each target width.

Task conditioning is ignored

  • Symptom: One embedding model works for clustering but underperforms on retrieval.
  • Cause: The model family expected query/passage prefixes or instructions, but every input was embedded as plain text.
  • Fix: Follow the model card format for retrieval, clustering, and classification separately.

Try it yourself

These exercises let you verify your understanding without needing a GPU cluster.

Exercise 1: compute triplet loss by hand

Given an anchor aaa, positive ppp, and negative nnn with distances d(a,p)=0.3d(a,p) = 0.3d(a,p)=0.3 and d(a,n)=0.7d(a,n) = 0.7d(a,n)=0.7, compute the triplet loss for margins m=0.1m = 0.1m=0.1 and m=0.5m = 0.5m=0.5. In which case does the model still have work to do?

Solution sketch: For m=0.1m = 0.1m=0.1: 0.3−0.7+0.1=−0.30.3 - 0.7 + 0.1 = -0.30.3−0.7+0.1=−0.3, so max⁡(0,−0.3)=0\max(0, -0.3) = 0max(0,−0.3)=0. The margin is already satisfied. For m=0.5m = 0.5m=0.5: 0.3−0.7+0.5=0.10.3 - 0.7 + 0.5 = 0.10.3−0.7+0.5=0.1, so max⁡(0,0.1)=0.1\max(0, 0.1) = 0.1max(0,0.1)=0.1. The larger margin forces the model to pull the positive even closer or push the negative farther away.

Exercise 2: spot the embedding mistake

A teammate reports that their semantic search system returns nearly identical similarity scores for every query-document pair. They're using a pretrained BERT model and taking the [CLS] token as the sentence embedding. What's the most likely cause, and what's the smallest change that would fix it?

Solution sketch: Raw BERT [CLS] embeddings weren't trained to make cosine distance a semantic-retrieval score, and anisotropic geometry can make their scores poorly discriminative. The smallest fix is to switch to a sentence embedding model that was fine-tuned with a sentence-level objective (for example, SBERT or E5), rather than using raw BERT.

Exercise 3: design a two-stage retrieval pipeline

You have 2 million documentation chunks and a latency budget of 200 ms per query. You own a bi-encoder that encodes a query in 10 ms and a cross-encoder that scores one query-document pair in 15 ms. Why is scoring the full corpus with the cross-encoder impossible, and what pipeline would hit the latency budget?

Solution sketch: 2,000,000×15 ms=30,000,000 ms≈8.32{,}000{,}000 \times 15\,\text{ms} = 30{,}000{,}000\,\text{ms} \approx 8.32,000,000×15ms=30,000,000ms≈8.3 hours per query. The cross-encoder is far too slow for the full corpus. Reserve 10 ms for query encoding and choose a top-10 shortlist only if ANN lookup and overhead fit inside the remaining 40 ms: reranking then costs 10×15 ms=150 ms10 \times 15\,\text{ms} = 150\,\text{ms}10×15ms=150ms, for at most 200 ms total. If Recall@10 is inadequate, the budget or reranker throughput must change; silently widening to top 100 violates the requirement.


Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Two prototype encoders fail differently. Averaging context-free word vectors maps "compiler calls linker" and "linker calls compiler" to the same vector. Raw BERT [CLS] gives nearly identical cosine scores across passages. Which diagnosis and remedy are correct?

Correct answer: Word-vector averaging is order-invariant, while raw [CLS] lacks retrieval-tuned geometry; use a sentence encoder trained with a sentence-level contrastive objective.

Averaging the same context-free token vectors is commutative, so reversing semantic roles doesn't change the result. Raw BERT [CLS] has a different problem: it wasn't trained so nearest-neighbor cosine scores express sentence relevance and can inherit crowded, anisotropic geometry. A sentence encoder trained with a sentence-level contrastive objective addresses the geometry while contextual encoding can preserve order and context.

2.For q1, the positive similarity is 0.90, the only negative similarity is 0.20, and tau = 0.05. Why is the InfoNCE loss for this row nearly zero?

Correct answer: After temperature scaling, the positive logit is much larger than the negative logit. Softmax assigns almost all probability to the true match, so -log of that probability is near zero.

With tau = 0.05, the positive logit is 0.90 / 0.05 = 18 and the negative logit is 0.20 / 0.05 = 4. The softmax denominator includes both the positive and the negative, but exp(18) is vastly larger than exp(4), so the positive probability is about 0.999999. Taking the negative log of a probability that close to 1 gives a tiny loss.

3.Triplet loss is L = max(0, d(a,p) - d(a,n) + m). If d(a,p) = 0.3 and d(a,n) = 0.7, what are the losses for m = 0.1 and m = 0.5?

Correct answer: 0 and 0.1; only m = 0.5 still has work because the 0.4 distance gap is smaller than the required 0.5 margin.

For m = 0.1, the expression is 0.3 - 0.7 + 0.1 = -0.3, so the max with zero is 0. For m = 0.5, the expression is 0.3 - 0.7 + 0.5 = 0.1, so the loss is nonzero. A nonzero triplet loss means the model must increase the distance gap until it meets the margin.

4.A contrastive batch has two different queries that both cite api-key-rotation-v3 and a third query that cites service-token-lifecycle-v2. The implementation uses InfoNCE with in-batch negatives and plain gradient accumulation. What should you fix?

Correct answer: Rows citing the same document are false negatives; deduplicate or group them, and don't assume plain gradient accumulation adds in-batch negatives.

InfoNCE treats other positives in the batch as negatives for the current anchor. If two rows cite the same relevant document, the loss pressures the model to push apart texts that should be close. Lowering tau would amplify that bad pressure. Plain gradient accumulation doesn't create a larger negative pool unless embeddings are explicitly gathered or reused across microbatches.

5.You have unlabeled sentences and a transformer with dropout. Which setup matches self-supervised SimCSE and its intended effect on embedding geometry?

Correct answer: Encode each sentence twice with different dropout masks as its positive pair; use other batch sentences as negatives to improve alignment and reduce crowding.

Self-supervised SimCSE runs the same sentence through the encoder twice with different dropout masks, making two noisy views of the same meaning. Pulling those views together promotes alignment. Comparing each anchor against other batch sentences supplies separation pressure that helps normalized representations avoid crowding into one region, although batches still require false-negative auditing.

6.A model already separates "API key rotation" from "GPU driver compatibility," but it confuses "How do I rotate an API key?" with "How do I rotate a KMS encryption key?" Which negative should you add, and why?

Correct answer: Add "How do I rotate a KMS encryption key?" because it shares rotate and key vocabulary while answering a different intent, forcing a finer semantic boundary.

Hard negatives are useful because they look tempting to the retriever without being correct. A random GPU-driver document is already easy to reject, so it contributes little new signal. The KMS-key candidate shares surface words with the anchor but asks a different question, which forces the encoder to separate close intents. A true paraphrase or same-document row shouldn't be mislabeled as a negative because that creates a false negative.

7.You have 2 million documentation chunks. Query encoding with a bi-encoder takes 10 ms, and a cross-encoder scores one query-document pair in 15 ms. With a 200 ms per-query budget, which pipeline is feasible?

Correct answer: Use the bi-encoder and ANN search to shortlist 10 candidates, then rerank those 10 with the cross-encoder if ANN overhead fits; query encoding plus reranking costs about 160 ms.

Scoring the full corpus with the cross-encoder would require 2,000,000 x 15 ms = 30,000,000 ms, or about 8.3 hours per query. A bi-encoder can pre-encode documents and use ANN search for first-stage recall. Reranking 10 shortlisted candidates costs 10 x 15 ms = 150 ms, and adding the 10 ms query encoding leaves about 40 ms for ANN lookup and overhead within a 200 ms budget.

8.In a ColBERT-style late-interaction scorer, the query has tokens "api" and "key." The best document-token similarities are 0.93 for "api" and 0.91 for "key." What score is produced, and what deployment tradeoff comes with this representation?

Correct answer: 1.84, found by summing per-query-token MaxSim values; the index is larger because documents store token vectors instead of one vector.

ColBERT computes a MaxSim value for each query token by finding that token's best match among document token vectors, then sums those values. Here the score is 0.93 + 0.91 = 1.84. This preserves more token-level relevance detail than a single document vector, but it increases index size because each document stores many token embeddings.

9.An E5-style model was embedded as plain text and selected only by a high MTEB average. The app also lets the nearest vector become answer evidence, even if it's a private incident note. What should change?

Correct answer: Use documented E5 query: and passage: prefixes, benchmark API-doc retrieval and latency, and keep approval gates before answer evidence.

Some embedding families expect task-specific formatting, and E5 distinguishes query and passage inputs during contrastive pretraining. A single MTEB average isn't enough to choose a production model because no model dominates every task and deployment depends on retrieval quality, latency, language mix, and storage. Vector proximity also isn't authorization; approved evidence gates must still prevent private notes from becoming answer evidence.

10.You have a 768-dimensional sentence embedding model and want to index only the first 128 coordinates. Which condition makes this truncation a defensible deployment choice?

Correct answer: The model was trained for prefix-aware truncation, such as Matryoshka-style training, or the provider explicitly documents a shorter output mode. Otherwise you must benchmark because naive truncation can destroy retrieval quality.

A normal embedding vector doesn't guarantee that the first 128 coordinates form a useful smaller embedding. Matryoshka training applies losses at selected prefix widths so those prefixes remain meaningful. Provider-documented shortening can also be valid, but each cutoff still needs evaluation against storage and retrieval quality targets. Strong benchmark scores, cosine normalization, or ANN compression don't by themselves make arbitrary slicing safe.

10 questions remaining.

Next Step
Continue to Embedding Similarity & Quantization

InfoNCE, hard negatives, and encoder architecture shape reusable sentence vectors. Next you'll keep those vectors fixed and study the scoring contract (cosine versus dot product) plus the compression tricks that decide whether the right passage still wins after you shrink the index.

PreviousCapstone: Reproducible ML Study
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks.

Reimers, N., & Gurevych, I. · 2019 · EMNLP 2019

https://arxiv.org/abs/1908.10084

How Contextual are Contextualized Word Representations? Comparing the Geometry of BERT, ELMo, and GPT-2 Embeddings.

Ethayarajh, K. · 2019

https://arxiv.org/abs/1909.00512

SimCSE: Simple Contrastive Learning of Sentence Embeddings.

Gao, T., Yao, X., & Chen, D. · 2021 · EMNLP 2021

https://arxiv.org/abs/2104.08821

Understanding Contrastive Representation Learning through Alignment and Uniformity on the Hypersphere

Wang, T., & Isola, P. · 2020 · ICML 2020

https://arxiv.org/abs/2005.10242

Text Embeddings by Weakly-Supervised Contrastive Pre-training.

Wang, L., et al. · 2022

https://arxiv.org/abs/2212.03533

Representation Learning with Contrastive Predictive Coding.

Oord, A. van den, Li, Y., & Vinyals, O. · 2018

https://arxiv.org/abs/1807.03748

Learning Transferable Visual Models From Natural Language Supervision.

Radford, A., et al. · 2021 · ICML 2021

https://arxiv.org/abs/2103.00020

Dense Passage Retrieval for Open-Domain Question Answering.

Karpukhin, V., et al. · 2020 · EMNLP 2020

https://arxiv.org/abs/2004.04906

ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT.

Khattab, O., & Zaharia, M. · 2020 · SIGIR 2020

https://arxiv.org/abs/2004.12832

One Embedder, Any Task: Instruction-Finetuned Text Embeddings.

Su, H., et al. · 2022 · arXiv preprint

https://arxiv.org/abs/2212.09741

Matryoshka Representation Learning.

Kusupati, A., et al. · 2022 · NeurIPS 2022

https://arxiv.org/abs/2205.13147

MTEB: Massive Text Embedding Benchmark.

Muennighoff, N., et al. · 2023 · EACL 2023

https://arxiv.org/abs/2210.07316

BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models.

Thakur, N., et al. · 2021 · NeurIPS 2021 Datasets and Benchmarks

https://arxiv.org/abs/2104.08663

MTEB Leaderboard

Hugging Face / MTEB contributors · 2026

https://huggingface.co/spaces/mteb/leaderboard

Billion-scale similarity search with GPUs.

Johnson, J., Douze, M., & Jégou, H. · 2017 · arXiv preprint

https://arxiv.org/abs/1702.08734

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.