Dense retrieval turns queries and documents into embeddings and compares their meaning—not just their exact words. A bi-encoder embeds the query and each document separately so document vectors can be precomputed and indexed. That is the standard setup for fast semantic search in RAG.
Keyword search is a checklist: "Did these words appear?" Dense search is a meaning map: "Are these ideas close?"
Example: the query "How much vacation do I get?" may fail to find "Employees accrue 18 days of paid leave annually" with keyword search—the words do not match. Dense retrieval can still connect them.
| Aspect | Sparse (keyword) retrieval | Dense retrieval |
|---|---|---|
| Core idea | Match exact words | Match semantic meaning |
| Good for | Names, codes, IDs, rare terms | Synonyms, paraphrases, related ideas |
| Weakness | Misses meaning when wording changes | Needs a good embedding model |
In a one-hot vector, every word is its own dimension. "Cat" and "kitten" are unrelated dimensions, so their dot product is zero even though the meaning is close.
Dense vectors spread meaning across dimensions. Similar sentences land near each other in vector space.
Modern systems embed whole sentences or paragraphs—not just single words.
BERT-style idea: the [CLS] token (a special token at the start) often acts as a summary of the whole input.
Cosine similarity compares the angle between two vectors:
cosine(a, b) = (a · b) / (||a|| × ||b||)
Closer to 1 means more similar meaning. Near 0 means unrelated.
BERT is trained to understand text—not to rank documents for search. Better approach: train the encoder so relevant query–document pairs are close and irrelevant pairs are far apart.
| Training pair | Plain-English idea |
|---|---|
| Positive pair | A query and a document that should match |
| Negative pair | A query and a document that should not match |
| In-batch negatives | Other docs in the same training batch act as easy negatives |
| Hard negatives | Docs that look relevant but are wrong—teach a stronger lesson |
| Model type | How it works | Speed | Accuracy |
|---|---|---|---|
| Bi-encoder | Encode query and document separately; compare embeddings | Fast | Good for first-stage retrieval |
| Cross-encoder | Encode query and document together; score relevance directly | Slow | Usually more accurate |
Use bi-encoders for wide first-stage search. Use cross-encoders later to rerank a small candidate set (covered in lesson 2.7 on reranking).
Key lesson: measure candidates on your own data, not only a public leaderboard.
| Model (examples) | Plain-English note |
|---|---|
all-MiniLM-L6-v2 |
Small, fast baseline for prototypes |
bge-base-en-v1.5 |
Strong open English retrieval |
e5-base-v2 |
Good asymmetric setup (different query vs passage format) |
text-embedding-3-small |
Hosted API; easy start, no GPU |
Matryoshka representation learning (MRL): some models train embeddings so the first part of the vector already carries useful meaning. You can truncate dimensions to save storage—only when the model was trained for this.
| Situation | Lean toward |
|---|---|
| General English, zero infrastructure | Hosted APIs |
| Sensitive or on-prem data | Open-weight models (BGE, multilingual-e5) |
| Multilingual text | Multilingual embedders |
| Code, logs, exact IDs | Code embedders or hybrid search with BM25 |
| Tiny corpus that fits in context | Maybe skip RAG entirely |
Cosine similarity and the bi-encoder mental model.
import numpy as np
def cosine_sim(a, b):
a = np.array(a, dtype=float)
b = np.array(b, dtype=float)
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-9))
# Toy: query and two doc vectors (pretend from a bi-encoder)
query_vec = np.array([0.9, 0.1, 0.0])
doc_a = np.array([0.85, 0.15, 0.0]) # relevant
doc_b = np.array([0.0, 0.1, 0.95]) # unrelated
print("doc_a", cosine_sim(query_vec, doc_a))
print("doc_b", cosine_sim(query_vec, doc_b))
In production, document vectors are computed offline and stored in the index; only the query is embedded at search time.
Bi-encoders embed queries and documents separately so meaning-based search can run fast at scale, after training (or choosing a model) so relevant pairs land close in vector space.