RAGEmbeddings
Parsing documents before you chunk them
The step chunking assumes already happened - turning a scanned page, a multi-column PDF, or a table into text worth splitting.
WHY THIS, FOR YOU
You pointed your chunker at a folder of PDFs and it ran without complaint. Retrieval came back wrong in three specific ways: a sentence from the left column glued to one from the right, a pricing table turned into a stack of bare numbers with no headers, and a scanned invoice that produced no text at all. You spent two days tuning chunk size and swapping embedding models, and none of it helped, because the input was already wrong before the chunker saw it. This lesson covers the step before the Chunking lesson: matching a parser to each kind of page, keeping tables intact, sending only the pages that need an expensive reader to one, and reading the raw text before you tune anything.
Chunking assumes it is handed clean text. This lesson is the step that makes it so. Skip it and every later lesson tunes a pipeline that was fed garbage on day one.
1.What a naive parser throws away
Call any PDF library's default text extraction and it hands you a string without complaint. That string is not the document. It is the order in which glyphs were drawn on the page, because a PDF does not store reading order - it stores positions. That the left column comes before the right, or that a page header is not a new sentence, is an inference the format never makes for you. On a single column of prose every extractor gets it right; on anything else it fails in predictable ways.
None of these are edge cases: contracts have footers, financial reports have tables on every page, and support tickets are exported with screenshots.
A chunk can only be as good as the text it was cut from. If the parser already lost the table, no chunk size and no embedding model fixes it.
2.Matching the tool to the document
No parser is always right, so think in four tiers and pick the cheapest that works. Names below are examples as of October 2026, not a ranking; none was benchmarked here.
Plain text extraction is the free baseline in every PDF library, such as PyMuPDF (version 1.28.2 at the time of writing). It is fine for clean single-column text. Read its output before reaching for anything fancier.
Layout-aware parsing detects the blocks on a page - title, paragraph, list item, table - and rebuilds reading order across columns. Docling (open source, MIT licence, started at IBM Research), Unstructured (an open-source library with fast, hi_res and ocr_only strategies, plus a hosted service) and LlamaParse (hosted, in tiers from fast to agentic) work this way. They return structured elements, which is what a heading-aware chunker wants.
OCR is for pages with no text at all, only an image of text: a scanned invoice, a photographed whiteboard. It produces the words, and a layout pass still has to run on them.
Vision-model page reading sends the page image to a model that sees layout and text together. It is the most expensive tier and the most tolerant of messy layouts.
| Handles scanned pages | Preserves table structure | Reading order on columns | Cost | Best for | |
|---|---|---|---|---|---|
| Plain text extraction | No | No | Poor | Free, instant | Clean single-column text |
| Layout-aware parser | No | Yes, usually | Good | A library call | Structured PDFs, reports, contracts |
| OCR | Yes | No, on its own | Poor, on its own | A per-page call | Scans and photographs |
| Vision-model page reading | Yes | Yes | Yes | A model call per page | Irregular layouts, forms, mixed content |
Real pipelines choose per page, not per corpus. Count words, images and tables on each page with the cheap library. A page with almost no words and an image goes to OCR or a vision model, one with a detected table to a layout parser, and the rest stay on plain extraction. Store the page number and tier beside each piece of text, so a bad chunk can be traced.
- no
- n/a
- none
Routing is where the money is. Take 10,000 pages, of which 6 percent are scans or messy layouts (an invented rate - count yours). Reading everything with a model is 10,000 calls; routing makes it 600, which is 94 percent fewer. As of October 2026 the Claude API accepts PDFs of up to 32 MB and 600 pages per request (100 when the context window is under 1M tokens) and treats each page as text and image, at roughly 1,500 to 3,000 text tokens a page before image tokens (vendor docs). Sent whole, a 100-page contract costs at least 150,000 text tokens (100 x 1,500), however few of its pages are hard.
3.Tables, scans, and knowing when parsing is the problem
The table that looks like data. Flattening a table is the most damaging thing a naive parser does. A paragraph that loses its formatting is still readable; a table that loses its grid yields values that look like real data and mean nothing. That chunk will retrieve, get pasted into a prompt and get summarised as if true. The quieter failure is the page that parsed to nothing. A scan returns an empty string, many pipelines embed it anyway, and the invoice is never found. Nothing errors, so alert on pages with zero words.
Both show up in a three-page test PDF built for this lesson, with invented content: two columns drawn line by line, a small pricing table, and a picture of page 1 with no text layer. For the table, convert it to markdown or JSON before the chunker, so each value keeps its row and column label.
Plan Seats Price per seat Starter 5 14 Team 25 30 Business 100 90
Twelve cells, one per line, headers first. Nothing says 14 is a price.
| Plan | Seats | Price per seat |
|---|---|---|
| Starter | 5 | 14 |
| Team | 25 | 30 |
| Business | 100 | 90 |
Rebuilt by the library's table finder, free.
The code below routes each page and holds the model call that only scan pages would get.
# pip install pymupdf anthropic
import base64
import os
import anthropic
import pymupdf
client = anthropic.Anthropic()
MODEL = os.environ["LLM_MODEL"] # set to a current vision-capable model
MIN_WORDS = 20 # invented starting threshold: tune it on your own pages
def route(page):
words = len(page.get_text().split())
if words < MIN_WORDS and page.get_images():
return "vision" # a picture of text: no text layer to extract
if page.find_tables().tables:
return "layout" # a grid to rebuild before chunking
return "text" # plain extraction is enough
def read_with_vision(page):
png = page.get_pixmap(dpi=150).tobytes("png")
message = client.messages.create(
model=MODEL,
max_tokens=1500,
messages=[{"role": "user", "content": [
{"type": "image", "source": {
"type": "base64",
"media_type": "image/png",
"data": base64.standard_b64encode(png).decode(),
}},
{"type": "text", "text": (
"Transcribe this page in reading order. Write every table "
"as a markdown table with its header row."
)},
]}],
)
return message.content[0].text
for number, page in enumerate(pymupdf.open("test.pdf"), start=1):
words = len(page.get_text().split())
print(f"page {number}: {route(page):7} ({words} words)") # vision pages go to read_with_vision
# Printed by PyMuPDF 1.28.2 alone for the test PDF; read_with_vision needs
# an API key and was not run here.
# page 1: text (25 words)
# page 2: layout (14 words)
# page 3: vision (0 words)
The router sent page 1 to plain extraction, though its text came back with lines from both columns taking turns. Counting words and tables cannot see column order. Real files vary with how they were written, which is why the check below stays. The layout branch only labels a page; hand those pages to a layout-aware parser.
Or skip parsing. For pages whose meaning is in a chart or table layout, you can embed a picture of the page and extract no text. It costs about a thousand small vectors per page and, at answer time, a generator that can read images. One benchmark, ViDoRe V3 (an arXiv preprint, January 2026), found page-image retrievers ahead of text embedders, yet a parsed-text pipeline with a strong reranker still beat the best image retriever alone, and pages mixing text and pictures were hardest. So test it as a route, not a replacement. Part 3 covers it in Retrieving pages as images; the reranker is in Reranking.
When not to use the expensive tiers. If your sources are born-digital single-column text, such as wiki exports, markdown and HTML, plain extraction plus a look at fifty chunks is the whole job. A vision model there adds cost and a new way to be wrong: it can misread a number a text layer would have copied exactly. If the corpus is mostly slides and charts, parsing is the wrong fight.
Keep one habit for the rest of the course. Before you touch chunk size, the embedding model or the reranker, read fifty of the chunks your parser produced. A misplaced number, a table with no headers or a paragraph that starts mid-sentence with words from another column is a parsing bug, and no retrieval tuning will ever see the correct document. The habit has a limit: it shows what the parser got wrong, not what it dropped, so keep the source file open beside the chunks.
WHAT YOU TAKE AWAY
- Read the raw extracted text before blaming chunking or the embedding model.
- Match the tool to the document: plain extraction, layout-aware parsing, OCR or a vision model.
- Convert every table to markdown or JSON before it reaches the chunker.
- Route page by page, so only the pages that fail a cheap check reach the expensive reader.
- Treat garbled input as a parsing bug, not a retrieval bug - no downstream tuning fixes it.
RECALL NO SCROLLING BACK
A two-column PDF page extracts as one interleaved paragraph, half of one column then half of the other. Why?
A pricing table is extracted as a stack of separate values, with the header words first. What is actually lost?
When should you reach for OCR rather than a layout-aware parser?
Retrieval on your corpus looks bad. What should you check before touching chunk size or the embedding model?
A corpus has 10,000 pages, and a cheap word-and-image count flags 600 as scans. What is the sensible use of a vision model?