Extracting tables from PDFs and spreadsheets for RAG
Ask any RAG pipeline builder which content type causes the most retrieval failures. A lot of them will say tables. A paragraph survives bad extraction gracefully. A jumbled sentence is still mostly readable. A table doesn’t survive as well. Strip out the column alignment and you’re left with a wall of numbers and labels. The order means nothing without the grid they came from.
Why tables break naive extraction
A table’s meaning lives entirely in its two-dimensional layout. Which cell is in which row, and which row belongs under which column header, both matter. Most “extract the text” libraries only understand one-dimensional text: a stream of characters in reading order. Fed a table, they have to guess how to flatten two dimensions into one. The guess is often wrong. Cells read top-to-bottom instead of left-to-right. Headers get separated from the rows they label by hundreds of characters. Merged cells get duplicated or dropped entirely.
The result is number-soup, and that’s worse than an outright error. It looks like extracted text. It gets chunked and embedded like extracted text. It only fails visibly later. That happens when someone asks a question whose answer was in row 14 of a table that got scrambled into row 3’s neighborhood.
What extraction for a table means here
Be careful with what txtfetch does and does not give you. It depends on the file format and on the output you ask for. We tested one small table in seven formats. Tables has the raw responses.
The default output is extracted_text. It has no table in it. A Word or
HTML table comes back with one cell on each line. An Excel or PowerPoint
table comes back as tab-separated rows.
Add format=markdown and an HTML, Word, Excel, PowerPoint or ODT table
comes back as a Markdown pipe table. A merged ODT cell is the one exception.
That table comes back as HTML with a colspan. Add format=json and the same table
comes back as a table element with a cells grid. Each cell holds its text,
its colspan and its rowspan.
curl -X POST "https://api.txtfetch.com/v1/extract?format=markdown" \
-H "Authorization: Bearer $TXTFETCH_KEY" \
-F file=@tables-plain.docx
{
"status": "success",
"markdown": "Sales by quarter.\n\n| Region | Q1 | Q2 |\n| --- | --- | --- |\n| North | 1.5 | 2.5 |\n| South | 1.0 | 2.0 |\n"
}
A PDF is different. In our test, the PDF table came back as one line for each
row, with the values split by spaces. The Markdown output had no pipe table.
The JSON output had no table element. This is that response, without its metadata field:
{
"status": "success",
"extracted_text": "\nSales by quarter.\n\nRegion Q1 Q2\n\nNorth 1.5 2.5\n\nSouth 1.0 2.0\n\n\n"
}
The same holds for a table in a scan. No cell model exists in the file. Your pipeline needs a tool that reads the page layout for those tables.
Spreadsheets give the best result. An Excel file comes back as a pipe table under a heading for each sheet. Fetch one from a URL:
curl -X POST "https://api.txtfetch.com/v1/extract?format=markdown&url=https://example.com/models/forecast.xlsx" \
-H "Authorization: Bearer $TXTFETCH_KEY"
Merged cells
Check merged cells before you trust a table. Excel and PowerPoint keep every
value in its column. ODT keeps the merge as a colspan. HTML and Word lose
it. The cells after the merge move one column left. In Markdown, that puts a
number under the wrong header. The cells grid in format=json shows the
problem. A row with a lost merge has fewer cells than the header row. The
Tables page has a short script that finds those rows.
Chunking tabular text so retrieval survives
Once a table is text, the chunking decision matters more than usual. Splitting a table in the middle of its rows, the same fixed-size cut you’d apply to prose, divorces a row from its header. That makes the chunk unanswerable on its own. Two things help:
- Keep small tables whole. A table under a few hundred tokens should usually be its own chunk rather than getting split, so the header stays attached to every row.
- Repeat the header row into each chunk of a large table. For tables too big to keep whole, prefix every chunk with the header row’s text. That way each chunk is self-describing, instead of assuming the header survived from a previous chunk.
See the chunking strategies guide for the general reasoning. Tables are the case where getting this specific decision right matters most.
Tables are common enough inside Office documents that it’s also worth reading the DOCX/PPTX/XLSX parsing guide. Read it if spreadsheets and embedded tables inside Word docs are a big share of your document set.
Wrestling with a document set that’s mostly tabular? Get in touch and we’ll set you up with an API key.