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 and a lot of them will say tables. A paragraph survives bad extraction gracefully — a jumbled sentence is still mostly readable. A table doesn’t: strip out the column alignment and you’re left with a wall of numbers and labels in an order that 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, which row belongs under which column header. 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, and the guess is often wrong: cells read top-to-bottom instead of left-to-right, headers separated from the rows they label by hundreds of characters, merged cells duplicated or dropped entirely.

The result is exactly the kind of number-soup that’s worse than an error — it looks like extracted text, gets chunked and embedded like extracted text, and only fails visibly 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, no JSON representation of cell boundaries. What Tika does well is flatten a table into that string in row-major reading order, keeping 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, so 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 genuinely structured, per-cell output — a JSON object per row, typed columns — that’s a different problem than text extraction, and it’s not something /v1/extract claims to solve. What it solves is getting the table’s content into linear text without scrambling row/column correspondence, which is 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 and 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 so 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 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.

Stop parsing. Start shipping.

Create an account and get an API key in minutes — the free Hobby plan needs no card.

Get started →