If you feed a PDF with tables to a RAG pipeline through plain text extraction, the numbers survive and the structure doesn't. I tested PDF table extraction for RAG on five PDFs I generated with known contents, comparing PyMuPDF's plain text, PyMuPDF's find_tables() and Docling. Both table extractors recovered almost every row. The plain text recovered none intact.
Everything below was run on 2026-10-30 (Python 3.14, macOS, CPU only). My PDFs are clean, born-digital and made by reportlab, which is the best case for every tool. Treat the scores as a floor for "does it work at all", not as a ranking for your messy scans.
Why tables break RAG
Here is what PyMuPDF's page.get_text() gives for a simple pricing table:
Plan
Seats
Storage (GB)
Price (USD/mo)
Free
1
5
0
Starter
5
100
19
Every cell is on its own line. A chunker will cut this somewhere, and the chunk with "Starter 5 100 19" no longer says which number is seats and which is price. The model then answers confidently from numbers it can't place. If your RAG bot gets pricing or spec questions wrong, this is a prime suspect. The chunking comparison covers general splitting; tables need extraction before splitting.
The five test PDFs
I generated each with a known truth table so scoring is mechanical. A row counts as recovered if an extracted row has the same non-empty cells in the same order, ignoring whitespace differences.
| File | What it stresses | Truth rows |
|---|---|---|
ruled.pdf | Normal grid with borders | 6 |
borderless.pdf | Aligned columns, only a rule under the header | 6 |
merged.pdf | Two-level header with a spanning cell | 5 |
multipage.pdf | 61 rows breaking across pages, repeated header | 61 |
wrapped.pdf | Cells with multi-line wrapped text | 4 |
Results
| File | get_text() rows intact | PyMuPDF find_tables, cell clipping | PyMuPDF find_tables, extract() | Docling |
|---|---|---|---|---|
| ruled | 0/6 | 6/6 | 5/6 | 6/6 |
| borderless | 0/6 | 6/6 | 5/6 | 6/6 |
| merged | 0/5 | 5/5 | 0/5 | 4/5 |
| multipage | 0/61 | 61/61 | 60/61 | 61/61 |
| wrapped | 0/4 | 4/4 | 1/4 | 4/4 |
Versions: PyMuPDF 2.0 (MuPDF 1.28.5) and Docling 2.145.0.
Three findings worth knowing before you copy anyone's snippet.
The documented extract() lost spaces in my run
The PyMuPDF docs describe Table.extract() as returning the text of each cell. In this environment it returned Storage(GB), USDper1kcalls and split decimals such as 1.20 into 120 plus a stray .. That is why its column scores low above. The same table via Table.to_markdown() was correct, and so was reading each cell's rectangle with page.get_text("text", clip=rect):
import pymupdf
doc = pymupdf.open("pricing.pdf")
for page in doc:
for table in page.find_tables().tables:
rows = [
[page.get_text("text", clip=pymupdf.Rect(c)).strip() if c else ""
for c in row.cells]
for row in table.rows
]
I only saw this on reportlab-made files with this version, so it may not affect your PDFs. The lesson stands: assert on your own sample and look at the raw output, don't trust a table's shape.
Borderless tables worked, but I made it easy
PyMuPDF's default strategy is "lines", which the docs say uses vector graphics to detect grid lines. My borderless file still had a rule under the header, which was probably enough. A table with no lines at all would need strategy="text", which the docs describe as inferring boundaries from text alignment, and I did not test that. Docling found the borderless table without any setting.
Merged headers differ in representation
For the spanning "Latency (ms)" cell, PyMuPDF put the text in the first column of the span and left the next as empty. Docling repeated the text in both columns:
['', 'Latency (ms)', 'Latency (ms)', 'Cost']
['Model', 'p50', 'p95', 'USD per 1k calls']
That is why Docling scores 4/5 there: my strict matcher saw the duplicate. Neither is wrong, but repeated text is easy to flatten into column names like Latency (ms) / p50, which is what you want for retrieval.
Multi-page tables come back as separate tables
Both tools returned two tables for the 61-row file, one per page, and the header repeated by repeatRows appeared in the second. Join them yourself and drop duplicate header rows.
Speed, install and licence
- PyMuPDF: 0.05 to 0.15 seconds per file here. The package's metadata lists it as dual licensed under GNU AGPL 3.0 or an Artifex commercial licence. If you ship it in a closed-source product, read the licence with a lawyer or buy the commercial one.
- Docling: the first conversion took 109.5 seconds, mostly loading and downloading models; later files took 0.4 to 2.2 seconds on CPU. Its docs list MIT as the licence and describe table structure as part of its PDF understanding. I did not verify which model does the table work, so I won't name it.
So Docling suits batch indexing where you can afford a warm-up. PyMuPDF suits request-time or very large volumes.
Turn tables into chunks that retrieve well
Extraction is half the job. This is a recipe, not something I measured here:
- One table, one chunk (or a split by rows with the header repeated in every piece).
- Store it as markdown, because PyMuPDF's docs say
to_markdown()is optimised for small token size for LLM and RAG feeds. - Put a caption line first: file name, page, and the nearest heading. Without it, "Pricing" and "Pricing (2024)" tables look identical to an embedder.
- Optionally add a row-sentence form: "Plan Starter: Seats 5, Storage 100 GB, Price 19 USD/mo". Untested here, but it is cheap and keeps each row self-describing.
- Keep the numbers out of any summarising step. A model that rewrites a table can silently change a value.
Then check retrieval with real questions, as in the beginner PDF RAG build, and add a keyword leg via hybrid search, because exact values like "Enterprise" or "999" are what embeddings blur. For invoices and other forms where you need fields rather than tables, see the document processing agent.
What I did not test
Scanned or rotated pages, nested tables, tables inside two-column layouts, PDFs from Word or Excel export with odd fonts, non-Latin scripts, and GPU speed. Build a ten-file sample from your own documents and rerun the scoring script idea above: define truth rows, extract, count recovered rows. That takes an hour and tells you more than my five files.



