Dense Retrieval and Bi-Encoders
Dense Retrieval is a family of neural search methods that encode queries and documents into dense vector representations and retrieve relevant content via approximate nearest-neighbor search in embedding space, replacing or augmenting sparse keyword-based methods like BM25. It matters enormously in modern AI because it powers the retrieval backbone of Retrieval-Augmented Generation (RAG) systems, enabling language models to access external knowledge at inference time without retraining. The counterintuitive fact: a bi-encoder trained on only tens of thousands of question-answer pairs can outperform BM25 on out-of-vocabulary queries that share zero lexical overlap with the answer document.
The fundamental insight is that semantic similarity lives in geometry: if you train two encoders to map queries and their relevant documents to nearby points in a shared high-dimensional vector space, retrieval becomes a pure nearest-neighbor lookup that is both differentiable and scalable. Unlike cross-encoders (which jointly encode query+document and cannot precompute document representations), bi-encoders encode each side independently, so you can index millions of documents offline and answer queries in milliseconds. The "aha moment" is realizing that the entire retrieval problem collapses to a single dot product or cosine similarity computation at inference time, making dense retrieval simultaneously more semantically powerful and computationally cheaper than it looks.
How It Works
Encoding and Similarity
A bi-encoder consists of two encoder functions — one for queries, one for documents — that are often weight-shared (same BERT backbone) or sometimes separate networks. Given a query and a document , the model produces:
Explanation: Each encoder independently maps its input to a fixed-length dense vector of hidden dimension (typically 768 for BERT-base), with no cross-attention between query and document tokens. The relevance score is then a simple inner product (or cosine similarity):
Explanation: This single dot product is all that happens at retrieval time — the entire semantic matching problem is compressed into one multiply-accumulate operation per candidate document.
Training with In-Batch Negatives
The dominant training objective is the InfoNCE / in-batch negative contrastive loss. Given a batch of (query, positive document) pairs , all other documents in the batch act as negatives:
Explanation: For each query , we want the score with its positive to be the maximum across all documents in the batch; is a temperature hyperparameter (often 1.0 or learned) that sharpens or softens the distribution. where is temperature, is the dot-product score from Eq. [2], and the denominator sums over all documents including the positive. With large batch sizes (e.g., 128–2048), this gives free negatives per positive, making training extremely efficient.
Hard Negative Mining
Standard in-batch negatives are often too easy (random documents are obviously irrelevant). Hard negative mining selects negatives that are semantically close but not relevant:
Explanation: The denominator now includes hard negatives retrieved by BM25 or a previous dense model — documents that look relevant lexically or semantically but are not the true answer, forcing the encoder to learn finer semantic distinctions.
Approximate Nearest Neighbor Retrieval
At inference, given a query embedding , we find:
Explanation: This is an exhaustive maximum inner product search (MIPS) over the full document corpus , which in practice is approximated using FAISS HNSW or IVF indexes, reducing search from to with small recall trade-offs.
PyTorch Implementation Sketch
```python import torch import torch.nn as nn import torch.nn.functional as F from transformers import AutoModel, AutoTokenizer class BiEncoder(nn.Module): def __init__(self, model_name: str = "bert-base-uncased"): super().__init__()
# Weight-shared encoder for query and document self.encoder = AutoModel.from_pretrained(model_name) def encode(self, input_ids, attention_mask):
# Pool [CLS] token representation out = self.encoder(input_ids=input_ids, attention_mask=attention_mask) return out.last_hidden_state[:, 0, :] # shape: (B, H) def forward(self, q_ids, q_mask, d_ids, d_mask): q_emb = self.encode(q_ids, q_mask) # (B, H) d_emb = self.encode(d_ids, d_mask) # (B, H) return q_emb, d_emb
def contrastive_loss(q_emb, d_emb, temperature=1.0): """In-batch negative InfoNCE loss."""
# Normalize for cosine similarity variant q_emb = F.normalize(q_emb, dim=-1) d_emb = F.normalize(d_emb, dim=-1)
# Score matrix: (B, B) — diagonal is positive pairs scores = torch.matmul(q_emb, d_emb.T) / temperature # (B, B)
# Labels: each query matches doc at same index labels = torch.arange(scores.size(0), device=scores.device)
# Cross-entropy over rows and columns (symmetric loss) loss_q = F.cross_entropy(scores, labels) loss_d = F.cross_entropy(scores.T, labels) return (loss_q + loss_d) / 2
# --- Indexing and retrieval with FAISS --- import faiss import numpy as np def build_faiss_index(doc_embeddings: np.ndarray, use_gpu=False): """Build an IVF-Flat FAISS index for MIPS.""" dim = doc_embeddings.shape[1] nlist = min(4096, len(doc_embeddings) // 39) quantizer = faiss.IndexFlatIP(dim) # Inner product index = faiss.IndexIVFFlat(quantizer, dim, nlist, faiss.METRIC_INNER_PRODUCT) if use_gpu: res = faiss.StandardGpuResources() index = faiss.index_cpu_to_gpu(res, 0, index)
# Normalize if using cosine similarity faiss.normalize_L2(doc_embeddings) index.train(doc_embeddings) index.add(doc_embeddings) return index def retrieve(query_emb: np.ndarray, index, top_k=100): faiss.normalize_L2(query_emb) scores, doc_ids = index.search(query_emb, top_k) # (1, k), (1, k) return doc_ids[0], scores[0] ```
Karpukhin is a research scientist at Meta AI whose 2020 paper "Dense Passage Retrieval for Open-Domain Question Answering" (with Oguz, Min, Lewis, Wu, Edunov, Chen, and Yih) established the modern bi-encoder paradigm for open-domain QA and introduced the in-batch negative training recipe that became the community standard. The paper demonstrated for the first time that a BERT-based bi-encoder trained on Natural Questions could outperform BM25 on retrieval recall despite using zero lexical matching. Surprisingly, the DPR codebase was released with only about 1,000 lines of core Python, yet it became the reference implementation for nearly every dense retrieval system built in the following three years.
Reimers, then a PhD student at TU Darmstadt (now at Cohere), published "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks" in 2019, which introduced the siamese bi-encoder architecture with mean-pooling and showed it produced sentence embeddings 27× faster than cross-encoder inference while remaining competitive on semantic textual similarity benchmarks. His `sentence-transformers` Python library democratized dense retrieval by giving practitioners a one-line API to encode millions of sentences, and it is now downloaded over 10 million times per month. An often-overlooked fact: the original paper's key motivation was practical — computing similarity for all sentence pairs in SNLI's 570k examples would take 65 hours with BERT cross-encoders but only 5 seconds with SBERT.
Intuition
Imagine a vast library where every book has been summarized into a single GPS coordinate on a map of ideas. When you walk into the library with a question, a librarian gives you your own GPS coordinate based on the meaning of your question — not its exact words. Finding relevant books then means finding the GPS coordinates closest to yours, which a computer can do in milliseconds even across millions of books. The key insight is that "close in space = similar in meaning," and this geography of meaning is learned, not hand-crafted. During training, we repeatedly tell the system "these two things are related — pull them together in space" and "these two things are unrelated — push them apart." Over enough examples, the space organizes itself so that a question about "cardiac arrest symptoms" lands near "heart attack warning signs" even though those phrases share almost no words. The bi-encoder's magic trick is that it processes the library and the question completely separately — the library is mapped to coordinates in advance (offline), and only the question is processed at query time. This is like a librarian who has already memorized the location of every book; they just need a few seconds to understand your question, then can instantly point you to the right shelf.