Paper: "ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction" (Khattab & Matni, 2020) ArXiv: https://arxiv.org/abs/2004.12832 Key Insight: Traditional dense retrieval (one vector per document) loses information. ColBERT computes vectors per token, then does late interaction (at retrieval time). Fast and accurate.
The Problem: Dense vs. Sparse Retrieval
Sparse (BM25):
- Fast: O(1) with index
- Accurate: Matches exact terms
- Problem: Fails on synonyms, paraphrases
Dense (Vector Search):
- Accurate: Semantic matching
- Problem: Slow (HNSW/IVF), expensive reranking
ColBERT: Late Interaction
Key idea: Don't compress to one vector. Keep per-token vectors, compute similarity at retrieval time.
Indexing:
- Tokenize passage: [CLS, term1, term2, ..., SEP]
- Encode each token with BERT
- Store all token embeddings (not just [CLS])
- Build index on token embeddings
Retrieval:
- Tokenize query: [CLS, q_term1, q_term2, ..., SEP]
- Encode each query token with BERT
- For each passage in index:
- Compute max similarity between each query token and passage tokens
- Sum max similarities
- Rank passages by score
Implementation
import torch
import torch.nn as nn from transformers import AutoTokenizer, AutoModel
class ColBERT: def __init__(self, model_name='bert-base-uncased'): self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.encoder = AutoModel.from_pretrained(model_name) self.encoder.eval()
def encode_passage(self, passage): """Encode passage to per-token embeddings""" tokens = self.tokenizer(passage, return_tensors='pt', truncation=True, max_length=512)
with torch.no_grad(): output = self.encoder(**tokens) embeddings = output.last_hidden_state # (1, seq_len, 768)
return embeddings[0] # (seq_len, 768)
def encode_query(self, query): """Encode query to per-token embeddings""" tokens = self.tokenizer(query, return_tensors='pt')
with torch.no_grad(): output = self.encoder(**tokens) embeddings = output.last_hidden_state # (1, seq_len, 768)
return embeddings[0] # (seq_len, 768)
def score(self, query_emb, passage_emb): """ Compute MaxSim score query_emb: (q_len, 768) passage_emb: (p_len, 768)
Returns: scalar score """ # Compute all pairwise similarities similarities = torch.matmul(query_emb, passage_emb.t()) # (q_len, p_len)
# MaxSim: for each query token, take max similarity to passage tokens max_sims = similarities.max(dim=1)[0] # (q_len,)
# Score = sum of max similarities score = max_sims.sum().item() return score
def retrieve(self, query, passages, k=10): """Retrieve top-k passages""" query_emb = self.encode_query(query)
scores = [] for passage in passages: passage_emb = self.encode_passage(passage) score = self.score(query_emb, passage_emb) scores.append((score, passage))
# Sort by score scores.sort(reverse=True) return [p for _, p in scores[:k]]
# Usage colbert = ColBERT() passages = [ "Python is a programming language", "Machine learning uses algorithms to learn from data", "Neural networks are inspired by biology" ]
query = "What is machine learning?" results = colbert.retrieve(query, passages, k=2) for passage in results: print(passage)
Why Late Interaction?
Early Interaction (Dense):
- Compress passage → single vector
- Loss of information (which tokens match?)
- Fast retrieval but low recall
Late Interaction (ColBERT):
- Keep all token vectors
- Preserve information
- Flexible matching at retrieval time
- Accuracy of dense + interpretability of sparse
Benchmarks
Retrieval: MS MARCO (1M passages)
BM25 (baseline):
- Recall@10: 69.6%
- Latency: <1ms per query
Dense Retrieval (ANCE):
- Recall@10: 75.0%
- Latency: 5-10ms per query (with reranking)
ColBERT:
- Recall@10: 75.8%
- Latency: 10-50ms per query (no reranking needed)
ColBERT beats both accuracy and efficiency!
Our Analysis: Why This Matters
ColBERT is a Pareto improvement: it's faster than dense retrieval and more accurate than sparse. The key insight—late interaction—is general and applies to other domains (image, video). This paper also influenced more recent work like SPLADE and HyDE.
References
- Paper: ColBERT (Khattab & Matni, 2020)
- Code: https://github.com/stanford-futuredata/ColBERT
- Benchmarks: MS MARCO leaderboard
Conclusion
ColBERT shows that keeping detailed information (per-token embeddings) enables better retrieval than compression. Late interaction is a general pattern: preserve information for as long as possible, then combine at the end. This principle applies beyond search to many ranking and matching problems. Next: more advanced papers on model scaling and emergence.
