teachyou.ai academy
← All posts
RAGColBERTvector searchembeddingsretrieval engineering

Multi-Vector Retrieval: ColBERT and Beyond

Pramod Dutta · Jun 21, 2026 · 16 min read

AUTHOR: Pramod Dutta

Multi-vector RAG represents each chunk as many token-level vectors instead of one pooled embedding, then scores a query against a document by matching individual query tokens to individual document tokens. The technique, popularized by ColBERT, closes a real gap in standard RAG pipelines: single-vector search often misses documents that match on specific terms or phrases because the pooling step that produces "one embedding per chunk" throws away exactly the information that made the match relevant. If your RAG system struggles with rare terms, acronyms, product codes, or multi-hop questions where only part of a chunk is relevant, multi-vector retrieval is the fix worth understanding before you reach for a bigger reranker or a more expensive LLM call.

This article covers why single-vector embeddings break down, how ColBERT's late interaction mechanism works, how to build a working multi-vector index with RAGatouille, how to run multi-vector search natively in Qdrant and Weaviate, what ColPali and its successors do for visual documents, and when multi-vector retrieval is worth the extra storage and compute.

Why Single-Vector Embeddings Break Down

A standard dense retriever takes a chunk of text, runs it through an embedding model, and pools the output into a single fixed-size vector, usually by mean-pooling or taking a [CLS] token. That single vector has to represent everything in the chunk: every entity, every claim, every keyword. When a query matches one narrow part of a long chunk, the pooled vector can end up far from the query vector in embedding space, because the rest of the chunk's content dilutes the signal.

This shows up in practice as a few recurring failure modes:

  • Rare or exact-match terms get washed out. A chunk about "error code E4521 causes a firmware rollback loop" pools the specific error code together with generic surrounding language. A query for E4521 alone competes with thousands of other error-code mentions in embedding space where the exact string barely matters.
  • Long chunks lose specificity. The bigger your chunk size, the more topics get compressed into one vector, and the worse single-vector search gets at surfacing chunks that are relevant for only one sentence out of twenty.
  • Lexical and semantic mismatch compound. Standard dense embeddings are already weak on exact lexical matches compared to BM25. Pooling makes this worse because it averages away the token identities that a lexical matcher would catch directly.

The common workaround is hybrid search: run BM25 and dense retrieval in parallel, then fuse the results with something like reciprocal rank fusion. Hybrid search helps, but it is still two independent single-vector-style signals bolted together after the fact. Multi-vector retrieval attacks the root problem instead: it never collapses the chunk into one vector in the first place.

ColBERT: Late Interaction Explained

ColBERT (Contextualized Late Interaction over BERT) keeps a vector for every token in both the query and the document, instead of pooling them into one. At query time, it computes similarity between every query token vector and every document token vector, keeps the maximum similarity for each query token (this is the "MaxSim" operation), and sums those maximums across all query tokens to get the final relevance score.

The core scoring function, in plain terms:

score(Q, D) = sum over each query token q in Q of:
    max over each document token d in D of:
        cosine_similarity(q, d)

Because the comparison happens after both the query and document have already been encoded independently, this is called "late interaction": the expensive cross-attention that a reranker would normally do at query time never happens. Instead, document token embeddings are precomputed and indexed once, and query time only needs to encode the (short) query and run the MaxSim comparison against precomputed document vectors.

This gives ColBERT a middle position between two extremes:

  • Bi-encoders (standard dense retrieval): encode query and document independently, compare with a single dot product. Fast, cheap to index, but loses fine-grained token interactions.
  • Cross-encoders (rerankers): feed the query and document together through a transformer, letting every token attend to every other token. Very accurate, but you cannot precompute anything, so it only works on a small candidate set at query time.

Late interaction gets most of the accuracy benefit of token-level matching while keeping document representations precomputable, because MaxSim is a much cheaper operation than full cross-attention and can be executed against an index rather than requiring a fresh forward pass per candidate.

ColBERTv2 added a training and compression pipeline (residual compression of token vectors, denoised supervision from a cross-encoder teacher) that made the index size and query latency practical enough for production use, and PLAID is the indexing engine that makes ColBERTv2 search fast by clustering token vectors and pruning most of the index before running MaxSim on the surviving candidates.

Setting Up Multi-Vector RAG with RAGatouille

RAGatouille wraps ColBERT (and its training/inference machinery) behind a much simpler API, so you do not need to hand-roll PLAID indexing or manage checkpoint loading yourself.

Install it:

pip install ragatouille

Build an index from raw text chunks. Note that ColBERT-style indexing works directly on full documents or chunks, since the token-level representation already gives you fine-grained matching without needing carefully tuned chunk boundaries:

from ragatouille import RAGPretrainedModel

RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")

documents = [
    "The E4521 error indicates a firmware rollback loop triggered by "
    "a failed OTA update signature check on the gateway module.",
    "Gateway modules ship with secure boot enabled by default; disabling "
    "it voids the hardware warranty and is not supported in production.",
    "Firmware update failures are usually caused by an interrupted "
    "flash write, a corrupted image, or a signature mismatch.",
]

index_path = RAG.index(
    index_name="firmware-docs",
    collection=documents,
    document_ids=["doc-1", "doc-2", "doc-3"],
    max_document_length=256,
    split_documents=True,
)

Querying returns ranked results with the MaxSim score already computed against the indexed token vectors:

results = RAG.search(query="what causes error E4521", k=3)

for r in results:
    print(r["score"], r["content"][:80])

For an existing index, load it back without re-embedding anything:

RAG = RAGPretrainedModel.from_index("./.ragatouille/colbert/indexes/firmware-docs")
results = RAG.search(query="signature check failure on gateway", k=5)

RAGatouille also exposes a RAG.rerank() method that runs the same late-interaction scoring on a candidate list you already retrieved some other way (say, from your existing pgvector or Pinecone index). This is a practical middle path: keep your existing single-vector first-stage retrieval, and slot ColBERT in as a more accurate second-stage reranker without rebuilding your whole pipeline:

reranked = RAG.rerank(
    query="what causes error E4521",
    documents=candidate_chunks,
    k=10,
)

Indexing and Querying with PLAID

If you outgrow RAGatouille's abstraction or need to run ColBERT at a scale where index build time and query latency matter, it is worth understanding what PLAID is actually doing under the hood, because it explains the storage and latency numbers you will see in practice.

PLAID indexes work in three steps:

  1. Clustering: all document token vectors in the collection are clustered with k-means into a set of centroids. Every token vector is then represented as its nearest centroid ID plus a compressed residual (the difference between the token vector and its centroid), which is where most of the storage savings come from compared to storing full-precision token vectors.
  2. Candidate generation: at query time, each query token vector is compared only against centroids, not against every document token, to find a shortlist of documents whose tokens live near the query token in embedding space. This pruning step is what keeps query latency close to standard dense search instead of scanning every token in the collection.
  3. Scoring: the shortlisted documents get their full (decompressed) token vectors reloaded, and MaxSim is computed exactly against that smaller candidate set.

The practical implication: multi-vector indexes are bigger than single-vector indexes (you are storing one vector per token, not one per chunk), but well-implemented compression and clustering keep query latency in a usable range for interactive RAG. If you are evaluating whether to run this yourself, budget for higher index storage and plan to benchmark query latency on your own hardware and document lengths rather than assuming numbers from someone else's benchmark will transfer.

Multi-Vector Search in Production Vector Databases

You do not need to run ColBERT's own indexing stack to get multi-vector retrieval into production. Several vector databases now support multi-vector fields natively, which means you can store a list of token vectors per document and configure MaxSim as the comparator, using the same infrastructure you already use for single-vector search.

Qdrant supports multi-vectors natively with a MaxSim comparator, so you can store ColBERT-style token embeddings as a named vector field:

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_collection(
    collection_name="firmware_docs",
    vectors_config={
        "colbert": models.VectorParams(
            size=128,
            distance=models.Distance.COSINE,
            multivector_config=models.MultiVectorConfig(
                comparator=models.MultiVectorComparator.MAX_SIM
            ),
        )
    },
)

client.upsert(
    collection_name="firmware_docs",
    points=[
        models.PointStruct(
            id=1,
            vector={"colbert": token_vectors_for_doc},  # list of per-token vectors
            payload={"content": documents[0]},
        )
    ],
)

A common production pattern with Qdrant is to combine a cheap single-vector field for first-stage candidate retrieval with a multi-vector field used only for reranking the top candidates, since running MaxSim against the full collection is more expensive than a single dense dot product. Weaviate and Vespa support similar patterns: Vespa in particular has long supported multi-vector and tensor-based late interaction as a first-class ranking feature, and is a solid choice if you are already comfortable with its ranking expression language.

The general architecture that works well in practice:

  1. First-stage retrieval with a fast single-vector index (or BM25, or both fused) to get a candidate set of, say, 100-500 chunks.
  2. Second-stage reranking with multi-vector MaxSim scoring on just that candidate set, either through RAGatouille's rerank(), a native multi-vector field in your vector database, or a hosted late-interaction reranking API.
  3. Pass the top-k reranked chunks to your LLM as before.

This keeps first-stage retrieval cheap and lets multi-vector scoring do what it is best at: fixing precision on a candidate set that is already in the right neighborhood.

Beyond ColBERT: ColPali and Visual Document Retrieval

ColBERT's late interaction idea generalizes past text. ColPali applies the same MaxSim scoring to documents rendered as images, using a vision-language model to produce a token-level (patch-level) embedding for each image patch of a page, and a text-token embedding for each query token. This means you can retrieve directly against PDF pages, slide decks, or scanned forms as rendered images, without running OCR or a document-parsing pipeline first, and still get token-level (in this case, patch-level) matching between the query and the visual layout of the page, including tables, charts, and figures that OCR-based text extraction typically mangles or drops.

The practical appeal is skipping the fragile part of most RAG pipelines: PDF parsing and layout extraction. A ColPali-style index treats each page as an image, embeds it with a vision-language backbone, and scores queries against patch embeddings the same way ColBERT scores against token embeddings. Follow-up work such as ColQwen builds on the same late-interaction pattern with newer vision-language backbones, and the same "index visually, skip OCR" idea has spread to several open-source visual retrieval libraries. If a meaningful share of your source documents are PDFs with dense tables, charts, or non-standard layouts where text extraction quality is inconsistent, a ColPali-style visual multi-vector index is worth a serious look before you invest more time tuning a text-extraction pipeline.

Multi-Vector as a Reranking Stage vs. a Primary Index

There are two distinct ways to bring multi-vector retrieval into a RAG system, and picking the right one depends on your latency budget and how much you trust your first-stage retriever.

As a primary index, every chunk is stored as its full set of token vectors, and search runs MaxSim against the whole collection (accelerated by PLAID-style clustering, or a native multi-vector database). This gives you the best possible recall on the first pass, because nothing gets filtered out before the fine-grained scoring runs. It also costs the most in storage and index build time, and is the right choice when retrieval quality is the bottleneck in your pipeline and you can afford the infrastructure.

As a reranking stage, your existing single-vector (or hybrid BM25 + dense) retriever pulls a first-stage candidate set, and multi-vector MaxSim scoring only runs on those candidates. This is dramatically cheaper, because MaxSim only has to compare a few hundred document token-sets instead of the full collection, and it recovers most of the accuracy benefit as long as your first-stage retriever's recall is good enough to put the right chunk somewhere in the candidate set. The risk is that if your first-stage retriever misses a relevant chunk entirely, no amount of reranking recovers it.

Most teams should start with multi-vector as a reranking stage. It is a smaller infrastructure change, it composes with whatever retrieval setup you already have, and it directly targets the precision problem (wrong chunks ranked above the right one) rather than a recall problem (right chunk never retrieved at all). Move to a full multi-vector primary index only after you have measured that first-stage recall, not reranking precision, is your actual bottleneck.

Cost, Latency, and Storage Tradeoffs

Multi-vector retrieval is not free, and the tradeoffs are worth stating plainly instead of glossing over:

  • Storage: you are storing roughly one vector per token instead of one vector per chunk, so raw storage grows with document length. Compression schemes (PLAID's residual compression, quantization in native multi-vector databases) bring this down substantially, but it will still be larger than a single-vector index of the same corpus. Measure it on your own data rather than assuming a fixed multiplier.
  • Indexing time: encoding every token (or every image patch, for ColPali-style visual retrieval) takes longer than encoding one pooled vector per chunk, and clustering-based indexes like PLAID have an upfront build cost that scales with collection size.
  • Query latency: as a reranking stage over a small candidate set, added latency is usually modest. As a primary index over a large collection, latency depends heavily on how well your indexing engine prunes candidates before running MaxSim, so this is the number you most want to benchmark directly on your own hardware and traffic patterns before committing to it in production.
  • Operational complexity: you now have a second embedding model (or the same model producing a different shape of output) and, if you are not using a database with native multi-vector support, a separate indexing and serving stack to operate alongside your existing retrieval pipeline.

None of these tradeoffs are disqualifying, but they mean multi-vector retrieval is a targeted fix for a specific failure mode (fine-grained, token-level relevance that pooled embeddings miss), not a default upgrade you should apply to every RAG pipeline without first confirming that failure mode is actually hurting your results.

When to Use Multi-Vector RAG (and When Not To)

Reach for multi-vector retrieval when:

  • Your queries frequently include exact terms, codes, identifiers, or short phrases that need to match specific tokens in a document, not the document's overall topic.
  • You have already tried hybrid search (BM25 plus dense) and are still seeing precision problems where the right chunk ranks below several less-relevant ones.
  • Your source documents are long or cover multiple sub-topics per chunk, so pooling dilutes the signal for any single query.
  • A meaningful share of your corpus is visual (PDFs with tables and charts, scanned documents, slide decks) where text extraction is unreliable, in which case ColPali-style visual multi-vector retrieval is worth evaluating specifically.

Stick with single-vector retrieval, or single-vector plus a standard cross-encoder reranker, when:

  • Your queries are natural-language questions where topical similarity, not exact term matching, drives relevance.
  • Your corpus is small enough, or your latency and cost budget tight enough, that the added storage and indexing complexity is not worth the marginal precision gain, and you have not yet confirmed (through actual evaluation, not intuition) that pooling is the bottleneck.
  • You have not yet tried simpler fixes: smaller chunk sizes, metadata filtering, hybrid BM25 plus dense fusion, or a standard cross-encoder reranker on your existing pipeline. These are cheaper to implement and often close most of the gap on their own.

The honest sequencing for most teams: get chunking and hybrid search right first, add a cross-encoder reranker if precision is still weak, and only reach for multi-vector retrieval once you can point to specific failed queries where token-level matching would have helped and a cheaper fix did not.

FAQ

What is multi-vector retrieval in RAG? Multi-vector retrieval represents each document or chunk as a set of token-level (or patch-level, for images) vectors instead of a single pooled embedding, and scores relevance by matching individual query tokens against individual document tokens rather than comparing one query vector to one document vector.

How is ColBERT different from a standard embedding model? A standard embedding model pools an entire chunk into one vector before comparison. ColBERT keeps one vector per token for both the query and the document, and scores relevance with the MaxSim late-interaction function, which finds the best-matching document token for each query token and sums those matches. This preserves fine-grained lexical and semantic signal that pooling discards.

Do I need to replace my existing vector database to use ColBERT? Not necessarily. You can run multi-vector retrieval as a reranking stage over candidates from your existing single-vector index using a library like RAGatouille, or you can migrate to a vector database with native multi-vector support, such as Qdrant or Weaviate, if you want multi-vector as your primary index.

Is multi-vector retrieval slower than standard dense search? It depends on where you use it. As a reranking stage over a small candidate set, the added latency is usually modest. As a primary index over a large collection, latency depends on how well the indexing engine (PLAID-style clustering, or a native multi-vector database's pruning) filters candidates before running the full MaxSim comparison, so this should be benchmarked directly on your own corpus and hardware.

What is ColPali and how does it relate to ColBERT? ColPali applies the same late-interaction, token-level scoring idea to documents rendered as images. Instead of extracting text with OCR, it embeds each page as a set of visual patch vectors and matches query tokens against those patches directly, which works well for documents with tables, charts, and complex layouts that text extraction tends to mangle.

Should every RAG pipeline use multi-vector retrieval? No. It is a targeted fix for precision problems caused by pooling away token-level detail, particularly with exact-match terms, codes, and long or multi-topic chunks. Teams should first try chunking improvements, hybrid BM25 plus dense search, and a standard cross-encoder reranker, and reach for multi-vector retrieval when those do not resolve specific, identifiable retrieval failures.