TAI BUI
Back to curriculum
LearnPython

Embedding Models — The 2026 Deep Dive

Word2Vec gave you a vector per word. Modern embedding models give you a vector per passage, cross-lingual, with sparse, dense, and multi-vector views, sized to fit your index. Pick wrong and your RAG retrieves the wrong thing.

TIME: ~60 minutesPREREQUISITES: Phase 5 · 03 (Word2Vec), Phase 5 · 14 (Information Retrieval)

Learning Objectives

  • Distinguish dense, sparse, and multi-vector (late-interaction) embeddings, and explain when each wins in a retrieval stack.
  • Explain how BGE-M3 produces dense, sparse, and multi-vector representations from a single model and fuses their scores.
  • Apply Matryoshka Representation Learning to truncate a trained embedding to fewer dimensions, including why you must re-normalize after truncating.
  • Diagnose the production pitfalls that quietly break retrieval quality: missing query prefixes, asymmetric query/doc encoding, silent context truncation, and picking a model on MTEB score alone.

The Problem

Your RAG system retrieves the wrong passage 40% of the time. The culprit is rarely the vector database or the prompt. It is the embedding model.

Choosing an embedding in 2026 means picking across five axes:

  1. Dense vs sparse vs multi-vector. One vector per passage, or one per token, or a sparse weighted bag of words.
  2. Language coverage. Monolingual English models still win on English-only tasks. Multilingual models win when corpora are mixed.
  3. Context length. 512 tokens vs 8,192 vs 32,768. Real effective capacity is often only 60-70% of the advertised max.
  4. Dimension budget. 3,072 floats at full precision = 12 KB per vector. At 100M vectors, storage is $1,300/month. Matryoshka truncation cuts this 4×.
  5. Open vs hosted. Open-weight means you control the stack and data. Hosted means you trade control for always-latest.

This lesson names the tradeoffs so you can pick on evidence, not on whatever was popular last quarter.

Warm-up0/3
1. Two vectors both have length 1. What does their dot product equal?Warm-up
2. A matryoshka (Russian nesting doll) set is doll-within-doll-within-doll. If you pull out just the smallest doll and set the rest aside, what do you have?Warm-up
3. A search box must find pages containing an exact error code such as E4012. Which classic approach matches on the literal words?Warm-up
Check: The Problem0/2
1. Choosing an embedding model in 2026 means picking across which five axes?Define
2. The lesson says 3,072 float32 values take 12 KB per vector. How many bytes does one vector take after Matryoshka truncation to 768 dims, still stored as float32?Apply

The Concept

Dense, sparse, and multi-vector embeddings

Dense embeddings. One vector per passage (usually 384-3,072 dimensions). Cosine similarity ranks passages by semantic proximity. OpenAI text-embedding-3-large, BGE-M3 dense mode, Voyage-3. Default choice.

Sparse embeddings. SPLADE-style. A transformer predicts a weight for every vocab token, then zeros out most of them. Result is a sparse vector of size |vocab|. Captures lexical matching (like BM25) but with learned term weights. Strong on keyword-heavy queries.

Multi-vector (late interaction). ColBERTv2, Jina-ColBERT. One vector per token. Scoring with MaxSim: for each query token, find the most similar document token, sum the scores. More expensive to store and score, but wins on long queries and domain-specific corpora.

BGE-M3: all three at once. Single model outputs dense, sparse, and multi-vector representations simultaneously. Each can be queried independently; scores fuse via weighted sum. The 2026 default when you want flexibility from one checkpoint.

Matryoshka Representation Learning. Trained so the first N dimensions of the vector form a useful standalone embedding. Truncate a 1,536-dim vector to 256 dim and pay ~1% accuracy for 6× storage savings. Supported by OpenAI text-3, Cohere v4, Voyage-4, Jina v5, Gemini Embedding 2, Nomic v1.5+.

The MTEB leaderboard tells a partial story

Massive Text Embedding Benchmark: 56 tasks across 8 task types at launch (2022), expanded to 100+ tasks in MTEB v2. In early 2026, Gemini Embedding 2 tops retrieval (67.71 MTEB-R). Cohere embed-v4 leads general (65.2 MTEB). BGE-M3 leads open-weight multilingual (63.0). The leaderboard is necessary but not sufficient. Always benchmark on your domain.

The three-tier pattern

Use casePattern
Fast first-passDense bi-encoder (BGE-M3, text-3-small)
Recall boostSparse (SPLADE, BGE-M3 sparse) + RRF fuse
Precision on top-50Multi-vector (ColBERTv2) or cross-encoder reranker

Most production stacks use all three.

Check: The Concept0/10
1. What is a dense embedding?Define
2. How does a SPLADE-style sparse embedding model work?Define
3. How do multi-vector (ColBERT-style) embeddings score a query-document pair?Define
4. What does BGE-M3 output from a single model?Define
5. What is a Matryoshka embedding?Define
6. What is MTEB?Define
7. What is BEIR?Define
8. On which workload does multi-vector scoring most clearly beat a single dense vector, despite higher storage and scoring cost?Explain
9. How does BGE-M3 combine its dense, sparse, and multi-vector scores into one ranking?Explain
10. You are designing a production retrieval stack and want fast first-pass recall, a boost for keyword-heavy queries, and high precision on the final results, without over-building. Which pattern fits?Design

Build It

Step 1: baseline dense embeddings with Sentence-BERT

python
from sentence_transformers import SentenceTransformer
import numpy as np
 
encoder = SentenceTransformer("BAAI/bge-small-en-v1.5")
corpus = [
    "The first iPhone launched in 2007.",
    "Apple released the iPod in 2001.",
    "Android is an operating system from Google.",
]
emb = encoder.encode(corpus, normalize_embeddings=True)
 
query = "When was the iPhone released?"
q_emb = encoder.encode([query], normalize_embeddings=True)[0]
scores = emb @ q_emb
print(sorted(enumerate(scores), key=lambda x: -x[1]))

normalize_embeddings=True makes the dot product equal cosine similarity. Always set it.

Step 2: Matryoshka truncation

python
def truncate(vectors, dim):
    out = vectors[:, :dim]
    return out / np.linalg.norm(out, axis=1, keepdims=True)
 
emb_256 = truncate(emb, 256)
emb_128 = truncate(emb, 128)

Re-normalize after truncation. Nomic v1.5, OpenAI text-3, and Voyage-4 are trained so this is lossless for the first few levels. Non-Matryoshka models (original Sentence-BERT) degrade sharply when truncated.

Step 3: BGE-M3 multi-functionality

python
from FlagEmbedding import BGEM3FlagModel
 
model = BGEM3FlagModel("BAAI/bge-m3", use_fp16=True)
 
output = model.encode(
    corpus,
    return_dense=True,
    return_sparse=True,
    return_colbert_vecs=True,
)
# output["dense_vecs"]:    (n_docs, 1024)
# output["lexical_weights"]: list of dict {token_id: weight}
# output["colbert_vecs"]:  list of (n_tokens, 1024) arrays

Three indexes, one inference call. Score fusion:

python
dense_score = ... # cosine over dense_vecs
sparse_score = model.compute_lexical_matching_score(q_lex, d_lex)
colbert_score = model.colbert_score(q_col, d_col)
final = 0.4 * dense_score + 0.2 * sparse_score + 0.4 * colbert_score

Tune the weights on your domain.

Step 4: MTEB eval on a custom task

python
from mteb import MTEB
 
tasks = ["ArguAna", "SciFact", "NFCorpus"]
evaluation = MTEB(tasks=tasks)
results = evaluation.run(encoder, output_folder="./mteb-results")

Run your candidate models on a representative subset. Do not trust leaderboard rank alone. Your domain matters.

Step 5: hand-rolled cosine from scratch

See code/main.py. Averaged Hashing Trick embeddings (stdlib-only). Not competitive with transformer embeddings, but shows the shape: tokenize → vector → normalize → dot product.

Check: Build It0/6
1. Why must you re-normalize a Matryoshka-truncated vector before cosine similarity?Explain
2. Why does truncating original Sentence-BERT embeddings hurt far more than truncating Nomic v1.5 embeddings?Explain
3. `emb` has shape (3, 384). What does `truncate(emb, 128)` return?Apply
4. With the three-sentence corpus from the BGE-M3 example, what shape is `output["dense_vecs"]`?Apply
5. Using weights 0.4, 0.2, 0.4 for dense, sparse, and colbert, what is the final score when the three scores are 0.8, 0.5, and 0.6?Apply
6. A row of `emb` begins [3, 4, 12] (ignore its other values). After `truncate(emb, 2)`, what are its two kept values?Apply

Pitfalls

  • Same model for query and doc. Some models (Voyage, Jina-ColBERT) use asymmetric encoding: query and document pass through different paths. Always check the model card.
  • Missing prefix. bge-* models need "Represent this sentence for searching relevant passages: " prepended to queries. 3-5 point recall gap if you forget.
  • Over-trimming Matryoshka. 1,536 → 256 is usually safe. 1,536 → 64 is not. Validate on your eval set.
  • Context truncation. Most models silently truncate inputs over their max length. Long docs need chunking (see lesson 23).
  • Ignoring latency tail. MTEB scores hide p99 latency. A 600M model might beat a 335M model by 2 points but cost 3× more per query.
Check: Pitfalls0/6
1. What is asymmetric encoding in an embedding model?Define
2. Why is picking an embedding model on MTEB score alone risky?Explain
3. Your BGE-based retrieval sits 3-5 points below published benchmark numbers, and you add no special text before embedding queries. What is the most likely cause?Analyze
4. Documents averaging 6,000 tokens are embedded with a 512-token model. Recall is poor on facts late in each document, though nothing errors. What explains this?Analyze
5. A challenger embedding model tops the MTEB leaderboard by 2 points over your incumbent, but scores worse on your own held-out domain eval. What should you conclude?Analyze
6. You cut 1,536-dim Matryoshka vectors to 64 dims to save storage and recall drops noticeably, while 256 dims was fine. What went wrong?Analyze

Use It

The 2026 stack:

SituationPick
English-only, fast, APItext-embedding-3-large or voyage-3-large
Open-weight, EnglishBAAI/bge-large-en-v1.5
Open-weight, multilingualBAAI/bge-m3 or Qwen3-Embedding-8B
Long context (32k+)Voyage-3-large, Cohere embed-v4, Qwen3-Embedding-8B
CPU-only deploymentNomic Embed v2 (137M params, MoE)
Storage-constrainedMatryoshka-truncated + int8 quantization
Keyword-heavy queriesAdd SPLADE sparse, RRF-fuse with dense

2026 pattern: start with BGE-M3 or text-3-large, evaluate on your domain with MTEB, swap if a domain-specific model wins by more than 3 points.

Check: Use It0/4
1. Your incumbent model scores 62.0 on your domain eval and a challenger scores 64.5. Following the 2026 pattern, what do you do?Apply
2. Your dense-only retrieval keeps missing queries that contain exact product SKUs and error codes, even though semantically similar queries retrieve fine. What is the fix?Design
3. You must store 100M vectors from an English model and storage cost is the main constraint. Which combination fits?Design
4. You must deploy embeddings on CPU-only hardware with no GPU. Which pick fits best?Design

Ship It

Save as outputs/skill-embedding-picker.md:

markdown
---
name: embedding-picker
description: Pick embedding model, dimension, and retrieval mode for a given corpus and deployment.
version: 1.0.0
phase: 5
lesson: 22
tags: [nlp, embeddings, retrieval]
---
 
Given a corpus (size, languages, domain, avg length), deployment target (cloud / edge / on-prem), latency budget, and storage budget, output:
 
1. Model. Named checkpoint or API. One-sentence reason.
2. Dimension. Full / Matryoshka-truncated / int8-quantized. Reason tied to storage budget.
3. Mode. Dense / sparse / multi-vector / hybrid. Reason.
4. Query prefix / template if required by the model card.
5. Evaluation plan. MTEB tasks relevant to domain + held-out domain eval with nDCG@10.
 
Refuse recommendations that truncate Matryoshka to <64 dims without domain validation. Refuse ColBERTv2 for corpora under 10k passages (overhead not justified). Flag long-document corpora (>8k tokens) routed to models with 512-token windows.
Check: Ship It0/1
1. A corpus has 5,000 passages and a teammate proposes ColBERTv2. What does the embedding-picker skill do?Apply
Author's Note

The mistake I see most often is treating MTEB rank as the purchase decision instead of the starting point. A 2-point jump on the leaderboard rarely survives contact with your actual corpus, and every model swap costs you a full re-embed, a new index, and a round of eval you didn't budget for. I default to BGE-M3 or text-3-large on day one, run MTEB plus a held-out slice of my own domain, and only migrate when a challenger clears the incumbent by more than noise, usually 3+ points, not 0.5. Sparse and multi-vector modes are worth adding once dense retrieval plateaus, not before. They're overhead you don't need until you've actually hit dense's ceiling.

Exercises

  1. Easy. Encode 100 sentences with bge-small-en-v1.5 at full dim (384), then at Matryoshka 128. Measure MRR drop on 10 queries.
  2. Medium. Compare BGE-M3 dense, sparse, and colbert on 500 passages from your domain. Which wins on recall@10? Does RRF fusion beat the best single mode?
  3. Hard. Run MTEB on three candidate models across your top-2 domain tasks. Report MTEB score, p99 latency on a 100-query batch, and $/1M queries. Pick the Pareto-optimal one.

Key Terms

TermWhat people sayWhat it actually means
Dense embeddingThe vectorOne fixed-size vector per text. Cosine similarity for ranking.
Sparse embeddingLearned BM25One weight per vocab token; mostly zeros; trained end-to-end.
Multi-vectorColBERT-styleOne vector per token; MaxSim scoring; bigger index, better recall.
MatryoshkaRussian doll trickFirst N dims are a valid smaller embedding on their own.
MTEBThe benchmarkMassive Text Embedding Benchmark: 56 tasks at launch, 100+ in v2.
BEIRThe retrieval benchmark18 zero-shot retrieval tasks; often cited for cross-domain robustness.
Asymmetric encodingQuery ≠ doc pathModel uses different projections for queries and documents.

Further Reading

Code

Ships With