Read Scanned PDFs with a Vision Model
Marco manages the archive for a regional environmental agency. Over two decades, the agency digitised field reports by scanning them: stacks of pages that are images, not text. Each report contains the usual prose, but also bar charts of pollutant readings, maps with annotated regions, and hand-drawn diagrams that are the actual finding. A plain text extraction pipeline either returns nothing or returns the sparse OCR fragments the scanner could recover — neither is useful for retrieval.
The problem is not the model or the embedder. It’s that text-only pipelines treat a scanned page as a blank. The meaning lives in the pixels, and nothing in the pipeline looks at them.
What you’ll build
Section titled “What you’ll build”An archive where every scanned report — text, charts, figures, and maps — is:
- Parsed into both text fragments and captured images by the
doclingparser. - Described in natural language by a vision-language model (VLM) that reads those images.
- Enriched with the same
type,topics,tags, andsummaryfields as any other document. - Queryable so that a question about a chart gets the chart’s described content, not silence.
By the end you’ll have a reports.indx archive where indx query surfaces answers derived from figures — not just from text.
How the VLM slot works
Section titled “How the VLM slot works”indx’s pipeline has two model slots in the Enrich stage (05 enrich):
- LLM slot — always active; handles text →
type,topics,tags,summary. - VLM slot — off by default (
none); activates with--vlm; describes images the parser captured intoParsedDoc.images.
The docling parser (the default) pulls images and figures out of each file into ParsedDoc.images during the Parse stage. When the VLM slot is active, those images are passed to the vision model and the resulting descriptions feed the same enrichment fields the LLM populates from text. The two contributions are merged — one document, one set of enriched metadata.
The VLM slot has two values:
| Value | Where it runs | Network egress |
|---|---|---|
| qwen-vl | Local (Qwen-VL via Ollama) | None |
| gpt4o | Cloud (OpenAI) | Images sent to provider |
| gpt4o:<model> | Cloud, pinned model | Images sent to provider |
The build
Section titled “The build”-
Install indx.
Choose the stack that matches your privacy and infrastructure requirements. Marco’s agency has sensitive records, so the fully-local option matters — but the cloud path is shown first because it’s the fastest starting point.
Terminal window pip install "indx[cloud]"export OPENAI_API_KEY="sk-..."Terminal window pip install "indx[local]"# Qwen-VL must be available through Ollama:# ollama pull qwen-vl -
Run the build.
Point indx at the reports folder. Add
--vlmto activate the vision slot.Terminal window indx ./reports --out ./ai-ready --name reports --vlm gpt4oTerminal window indx ./reports --out ./ai-ready --name reports --llm ollama:qwen2.5 --vlm qwen-vlHere is what a representative run looks like (cloud path,
gpt4oVLM):indx ./reports → ./ai-ready (--name reports)01 walk 318 files, 24 folders02 parse 318 ok, 0 skipped03 chunk 2740 chunks04 relate 894 relations05 enrich 318 documents (openai:gpt-5-mini + gpt-4o vision)06 embed 2740 vectors → qdrant, sealed reports.indxdone: 2740 chunks, 318 docs, embed_dim=1536 (48.2s)Notice the
05 enrichline: both the LLM model and the vision model are at work. Every document that had captured images inParsedDoc.imagesreceived VLM descriptions alongside the LLM’s text-derived enrichment. -
Sanity-check retrieval.
Before connecting an agent or exporting to another system, query the archive directly. Pick a question whose answer would only be in a chart.
Terminal window indx query ./ai-ready/reports.indx "what did revenue do in Q3?" -k 3#1 score 0.81 reports/2023/q3-annual-summary.pdf (reports/2023 · financial-report)"The Q3 bar chart shows revenue rising from €2.1 M in July to €2.8 M in September,a 33 % increase over the quarter. The trend line projects continued growth into Q4."neighbors: chunk_1042, chunk_1044That answer is derived from the chart image, not from surrounding prose — the scanned page had no machine-readable text at that position. Without the VLM the chunk would have been empty or absent from the results entirely.
-
Optional — use the SDK instead of the CLI.
from indx import DirectoryPipeline# CloudDirectoryPipeline(vlm="gpt4o").run("./reports", "./ai-ready")# Fully localDirectoryPipeline(llm="ollama:qwen2.5", vlm="qwen-vl").run("./reports", "./ai-ready")
Why scanned PDFs need a separate model slot
Section titled “Why scanned PDFs need a separate model slot”A standard RAG pipeline operates on extracted text. For a clean PDF that text is accurate and complete. For a scanned PDF it is absent, sparse, or garbled by OCR. The semantic content — what a chart shows, what a diagram labels, what a table of figures reports — is encoded in pixels.
indx separates the two concerns deliberately:
- The parser (
doclingby default) is responsible for pulling images out of files and collecting them intoParsedDoc.images. It does not describe them. - The VLM slot is responsible for description. It is opt-in because most archives do not need it, and activating it for a clean-text corpus adds cost without benefit.
Because the VLM’s descriptions feed the same enrichment fields as the LLM, downstream retrieval, filtering, and agent tools work identically whether a chunk’s metadata came from text or from a described figure.
When to use the VLM — and when not to
Section titled “When to use the VLM — and when not to”| Your content | Recommendation |
|---|---|
| Scanned / image-only PDFs | Enable VLM — text extraction returns nothing useful |
| PDFs with embedded charts and diagrams | Enable VLM — figures carry signal the text does not |
| Screenshots, annotated maps, architecture diagrams | Enable VLM |
| Clean text-only PDFs, Markdown, plain text | Leave VLM at none — it adds cost with no signal |
| Mixed corpus (some scanned, some clean) | Enable VLM — it only acts on documents that have captured images |
What you learned
Section titled “What you learned”- The
doclingparser captures images and figures intoParsedDoc.imagesduring the Parse stage; the VLM slot then describes them during Enrich. - The VLM slot is
noneby default. Set--vlm qwen-vlfor fully local processing or--vlm gpt4o(optionallygpt4o:<model>to pin a specific model) for cloud processing. - LLM and VLM contribute to the same enrichment fields —
type,topics,tags,summary— so the rest of the pipeline and all downstream tools are unaware of the distinction. - A question whose answer lives in a chart now returns that answer; without the VLM the chunk would be empty.