ColPali and Vision-Native Document RAG
Traditional RAG parses PDFs into text, splits the text into chunks, embeds the chunks, and stores vectors. Each step loses signal: OCR drops chart data, chunking breaks table rows, and text embeddings ignore figures. ColPali (Faysse et al., July 2024) asked the simpler question of why extract text at all. It embeds the page image directly via PaliGemma and retrieves with ColBERT-style late interaction, so the layout, figures, fonts, and formatting all stay in play. Published benchmarks report 20-40% better end-to-end accuracy than text-RAG on visually-rich documents. ColQwen2, ColSmol, and VisRAG extended the pattern. This lesson reads the vision-native RAG thesis and builds a tiny ColPali-like indexer.
Learning Objectives
- Explain the difference between bi-encoder retrieval (one vector per document) and late-interaction retrieval (many vectors per document).
- Describe ColBERT's MaxSim operation and how ColPali generalizes it from text tokens to image patches.
- Build a tiny ColPali-like indexer: page → patch embeddings → MaxSim over query-term embeddings → top-k pages.
- Compare ColPali + Qwen2.5-VL generator vs text-RAG + GPT-4 on an invoices / financial reports use case.
The Problem
Text-RAG on PDFs throws away most of what makes a document a document. A financial report's Q3 revenue growth usually lives in a chart. A medical report's findings sit in annotated images. A legal contract's signature block is a layout fact, not a text fact.
The text-RAG pipeline:
- PDF → text via OCR / pdftotext.
- Text → 300-500 token chunks.
- Chunk → bi-encoder embedding (one vector).
- User query → embedding → cosine similarity → top-k chunks.
- Chunks + query → LLM.
That is five lossy steps. Charts go uncaptured, tables get broken across chunks, multi-column layouts flatten, and figure annotations disappear.
ColPali's fix is to skip OCR and embed the page image directly. Late interaction, the ColBERT way, lets the model attend to fine-grained patches at query time.
The Concept
ColBERT (2020)
ColBERT (Khattab & Zaharia, arXiv:2004.12832) is a text retrieval method. Instead of one vector per document, it produces one vector per token. At query time:
- Query tokens get their own embeddings (N_q vectors).
- Document tokens get embeddings (N_d vectors, typically cached).
- Score = sum over query tokens of max over document tokens of cosine similarity: Σ_i max_j cos(q_i, d_j).
This is the MaxSim operation. Each query token "picks" its best-matching document token, and the final score is the sum.
Pros: strong recall, handles term-level semantics. Cons: N_d vectors per document, so storage is expensive.
ColPali
ColPali (Faysse et al., arXiv:2407.01449) applies the ColBERT pattern to images.
- Each page is encoded by PaliGemma (ViT + language) into patch embeddings: N_p vectors per page.
- Each user query (text) is encoded into query-token embeddings: N_q vectors.
- Score = Σ_i max_j cos(q_i, p_j), i.e., MaxSim over query-text-tokens and page-image-patches.
- Retrieve top-k pages by total score.
At ingestion time, embed every page with PaliGemma and store all the patch embeddings. At query time, embed the query tokens, compute MaxSim against every stored page, and return the top-k pages.
Pros: end-to-end accuracy beats text-RAG by 20-40% on visually rich documents, because each patch vector captures local layout and content.
Cons: N_p patches × 4-byte floats × D-dim vectors per page means storage grows fast. PQ / OPQ quantization mitigates it.
ColQwen2 and ColSmol
ColQwen2 (illuin-tech, 2024-2025) swaps PaliGemma for Qwen2-VL. That gives a better base encoder and better retrieval.
ColSmol is the smaller-scale variant for local / edge use. A ColSmol retriever at ~1B params runs on a consumer GPU.
VisRAG
VisRAG (Yu et al., arXiv:2410.10594) takes a different route. Instead of MaxSim on patches, it pools each page into a single vector with a VLM and then does bi-encoder retrieval. Indexing is faster and storage is smaller, but recall is weaker.
The trade-off is quality versus cost: ColPali for quality, VisRAG for scale.
M3DocRAG
M3DocRAG (Cho et al., arXiv:2411.04952) extends multi-modal retrieval to multi-page, multi-document reasoning. It retrieves pages across documents and composes a multi-page context for the VLM.
ViDoRe, the benchmark
ViDoRe (Visual Document Retrieval Evaluation) is ColPali's companion benchmark. Tasks include financial reports, scientific papers, administrative documents, medical records, and manuals. The metric is nDCG@5.
ColPali-v1 scores ~80% nDCG@5 on ViDoRe. Text-RAG on the same documents scores ~50-60%.
The end-to-end RAG pipeline
For a vision-native RAG:
- Ingest: PDF → page images → PaliGemma encoding → store all patch embeddings.
- Query: user text → query-token embeddings → MaxSim against all indexed pages → top-k pages.
- Generate: top-k page images + query → VLM (Qwen2.5-VL or Claude) → answer.
There is no OCR step, so figures, charts, fonts, and layout can all flow into the answer.
Storage math
A 50-page financial report with 729 patches per page and 128-dim embeddings:
- ColPali: 50 * 729 * 128 * 4 bytes = ~18 MB raw, ~4 MB after PQ.
- Text-RAG: 50 chunks * 768-dim * 4 bytes = ~150 kB.
Raw, ColPali needs roughly 120x the storage of text-RAG for the same document. At scale, OPQ / PQ brings that down to ~5-10x, which is usually tolerable.
When text-RAG still wins
- Pure-text documents with no layout signal (wiki articles, chat logs). Text-RAG is simpler and cheaper to store.
- Multi-million-page archives where storage dominates cost.
- Strict regulatory requirements that demand extractable OCR text alongside the retrieval.
For everything else in 2026, including financial reports, scientific papers, legal contracts, medical records, and UX documentation, vision-native RAG wins.
Use It
code/main.py:
- Builds 5 toy pages of 16 random 32-dim patch embeddings each, with a per-topic mean shift. There is no real encoder, only random vectors.
- MaxSim scorer: computes the ColBERT-style score between a set of query token embeddings and a page's patch set.
- Indexes the 5 pages, runs 3 queries of 4 token embeddings each, and prints the top 3 pages with scores.
- Also prints MaxSim versus mean similarity on a small hand-built example, and a storage table.
Ship It
This lesson produces outputs/skill-vision-rag-designer.md. Given a document-RAG project, picks ColPali / ColQwen2 / VisRAG / text-RAG and sizes the storage.
I'd run main.py and read the MaxSim versus mean printout first, because it is the whole idea in a dozen lines. On the hand-built example each of the two query tokens lands on the one strong patch, and MaxSim keeps those matches (1.478) while the mean gets diluted by the other patches (0.501).
Be careful with the retrieval half of the output. Pages and queries are random Gaussian vectors with a small per-topic mean shift, and there is no PaliGemma anywhere. In my run, none of the three queries ranked its matching topic page first. Read that output as a check that maxsim and retrieve behave, not as evidence that late interaction retrieves well. The 20-40% and ViDoRe figures come from the published benchmarks, not from this code.
The storage table is hardcoded strings too, not something the script measures. The arithmetic behind it is worth redoing by hand, and Exercise 1 does that for a 200-page report. Storage is what pushes a real project toward VisRAG or text-RAG, so I'd size it before picking a retriever.
Exercises
-
A 200-page annual report at 729 patches per page, 128-dim emb, 4-byte floats. Compute raw storage and PQ-compressed (8x) storage.
-
MaxSim is Σ_i max_j cos(q_i, p_j). What does this sum capture that a simple mean similarity does not?
-
ColPali indexes pages as patch sets. What changes if we instead index at the word level (as ColBERT does)? Trade-offs?
-
Design the end-to-end pipeline for a 1M-page corpus with a latency budget of 500ms per query. Pick ColQwen2 / VisRAG and justify.
-
Read M3DocRAG (arXiv:2411.04952). Describe the multi-page attention pattern and how it differs from single-page ColPali retrieval.
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Late interaction | "ColBERT-style" | Retrieval using per-token or per-patch embeddings + MaxSim, not a single doc vector |
| MaxSim | "Max-over-patches" | For each query token, pick the highest-similarity document token; sum across query |
| Bi-encoder | "Single-vector" | One vector per document; faster but loses granularity |
| Multi-vector | "Many-vectors-per-doc" | Store N_p vectors per document / page; storage cost grows but recall improves |
| Patch embedding | "Page feature" | One vector per image patch from a VLM encoder, cached per page |
| ViDoRe | "Vision doc bench" | ColPali's benchmark suite for visual document retrieval |
| PQ quantization | "Product quantization" | Compression that maintains vector similarity while shrinking storage ~8x |
Code
- main.py (python, 132 lines)