Skip to main content

Multi-Modal RAG

Multi-modal RAG extends retrieval-augmented generation beyond plain text to handle images, tables, charts, audio, and mixed-layout documents. Production systems now routinely ingest PDFs with diagrams, slide decks, scanned invoices, and research papers where the visual layout is the meaning. Three architectures dominate: caption-and-index, unified vision-text embeddings (Cohere Embed v4, Voyage-Multimodal-3.5, Gemini Embedding 001), and page-as-image with late interaction (ColPali, ColQwen2.5, ColNomic).

Table of Contents​


Why Text-Only RAG Fails​

Traditional RAG pipelines parse documents into text chunks, embed them, and retrieve against a text query. This breaks on real-world documents:

Document ElementText-Only RAG BehaviorActual Information Lost
Bar ChartExtracts axis labels onlyTrends, comparisons, magnitudes
Architecture DiagramMisses entirelyComponent relationships, data flow
TableFlattened rows lose structureRow-column associations, headers
InfographicCaptures scattered text fragmentsVisual hierarchy, spatial groupings
Photo with CaptionGets caption, loses imageVisual evidence, spatial context

Reality: Enterprise documents are 40-60% non-textual content. A financial report's value is in its charts. A medical paper's key finding is in its figures. Ignoring visual content means ignoring most of the knowledge.


Architecture Patterns​

There are three dominant patterns for multi-modal RAG, each with distinct trade-offs:

Pattern 1: Unified Embedding Space​

Shared Vector Space
+-------------------+
Text --> Encoder | [0.2, 0.8, ...] |
Image --> Encoder | [0.3, 0.7, ...] | --> Single Index --> Retrieve
Table --> Encoder | [0.1, 0.9, ...] |
+-------------------+

Query "show revenue trends" --> encode --> nearest neighbors across ALL modalities
  • How: Use a model like CLIP or SigLIP to project text and images into the same vector space.
  • Pros: Single index, single query, simple retrieval logic.
  • Cons: Embedding quality varies across modalities; tables need serialization.

Pattern 2: Modality-Specific Retrieval with Fusion​

Query --> +----> Text Index --> Top-K text chunks
|
+----> Image Index --> Top-K images
|
+----> Table Index --> Top-K tables
|
v
Fusion / Reranking Layer --> Combined Top-K --> VLM Generator
  • How: Separate embeddings and indices per modality. A reranker or reciprocal rank fusion (RRF) merges results.
  • Pros: Best-in-class embeddings per modality; can tune each retriever independently.
  • Cons: More infra complexity; fusion logic is non-trivial.

Pattern 3: Vision-First (Page-as-Image)​

Document Page --> Screenshot/Render --> Vision Encoder --> Multi-vector Index
|
Query ---------> Text Encoder --------------+---> Late Interaction Score
--> Retrieve top pages
  • How: Treat every document page as an image. Use a vision-language model (e.g., ColPali) to create patch-level embeddings. Score via late interaction (MaxSim).
  • Pros: No OCR, no layout parsing, no table extraction pipeline. End-to-end trainable.
  • Cons: Higher compute at indexing; loses fine-grained text search.

Recommendation: Pattern 3 (vision-first) is gaining ground fast for document-heavy use cases. Pattern 2 remains the production workhorse when you need precise text search alongside visual retrieval.


Multi-Modal Embedding Strategies​

CLIP (Contrastive Language-Image Pretraining)​

The original dual-encoder that maps text and images to a shared 512/768-dim space.

  • Strengths: Huge ecosystem, well-understood, many fine-tuned variants.
  • Weaknesses: Weaker on document-style images (charts, tables) vs. natural photos. Contrastive loss requires large batch sizes.

SigLIP / SigLIP 2​

Replaces CLIP's softmax cross-entropy with a sigmoid loss, allowing each image-text pair to be evaluated independently.

  • SigLIP 2 (2025): Adds captioning decoders, self-distillation, and masked prediction. Trained on 10B+ images across 109 languages.
  • Key Win: Outperforms CLIP at small batch sizes (4-8k) and provides denser, more robust features.
  • Production Use: National Library of Norway, e-commerce visual search, AI art curation.

Comparison for RAG​

ModelBest ForEmbedding DimDocument QualityNatural Image Quality
CLIP ViT-L/14General purpose768MediumHigh
SigLIP 2 So400mMulti-lingual docs1152HighHigh
Nomic Embed VisionText-heavy docs768HighMedium
Voyage Multimodal 3Mixed documents1024HighHigh

Embedding Strategy Decision​

Is your content mostly natural images (photos, products)?
YES --> CLIP or SigLIP fine-tuned on your domain
NO
|
v
Is your content document pages (PDFs, slides, reports)?
YES --> ColPali / ColQwen (vision-first, no OCR needed)
NO
|
v
Is it a mix of text, images, and structured data?
YES --> Modality-specific encoders + fusion (Pattern 2)

Vision-Language Models for Document Understanding​

VLMs serve two roles in multi-modal RAG: (1) as the generator that synthesizes answers from retrieved multi-modal context, and (2) as the indexing engine that extracts structured information at ingestion time.

VLM Capabilities Comparison​

CapabilityClaude Opus 4.7 / Sonnet 4.6GPT-5.5Gemini 3.1 Pro
Chart ReadingExcellentExcellentExcellent
Table ExtractionExcellentGoodExcellent
Diagram UnderstandingExcellentGoodExcellent
Handwriting OCRGoodGoodGood
Multi-page ReasoningExcellent (1M ctx on Sonnet 4.6)Excellent (1M ctx)Excellent (1M ctx)
Structured OutputNative JSON modeNative JSON modeNative JSON mode

VLM-Augmented Ingestion Pipeline​

Raw PDF
|
v
Page Renderer (pdf2image, 300 DPI)
|
v
VLM Extraction Pass:
+-- "Extract all tables as markdown"
+-- "Describe this chart: axes, trends, key data points"
+-- "Summarize the diagram: components and relationships"
|
v
Structured Output (JSON)
|
+---> Text chunks --> Text embedding index
+---> Table markdown --> Text embedding index (with metadata: "type=table")
+---> Chart summaries --> Text embedding index (with metadata: "type=chart")
+---> Page images --> Image embedding index (CLIP/SigLIP)

This "describe-then-embed" approach converts visual content into searchable text while preserving the original image for the generation step.


ColPali and Vision-Based Retrieval​

ColPali represents a paradigm shift: instead of building complex OCR + layout + table extraction pipelines, treat each document page as a single image and let a vision-language model handle everything.

How ColPali Works​

Document Page Image
|
v
SigLIP Vision Encoder (So400m)
|
Splits image into patches (e.g., 32x32 grid = 1024 patches)
|
v
Gemma 2B Language Model (contextualizes patch embeddings)
|
v
Linear Projection --> 128-dim patch embeddings
|
Result: 1024 vectors of dim 128 per page
|
v
Stored in Multi-Vector Index

At query time:
Query --> Tokenize --> Embed --> 128-dim token embeddings
|
v
Late Interaction (MaxSim):
Score = Sum over query tokens of Max similarity to any patch

ColPali vs. Traditional Pipeline​

AspectTraditional PipelineColPali
OCRRequired (Tesseract, Azure OCR)Not needed
Layout DetectionRequired (Detectron2, LayoutLM)Not needed
Table ParserRequired (Camelot, Tabula)Not needed
Chart ExtractorRequired (ChartOCR)Not needed
Indexing SpeedSlow (multi-stage)Fast (single forward pass)
Retrieval QualityHigh on text, poor on visualsHigh across all modalities
StorageText index (~small)Multi-vector index (~larger)

ColPali Family​

  • ColPali (v1): PaliGemma-3B backbone. The original.
  • ColQwen 2.5: Qwen2-VL backbone. Better multilingual support, improved on Asian-language documents.
  • ColSmol: Smaller variant for edge deployment. ~1B parameters.

ViDoRe Benchmark Results​

ColPali excels on visually complex benchmarks like InfographicVQA, ArxivQA, and TabFQuAD, which test infographics, figures, and tables respectively. It outperforms traditional text-based pipelines even on text-centric documents.


Table Extraction and Structured Data Retrieval​

Tables are the hardest modality for traditional RAG. Flattening a table row-by-row destroys the column-header relationships that give each cell meaning.

Strategy 1: VLM-Based Extraction​

# Pseudocode: Extract tables using a VLM
def extract_tables_from_page(page_image: bytes) -> list[dict]:
prompt = """
Extract ALL tables from this document page.
For each table, return:
{
"title": "table title or caption",
"headers": ["col1", "col2", ...],
"rows": [["val1", "val2", ...], ...],
"markdown": "| col1 | col2 |\\n|---|---|\\n| val1 | val2 |"
}
Return JSON array. If no tables, return [].
"""
response = vlm.generate(image=page_image, prompt=prompt)
return json.loads(response)

Strategy 2: Specialized Table Parsers​

  • Tabula / Camelot: Rule-based PDF table extraction. Fast but brittle on complex layouts.
  • Table Transformer (DETR-based): Detects table boundaries and cell structure from images.
  • Unstructured.io: Combines heuristics with ML models for layout-aware parsing.

Strategy 3: Table-Aware Chunking​

Original Table (20 rows x 8 columns)
|
v
Chunk as complete unit (do NOT split tables across chunks)
|
v
Embed the full markdown table as a single chunk
|
v
Add metadata: {"type": "table", "page": 14, "caption": "Q3 Revenue by Region"}
|
v
At generation time: pass the FULL table to the LLM, not a fragment

Key Principle: Tables must be atomic retrieval units. Never split a table across chunk boundaries.


Chart and Diagram Understanding​

Chart Types and Extraction Approaches​

Chart TypeWhat to ExtractBest Approach
Bar/Line/PieData values, trends, comparisonsVLM description + data table extraction
Flow DiagramSteps, decisions, connectionsVLM structured extraction (nodes + edges)
Architecture DiagramComponents, relationships, data flowVLM description + entity extraction
Scatter PlotCorrelations, outliers, clustersVLM trend description + raw data if available
Gantt ChartTimeline, dependencies, milestonesVLM structured extraction

Dual-Representation Strategy​

For each chart or diagram, store TWO representations:

Chart Image
|
+---> (1) Text Description (for text-based retrieval)
| "This bar chart shows Q3 revenue by region.
| North America: $4.2M, Europe: $3.1M, APAC: $2.8M.
| NA grew 15% QoQ while APAC declined 3%."
|
+---> (2) Original Image (for visual retrieval + generation context)
Stored with CLIP/SigLIP embedding for image-based queries

This ensures the chart is retrievable by both text queries ("what was APAC revenue?") and visual queries ("show me the revenue chart").


Production Architecture​

Full Multi-Modal RAG Pipeline​

INGESTION:
Raw Docs --> Doc Classifier --+--> Text-Heavy --> chunking + text embeddings
+--> Visual-Heavy --> page render + ColPali
+--> Mixed --> VLM extraction + hybrid
|
v
[Text Index] [Image Index] [Table Index]

RETRIEVAL:
Query --> Query Analyzer --+--> Text: BM25 + dense search
+--> Image: CLIP/ColPali search
+--> Table: metadata-filtered dense
|
v
Cross-Modal Reranker --> Context Assembly --> VLM --> Response

Scaling Considerations​

ConcernSolution
Index SizeColPali stores ~1024 vectors/page. For 1M pages = ~1B vectors. Use quantization (binary, PQ).
Ingestion LatencyVLM extraction is slow (~2-5s/page). Use async workers with GPU acceleration.
Query LatencyMulti-index fan-out adds latency. Use parallel retrieval + aggressive top-k pruning.
CostVLM calls at ingestion are one-time. Amortize over query volume. Budget $0.01-0.05/page for extraction.
StorageStore page images in object storage (S3). Store embeddings in vector DB. Store text in search index.

Implementation Example​

End-to-End Multi-Modal RAG with ColPali + VLM​

# Pseudocode: Production multi-modal RAG pipeline

from colpali_engine import ColPali, ColPaliProcessor
from qdrant_client import QdrantClient
import anthropic

# --- INDEXING ---

def index_document(pdf_path: str, collection: str):
"""Index a PDF document using ColPali for visual retrieval
and VLM extraction for text-based retrieval."""

pages = render_pdf_to_images(pdf_path, dpi=300)

colpali_model = ColPali.from_pretrained("vidore/colpali-v1.3")
processor = ColPaliProcessor.from_pretrained("vidore/colpali-v1.3")
vlm_client = anthropic.Anthropic()

for page_num, page_image in enumerate(pages):
# 1. Generate ColPali multi-vector embeddings
inputs = processor(images=[page_image])
patch_embeddings = colpali_model(**inputs) # shape: [1, 1024, 128]

# 2. Extract structured content via VLM
extraction = vlm_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": encode_image(page_image)},
{"type": "text", "text": """Extract from this page:
1. All text content (preserve structure)
2. Tables as markdown
3. Chart descriptions with data points
Return as JSON with keys: text, tables, charts"""}
]
}]
)

structured = json.loads(extraction.content[0].text)

# 3. Store in vector DB
qdrant.upsert(collection, points=[
# ColPali multi-vector for visual retrieval
PointStruct(
id=f"{pdf_path}:page:{page_num}:colpali",
vector={"colpali": patch_embeddings[0].tolist()},
payload={
"source": pdf_path,
"page": page_num,
"type": "page_image",
"text_preview": structured["text"][:500]
}
),
# Text embeddings for each extracted element
*create_text_chunks(structured, pdf_path, page_num)
])


# --- RETRIEVAL ---

def retrieve(query: str, collection: str, top_k: int = 5):
"""Hybrid retrieval: ColPali visual + text semantic search."""

# Visual retrieval via ColPali
query_inputs = processor(text=[query])
query_embeddings = colpali_model(**query_inputs)

visual_results = qdrant.query(
collection,
query_vector=("colpali", query_embeddings[0].tolist()),
limit=top_k,
query_filter=Filter(must=[FieldCondition(key="type", match="page_image")])
)

# Text retrieval via dense embeddings
text_embedding = text_encoder.encode(query)
text_results = qdrant.search(
collection,
query_vector=("text", text_embedding.tolist()),
limit=top_k
)

# Fuse results using reciprocal rank fusion
fused = reciprocal_rank_fusion(visual_results, text_results, k=60)
return fused[:top_k]


# --- GENERATION ---

def generate_answer(query: str, retrieved_context: list) -> str:
"""Generate answer using VLM with multi-modal context."""

content_blocks = [{"type": "text", "text": f"Question: {query}\n\nContext:"}]

for ctx in retrieved_context:
if ctx.payload["type"] == "page_image":
# Include the actual page image
content_blocks.append({
"type": "image",
"source": load_page_image(ctx.payload["source"], ctx.payload["page"])
})
else:
# Include text/table content
content_blocks.append({
"type": "text",
"text": f"[{ctx.payload['type']}] {ctx.payload['content']}"
})

content_blocks.append({
"type": "text",
"text": "Answer the question using ONLY the provided context. Cite sources."
})

response = vlm_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{"role": "user", "content": content_blocks}]
)
return response.content[0].text

System Design Interview Angle​

Q: Design a RAG system for a financial research platform that needs to answer questions about earnings reports containing text, tables, and charts.​

Strong answer:

The core challenge is that 60%+ of the information in earnings reports lives in tables and charts, not prose. A text-only RAG pipeline would miss revenue breakdowns, trend lines, and comparative data.

Architecture: I would use a hybrid approach (Pattern 2 + elements of Pattern 3):

  1. Ingestion: Render each PDF page at 300 DPI. Run a VLM extraction pass to convert tables to markdown and charts to structured descriptions. Simultaneously generate ColPali multi-vector embeddings for each page image.

  2. Storage: Three indices -- (a) text chunks with dense embeddings (financial text), (b) table markdown with dense embeddings plus metadata filters for table type, (c) ColPali multi-vector index for page-level visual retrieval.

  3. Retrieval: Query analyzer classifies the query type. "What was Q3 revenue?" triggers text + table search. "Show me the revenue trend" triggers visual (ColPali) search. Results are fused via RRF and reranked by a cross-encoder.

  4. Generation: A VLM (Claude or Gemini) receives the fused context -- text chunks, table markdown, and relevant page images. It generates a grounded answer with citations to specific pages and tables.

Key trade-offs: ColPali gives excellent recall on visual content but stores ~1024 vectors per page, so for 100k documents (500k pages), that is ~500M vectors. I would use binary quantization to reduce storage by 32x, accepting a small recall hit. For the text path, BM25 + dense hybrid search handles financial terminology well.

Q: How would you handle a query that requires information from BOTH a chart and a table on different pages?​

Strong answer:

This is the cross-modal, cross-page retrieval problem. The solution has three parts:

  1. Retrieval diversity: Ensure the retriever returns results from multiple modalities. Set minimum quotas -- at least 2 text results, 2 table results, and 1 visual result in every retrieval set, regardless of which modality scores highest.

  2. Context assembly: When assembling the VLM prompt, include all retrieved content with explicit provenance: "[Table from page 14: Q3 Revenue by Region]" and "[Chart from page 22: Revenue Trend 2024-2026]". The VLM can then reason across both.

  3. Agentic fallback: If the initial retrieval does not surface enough cross-modal context, an agentic layer can issue follow-up retrievals: "The table shows revenue numbers but the user asked about trends -- let me also search for charts related to revenue."

The key insight is that cross-modal questions are inherently multi-hop. The system needs to retrieve from one modality, recognize the gap, and retrieve from another.


References​

  • Faysse et al. "ColPali: Efficient Document Retrieval with Vision Language Models" (ICLR 2025)
  • Google. "SigLIP 2: Multilingual Vision-Language Encoders" (2025)
  • NVIDIA. "An Easy Introduction to Multimodal Retrieval-Augmented Generation" (2025)
  • HKUDS. "RAG-Anything: All-in-One Multimodal RAG Framework" (2025)
  • Vespa Blog. "PDF Retrieval with Vision Language Models" (2024)

Previous: Advanced Retrieval Patterns | Next: RAG Evaluation Patterns