PDF and Table Extraction for Production RAG in October 2026: Layout-Aware Parsing, OCR Fallbacks, and Vision Models for Scanned Pages
Most retrieval-augmented generation (RAG) failures that get blamed on the model actually start much earlier, at the moment a PDF was turned into text. A two-column page read straight across, a table flattened into a run of numbers, a scanned contract that produced no text at all: each of these quietly poisons the index, and no amount of prompt tuning will fix it later. This guide walks through a practical extraction pipeline for production RAG: how to tell what kind of PDF you have, which parser to use for each kind, how to keep tables usable, and how to check the output before it ever reaches your vector store.
Why PDFs are harder than they look
A PDF is a set of drawing instructions, not a document with paragraphs. The file says "put these glyphs at these coordinates," and it is up to the extractor to guess which glyphs form words, which words form lines, and which lines belong to the same paragraph or table cell. That guess goes wrong in predictable ways:
- Reading order. Multi-column layouts, sidebars, and pull quotes get interleaved, so a chunk ends up mixing two unrelated arguments.
- Headers and footers. Page numbers, running titles, and legal footers repeat on every page and pollute every chunk.
- Tables. Cells come out as loose tokens with no row or column structure, so "Q3 revenue" and the number next to it are no longer connected.
- Hyphenation and ligatures. Words split across lines ("retriev- al") or stored as ligature glyphs break keyword search and embeddings alike.
- No text layer. Scanned pages are just images. A plain text extractor returns an empty string, and the document silently disappears from search.
Step 1: Classify each page before you parse it
Do not run one parser over every file. Classify at the page level, because a single PDF can mix born-digital pages with scanned appendices. A simple, cheap classifier works well:
- Extract the text layer with a fast library such as PyMuPDF or pdfplumber.
- Count characters per page and compare against the page area. Very little text on a full page usually means a scan or an image-only page.
- Check for garbage: a high ratio of replacement characters, private-use Unicode, or strings with no dictionary words often means a broken font encoding, which should be treated like a scan.
- Detect likely tables by looking for ruling lines (drawn rectangles and lines) or many short, vertically aligned text spans.
Route each page into one of three lanes: clean digital text, digital with tables or complex layout, or needs OCR. Store the lane as metadata so you can audit it later.
Step 2: Use the cheapest parser that handles the page
Clean digital text
For ordinary single-column pages, a fast text-layer extractor is enough. Ask it for text blocks with coordinates rather than one big string, then sort blocks top-to-bottom and left-to-right within detected columns. Keep the font size and weight of each block: larger or bold lines are good heading candidates, and headings are what make structure-aware chunking possible.
Complex layout and tables
For multi-column pages, forms, and tables, use a layout-aware tool. Open-source options such as Docling, Unstructured, and Marker run layout detection models that label regions as title, paragraph, list, table, figure, or caption, and then emit Markdown or JSON with that structure intact. They are slower than a raw text extractor, which is exactly why you only send the pages that need them.
Scanned pages
For pages with no usable text layer, you have two broad choices:
- Classic OCR (for example Tesseract, or a cloud document OCR service). Predictable, cheap per page, and gives you word-level bounding boxes and confidence scores. Weak on complex tables and handwriting.
- Vision-language models. Send the page image to a multimodal model and ask for Markdown output. These handle messy layouts and tables well, but they can hallucinate: a model may "fix" a number it could not read, or invent a row that looks plausible. Treat the output as untrusted until it is checked.
A sensible default is OCR first, and a vision model only for pages where OCR confidence is low or a table was detected. That keeps cost bounded and limits the surface where hallucinated content can enter the index.
Step 3: Keep tables as tables
Tables are where most of the value lives in financial reports, spec sheets, and policies, and they are the easiest thing to destroy. A few rules help:
- Serialize to Markdown or HTML, not prose. A Markdown table keeps rows and columns visible to both the embedding model and the LLM that reads the retrieved chunk.
- Never split a table across chunks unless it is very large. If you must split, repeat the header row in every piece so each chunk can stand alone.
- Attach the caption and the nearest heading. "Table 4" means nothing on its own; "Table 4: Warranty periods by product line" is retrievable.
- Add a short text summary next to big numeric tables, for example a one-line description of what the table covers. Embeddings of pure numbers retrieve poorly, and the summary gives the vector something meaningful to match on. Generate the summary from the extracted table only, and keep the original table in the chunk so answers cite real values.
Here is what a well-formed table chunk might look like:
## 5.2 Warranty coverage
Table 4: Warranty periods by product line
| Product line | Parts | Labor |
|--------------|-------|-------|
| Pumps | 24 mo | 12 mo |
| Valves | 36 mo | 12 mo |
source: handbook-2026.pdf, page 41, lane: layout
Step 4: Clean the text before chunking
- Strip repeated headers and footers by finding lines that appear in the same position on most pages.
- Rejoin hyphenated words when a line ends in a hyphen and the next line starts with a lowercase letter.
- Normalize Unicode (NFKC) so ligatures and full-width characters become ordinary text.
- Keep page numbers as metadata, not in the text, so citations can point to "page 41" without that string showing up in embeddings.
Step 5: Validate extraction before indexing
Extraction errors are silent, so build checks that make them loud. These are cheap and catch most problems:
- Empty or tiny pages. Flag any page whose final text is far shorter than its neighbors. It is often a scan that skipped OCR.
- OCR confidence. Record mean word confidence per page and send low-scoring pages to a fallback lane or a review queue.
- Numeric cross-checks. For vision-model output, compare numbers against the OCR text of the same page. A number that appears in the model's table but nowhere in the OCR output deserves a flag.
- Table shape. Every row should have the same number of cells as the header. Ragged tables usually mean a merged cell or a misread column boundary.
- Spot-check sample. Render a few random pages next to their extracted Markdown each week. Ten minutes of human review catches regressions that metrics miss, especially after a parser upgrade.
Step 6: Version everything
Store the parser name, version, lane, and settings alongside each chunk. When you upgrade a layout model or switch OCR engines, you will want to re-extract only the affected lane and compare the old and new output on a fixed sample of documents before re-indexing. Keep the raw extraction output, not just the chunks, so you can re-chunk without paying to parse again.
A minimal pipeline sketch
for page in pdf.pages:
text = fast_extract(page)
lane = classify(page, text) # "digital", "layout", "ocr"
if lane == "digital":
blocks = order_blocks(text_blocks(page))
elif lane == "layout":
blocks = layout_parser(page) # regions with types
else:
blocks, conf = ocr(page)
if conf < THRESHOLD or has_table(page):
blocks = vision_to_markdown(page)
flag_for_numeric_check(page, blocks)
blocks = clean(blocks) # headers, hyphens, unicode
validate(page, blocks) # emptiness, table shape
save_raw(page, blocks, lane, parser_versions)
chunks = structure_aware_chunk(all_blocks)
Common mistakes to avoid
- Running a vision model on every page "for quality," which multiplies cost and adds hallucination risk to pages that already had perfect text.
- Throwing away coordinates, which makes column detection and citation highlighting impossible later.
- Indexing scans with empty text and never noticing, because retrieval simply never returns them.
- Letting a model summarize a table and indexing only the summary, so answers quote numbers that were never in the source.
The takeaway
Good RAG answers depend on good text, and PDFs do not hand you good text for free. Classify pages first, use the cheapest parser that fits each one, keep tables intact as Markdown with their captions, clean and validate before chunking, and version the whole pipeline so upgrades are measurable. Get extraction right and everything downstream, from chunking to reranking to citations, gets easier.
Comments
Post a Comment