Notebook 15 · Multimodal RAG · Real Document Case Study
GenAI 2026 · Module 10 · Notebook 15

Multimodal RAG
Teaching Your Chatbot to See

Every RAG pipeline so far reads text beautifully and throws every image straight in the trash.
Real documents hide their most important numbers inside charts. This deck walks through three real, working architectures — built and run against a real 63-page thesis, not a toy example.

21
Real embedded chart images extracted from one real PDF
3
Full multimodal RAG architectures built and compared
252
Text + image chunks in one hybrid retrieval index
9/9
Real business questions, verified correct end-to-end
GPT-4o Visiontext-embedding-3-smallChromaBM25Cross-Encoder RerankCLIPColPali / MaxSim
The Problem

Real documents hide their answers inside pixels

Our loader from Notebook 02 (PyMuPDF4LLMLoader) extracts text beautifully. It throws every embedded image straight in the trash — and business documents put their most important findings inside those images.

TEXT-ONLY RAG
Real 63-page dissertation: "Apple as a Brand: A Detailed Analytical Approach." 109-respondent survey. 21 charts embedded as Microsoft Forms screenshots. Text-only extraction sees none of them.
✗ "Not provided in the context" for chart-only facts
✗ Can be misled by the author's own prose errors
✗ Zero of the 21 charts are searchable at all
MULTIMODAL RAG
Same document. The chart itself gets read by a vision model and grounded on the actual pixels — not a caption someone wrote about it.
✓ Reads exact numbers straight off the chart
✓ Resolves conflicts between prose and data
✓ All 21 charts become genuinely searchable
A genuine gotcha we found in this real document: the author's own prose analysis of "Chart 4.1" states "46 out of 121" respondents used Apple products for 5–10 years. The chart itself — the original data — shows 43 (across six categories that sum to 108, not 121). Two numbers, one document, one true answer — and only the one that actually reads the chart pixels gets it right.
🎯 Check your understanding
A document's prose paragraph and its own chart disagree on a number. What should a well-designed multimodal RAG system do?
The Landscape

Three ways to make an image searchable

An embedding model can't search raw image bytes unless you pick one of three real architectures. As of 2026 the market has converged on exactly these three — and this notebook builds and runs all three against the same real document, not just describes them.

ArchitectureHow it worksBest forTradeoff
① Caption-and-index A vision LLM writes a detailed text caption of each image; embed the caption; keep the raw image linked by ID Business/report documents — reads exact chart numbers correctly Extra LLM call per image; needs generation grounded on real pixels
② Unified embeddings One model (CLIP here; Cohere Embed-v4 / Voyage multimodal-3.5 in production) embeds text and images in the same vector space — no captioning at all Genuinely diverse visual corpora — zero LLM calls Can't read the fine legend text that tells two similar charts apart
③ Page-as-image / late interaction Skip parsing — embed at patch level (dozens of regions per image), score with MaxSim (ColBERT-style) Visually dense documents, layout-sensitive matching Needs a purpose-trained backbone (real ColPali); heavier infra
We start with ① because it reuses everything already built in Notebooks 05–08 unchanged — and by the end of this deck you'll see real evidence for exactly why it's the right default for a document like this one.
Architecture ① — Core Idea

What you search on ≠ what you return

A vector index needs searchable text. A user answer needs the real content. For an image chunk those are two different things — and treating them as the same thing is the root cause of most broken multimodal RAG demos.

Chart image
(pixels)
docstore value
→
GPT-4o Vision
caption
searchable text
→
Embed +
index
what gets searched
→
Winning ID
retrieval result
→
Look up real
image bytes
what the user sees
LangChain calls this the multi-vector retriever pattern: a docstore (id → raw content, text or image) kept separate from the vector store (id → embedding of the searchable representation). Retrieval searches the vector store; once you have the winning ID, you look up the real content in the docstore.
CAPTION PROMPT — WHAT MAKES IT WORK
Ask for the chart title, every axis/legend label, and every exact number — never "describe this image." A vague prompt invites editorializing; a specific one produces genuinely searchable text.
A REAL CAPTION FROM THIS RUN
"...it notes that '43 respondents (43%) answered Samsung for this question.'"
Architecture ① — End to End

Extract → Caption → Index → Retrieve → Ground

No new retrieval machinery beyond Notebooks 05–08 — dense (Chroma) + sparse (BM25) + RRF fusion + cross-encoder rerank. The index has no idea it's holding a mix of modalities.

for page_num in range(len(doc)): for img_index, img in enumerate(doc[page_num].get_images(full=True)): base = doc.extract_image(img[0]) # drop tiny decorative icons — real charts only if base["width"] < 100 or base["height"] < 100: continue images.append({"id": f"img_{page_num}_{img_index}", "bytes": base["image"]}) # → 21 real chart images extracted (PyMuPDF4LLMLoader extracted 0)
Step 1–2
Extract + Caption
fitz pulls real embedded raster images. GPT-4o Vision (temperature=0) captions each one with exact numbers.
21 imagestemp=0
Step 3
Hybrid Index
231 text chunks + 21 image captions = 252 chunks, indexed into Chroma (dense) and BM25 (sparse).
ChromaBM25
Step 4–5
Retrieve + Ground
RRF fusion, cross-encoder rerank, then GPT-4o Vision reads the actual pixels of any retrieved chart — not its caption.
Real pixelsCitations
A Real Bug We Found And Fixed

The reranker was biased against charts

The obvious first design: one unified ranking — dense + BM25 fusion over all 252 chunks, then a single cross-encoder reranker picks the winners, text and image captions competing head to head. Checking the actual scores against a known-correct answer exposed a real problem.

RERANK SCORE FOR: "What % considered Samsung as an alternative?"
[text] "Customer Loyalty and Satisfaction..."
+5.06
[text] "Samsung emerges as a significant..."
+4.48
[text] "Chart 4.2. What alternatives did..."
+3.92
[image] has the real "43%" answer
−3.65
✗ BURIED OUTSIDE TOP 6— the chunk with the actual answer never reaches GPT-4o's context at all
Why: the cross-encoder was trained on natural prose passages (MS-MARCO). It consistently rates a chart caption's plain data listing below a flowing paragraph that merely discusses the same topic — even when only the caption has the exact number. The fix: don't force text and image captions into one ranking. Rerank text among itself; let images compete in their own dedicated embedding-similarity ranking, guaranteed into context. Not a hack — production multimodal RAG systems commonly score modalities separately for exactly this reason.
Architecture ① — The Other Half Of Correctness

Show exactly what was used — nothing more

Guaranteeing charts reach context doesn't mean every chart retrieved was actually relevant. GPT-4o cites its sources inline as [1], [2]... — parse those citation numbers and display only the chart(s) genuinely used in the final answer.

Two real runs from this notebook

Click a question to see which of its retrieved candidates actually got cited and shown to the user.

This is the actual fix for the classic multimodal RAG failure mode: showing every image the retriever pulled back, whether the answer used it or not. Verified across 9 real questions on this document — exactly the right number of charts get shown every time, from 0 up to 2, never a fixed count.
Architecture ②

Unified embeddings — CLIP
No captioning step at all

CLIP is trained by contrastive learning: millions of (image, caption) pairs, pulled together in one shared vector space, everything else pushed apart. The result — a single model that embeds raw pixels and raw text into the same space, so cosine similarity alone can match them. Zero LLM calls.

REAL WIN — CHART TYPE
"a word cloud of survey responses" → img_35_0 (0.254) ✓
"a pie chart" → img_39_0 (0.279) ✓
"a bar chart" → img_41_0 (0.282) ✓
CLIP genuinely tells word clouds, pie charts, and bar charts apart by their broad visual shape.
REAL LOSS — CHART TOPIC
"a chart about age" → img_41_0 ✗ (wrong)
"a chart about gender" → img_41_0 ✗ (wrong, same wrong pick)
Both queries default to the same unrelated bar chart. CLIP never learned to read the tiny legend text that actually distinguishes them.
🎯 Check your understanding
Why can CLIP tell a pie chart from a bar chart, but not the "age" chart from the "gender" chart?
Architecture ③

Page-as-image — late interaction
The real ColPali/ColQwen mechanism

Instead of one pooled vector per image, keep every patch — roughly 49 small regions, each with its own vector. Score with MaxSim (the ColBERT "late interaction" rule): for every query token, find its single best-matching patch anywhere in the image, then sum those best matches.

Image
→
49 patch
vectors
not 1 pooled vector
→
Query token
vs. every patch
→
Sum of
best matches
= MaxSim score
The honest finding: naive MaxSim on CLIP's own raw patches did worse than pooled CLIP — on the exact same "easy" chart-type queries pooled CLIP got 3/3 right, patch-level MaxSim got 0/3. Reason: CLIP's projection layers were trained only on the single pooled vector, never on individual patches — repurposing them per-patch is out-of-distribution, so the signal is noisier, not richer.
WHY REAL COLPALI IS DIFFERENT
Real ColPali fine-tunes the vision-language backbone with a MaxSim loss — directly supervising patch-level alignment, instead of bolting MaxSim onto a backbone that never saw it during training.
from colpali_engine.models import ColPali, ColPaliProcessor model = ColPali.from_pretrained("vidore/colpali-v1.2", device_map="cuda") scores = processor.score_multi_vector(query_emb, page_emb)
Putting It Together

Which architecture, for which document?

No architecture wins everything — that's the actual lesson from running all three against one real document. The right choice depends on how visually diverse your images are.

Interactive · Pick a document type
Verified, Not Claimed

9 real questions, 9 correct answers

Every answer below was checked against the actual chart pixels by hand before being accepted — not just "the code ran without an error."

✓
Samsung considered as an alternative?
43%
✓
Most valuable product feature?
Camera quality
✓
Biggest factor behind Apple's growth?
Strong brand reputation (39)
✓
Largest respondent age group?
26–35 (78 respondents)
✓
Gender split of respondents?
58 women / 49 men
✓
Marital status breakdown?
70 single / 33 married
✓
Highest level of education?
Bachelor's degree (49)
✓
Avg. rating to recommend Apple?
7.89 / 9
✓
Brand positioning strategy? (text-only control — should cite zero charts)
Customer experience, innovation, design — 0 images cited ✓
Summary

The idea generalizes further than images

What you search on and what you show the user don't have to be the same object. That's the multi-vector pattern — and it's one instance of a bigger idea: search doesn't have to happen in text-space at all (CLIP), and it doesn't have to happen at one-vector-per-item granularity either (patch-level late interaction).

ArchitectureWhat we actually saw it do wellWhat we actually saw it struggle with
① Caption-and-indexCorrectly read exact survey numbers off 8 different real chartsExtra LLM call per image; needs generation grounded on pixels
② CLIP embeddingsTold word cloud / pie / bar chart types apart, zero LLM callsCouldn't tell which pie chart — no OCR ability
③ Late interactionReal, working MaxSim mechanism, end to endWorse than pooled CLIP without a purpose-trained backbone
What's still missing: scale (captioning doesn't work at 50,000 pages), scanned documents with no text layer at all, the real cost of grounding on pixels twice, and video/audio as the next modalities where the same "decouple search key from returned value" idea applies unchanged.
Notebook 15 · zero-to-genai-engineerModule 10 · RAGGenAI 2026