Extracting tables from PDFs and spreadsheets for RAG

Jun 2, 2026 · 3 min read · rag, tables, pdf, office-docs

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 actually means here

It’s worth being precise about what txtfetch does and doesn’t give you. /v1/extract returns a single flat extracted_text string. There’s no structured rows/columns object, and no JSON representation of cell boundaries. What Tika does well is flatten a table into that string in row-major reading order. It keeps each row’s cells together and headers attached to the data below them, instead of scattering them across the output:

curl -X POST https://api.txtfetch.com/v1/extract \
  -H "Authorization: Bearer $TXTFETCH_KEY" \
  -F file=@quarterly-report.pdf
{
  "status": "success",
  "extracted_text": "Region\tQ1\tQ2\tQ3\nNorth America\t3.1M\t3.6M\t4.2M\nEMEA\t1.8M\t2.0M\t2.3M..."
}

Spreadsheets go through the same path. A sheet’s rows and columns come back as linear text in the same shape. An XLSX model and a PDF table both resolve to the same kind of extracted_text your pipeline already knows how to handle:

curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/models/forecast.xlsx" \
  -H "Authorization: Bearer $TXTFETCH_KEY"

If your pipeline needs structured, per-cell output, such as a JSON object per row with typed columns, that’s a different problem than text extraction. /v1/extract doesn’t claim to solve it. What it solves is getting the table’s content into linear text without scrambling row/column correspondence. That’s the part that actually breaks most pipelines.

Chunking tabular text so retrieval survives

Once a table is flattened, 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.

Try it on your own file.

The free reader runs in your browser. Nothing gets uploaded.

Open the file reader →

Get an API key →