CloudCodeTree LogoCloudCodeTree
AI NewsTutorialsAbout
CloudCodeTree Logo
CloudCodeTree
  • AI News
  • Tutorials
  • About
← Back to AI News
Dense Embeddings Flatten a Document to One Vector: ColBERT Keeps Every Token — RAGatouille Makes It Three Lines

Dense Embeddings Flatten a Document to One Vector: ColBERT Keeps Every Token — RAGatouille Makes It Three Lines

Chris Harper

2 min read

Aug 4, 2026 · 04:05 UTC

AI
Tutorial
RAG
Best Practices

ColBERT keeps a separate embedding per token instead of one per document — dense embeddings lose exact-match signals ColBERT catches; RAGatouille makes it three lines.

What you'll be able to do after this:

  • Build a ColBERT index and search it in under 20 lines of Python with RAGatouille
  • Explain why single-vector (biencoder) embeddings lose per-token precision and when that costs recall
  • Use ColBERT as a drop-in reranker on top of any retriever you already have

Why dense-only fails on precision queries

A biencoder embedding model flattens every document to a single 768-d vector via mean pooling. Two documents mentioning "HNSW" and "pgvector" score almost identically against a query for either — the averaging hides token-level specificity. ColBERT's late interaction keeps one embedding per token for every document. At query time it computes MaxSim: for each query token, find its highest-scoring match across all document tokens, then sum those per-token scores. The "HNSW" token in a query gets a strong match against the document that uses it, not the one that doesn't.

Walk-through with RAGatouille

pip install ragatouille
from ragatouille import RAGPretrainedModel

# Step 1: Index (downloads ColBERTv2 weights ~500MB on first run)
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

documents = [
    "pgvector stores vector embeddings natively inside PostgreSQL.",
    "HNSW is a graph-based approximate nearest-neighbor index algorithm.",
    "ColBERT keeps a separate embedding per token for late interaction scoring.",
]

RAG.index(
    index_name="my-docs",
    collection=documents,
    document_ids=["doc-0", "doc-1", "doc-2"],
)

# Step 2: Search
results = RAG.search("how does HNSW differ from flat vector search?", k=3)
for r in results:
    print(r["rank"], round(r["score"], 2), r["content"][:70])

Use it as a reranker (no index needed)

If you already have a retriever, ColBERT can re-score its top-N candidates on the fly without a pre-built index:

# Pass any candidate strings from your existing retriever
reranked = RAG.rerank(
    query="pgvector vs HNSW: which index is faster?",
    documents=initial_candidates,  # list[str] from your dense retriever
    k=5,
)

This is the lowest-friction entry point: keep your existing embedding pipeline, add .rerank() as a second-stage pass to improve precision at virtually no infrastructure cost.

When to reach for ColBERT: high-precision technical docs, code search, legal text, or any domain where exact terminology matters. For general semantic similarity, dense embeddings remain fast and sufficient.

Sources: RAGatouille — GitHub · Late Interaction Overview — Weaviate · Supercharge Your RAG with Late Interactions — YouTube