Extracting Tables for RAG

AIRAGTablesDocument Processing
Share on LinkedIn

Your pricing RAG returned "Starter plan: $29/mo" from a chunk that accidentally merged the Starter row with the Enterprise footnote about annual billing discounts. The table was in the PDF; the chunker treated it as plain text and sliced through row boundaries. Tables are structured data wearing plain-text clothing — standard RAG chunking undresses them into useless fragments.

How tables fail in RAG pipelines

Common failure modes:

Each produces chunks that embed poorly and generate wrong answers.

Table extraction pipeline

import camelot

def extract_tables_from_pdf(pdf_path: str) -> list[dict]:
    tables = camelot.read_pdf(pdf_path, pages="all", flavor="lattice")
    extracted = []

    for i, table in enumerate(tables):
        df = table.df
        # Set first row as header if it looks like headers
        if is_header_row(df.iloc[0]):
            df.columns = df.iloc[0]
            df = df[1:]

        extracted.append({
            "table_id": f"{pdf_path}:table-{i}",
            "page": table.page,
            "markdown": df.to_markdown(index=False),
            "json": df.to_dict(orient="records"),
            "source": pdf_path,
        })

    return extracted

Validate extraction on a sample — PDF table extraction is imperfect, especially for borderless tables and scanned documents.

Chunking strategies for tables

Whole-table chunking — small tables (under 500 tokens) index as a single chunk with markdown formatting:

| Plan     | Price  | Uptime SLA |
|----------|--------|------------|
| Starter  | $29/mo | 99.5%      |
| Pro      | $99/mo | 99.9%      |
| Enterprise | Custom | 99.99%   |

Row-level chunking — wide or long tables split by row, with headers prepended to each row:

def row_chunks(table: dict) -> list[str]:
    headers = table["columns"]
    chunks = []
    for row in table["rows"]:
        text = " | ".join(f"{h}: {v}" for h, v in zip(headers, row))
        chunks.append(f"Table: {table['title']}\n{text}")
    return chunks

"Plan: Enterprise | Price: Custom | Uptime SLA: 99.99%" embeds and retrieves much better than a bare "99.99%."

Summary + detail — generate an LLM summary of the table for broad queries, keep row chunks for specific lookups:

summary = llm.generate(f"Summarize this table in 2-3 sentences:\n{table['markdown']}")
# Index summary as parent, rows as children (parent-document pattern)

Embedding and retrieval for tables

Tables benefit from hybrid search:

def search_tables(query: str, filters: dict = None):
    bm25_results = bm25_search(query, filter={**filters, "chunk_type": "table"})
    vector_results = vector_search(embed(query), filter={**filters, "chunk_type": "table"})
    return reciprocal_rank_fusion([bm25_results, vector_results])

Tag table chunks with chunk_type: table and the table's section title as metadata for filtered search.

Handling complex table formats

Multi-page tables — repeat header row on every page during extraction. Most PDF extractors do not do this automatically; post-process to inject headers.

Nested tables — flatten to a single level or extract as separate tables with parent-child metadata linking them.

Tables with footnotes — attach footnotes to the relevant row chunk, not as a separate floating chunk.

Scanned PDFs — run OCR with table detection (Azure Document Intelligence, Google Document AI) before standard extraction. Quality varies; human-review a sample.

from azure.ai.formrecognizer import DocumentAnalysisClient

def extract_with_azure(pdf_bytes: bytes):
    poller = client.begin_analyze_document("prebuilt-layout", pdf_bytes)
    result = poller.result()
    for table in result.tables:
        rows = extract_rows_with_headers(table)
        yield format_as_markdown(rows)

Generation with table context

When retrieved chunks contain tables, instruct the LLM to preserve tabular formatting in answers:

When answering from table data, present comparisons in table format.
Cite the source table by name. Do not interpolate values not present
in the retrieved table.

This reduces the model inventing prices or SLA numbers not in the source data.

Evaluating table retrieval

Build eval questions specifically about tabular data:

Measure separately from prose retrieval. Table extraction quality is the ceiling — no retrieval strategy compensates for garbled extraction input.

Common production mistakes

Teams get table extraction documents wrong in predictable ways:

RAG pipelines for table extraction documents degrade when chunk boundaries split tables, embeddings go stale after doc updates, and retrieval metrics are measured offline only. Re-index incrementally and monitor answer faithfulness on live traffic samples.

Debugging and triage workflow

When table extraction documents misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

Why do tables break standard RAG chunking?

Fixed-size text splitters cut through table rows and columns, producing chunks with partial headers, orphaned data cells, and no column context. Embedding a fragment like '99.99%' is meaningless without knowing it is the Enterprise plan uptime SLA. Tables need extraction as structured units before chunking, not blind text splitting after the fact.

What is the best format to store extracted tables for RAG?

Markdown tables preserve structure in a format LLMs understand well and embed reasonably. For wide tables, store row-level chunks with column headers repeated in each row's text. JSON or CSV works when you need programmatic access or metadata filtering by column values. Many teams store both — markdown for generation context and JSON for filtered retrieval.

Which tools extract tables from PDFs accurately?

Unstructured.io, Docling (IBM), and Camelot handle most digital PDFs well. Scanned PDFs need OCR first — Azure Document Intelligence and Google Document AI include table detection in their OCR pipelines. Tabula works for simple digital PDFs with clear table borders. Always validate extraction quality on a sample before indexing an entire corpus.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →