Runbooks/ RAG Runbook Track A · Ingestion and chunking Deep readQuick learn
  1. 00 Start
  2. /
  3. 01 Chunking
  4. 02 Parsing
  5. 03 Identity
  6. 04 Access
  7. /
  8. 05 Models
  9. 06 Vectors
  10. 07 Limits
  11. 08 Model ops
  12. 09 Index I
  13. 10 Index II
  14. 11 Tuning
  15. 12 Capacity
  16. /
  17. 13 Sharding
  18. 14 Filtering
  19. 15 Hybrid
  20. /
  21. 16 Proof
Document 02 · Quick learn Typed blocks · The splitter never sees a table · Tier the expensive parts

Ink, notmeaning

Where enterprise RAG actually dies: a page format that records ink rather than meaning, tables that lose their labels, and twelve source systems that all want their own code path.

“Somebody photographed every page and threw away the books.”
300×
OCR against plain text extraction
~20 rows
whole table, or row-level
94%
OCR on digits = the accuracy ceiling
4,167 → 357 hrs
1M documents, tiered
Why PDFs are the hardest parta format that records ink, not meaning

A PDF is a description of where ink goes on a page. It has no concept of a paragraph, a heading, a table, or a reading order. Run a naive text extractor over it and you get characters in roughly the order they were drawn — which, for anything other than a single column, is not the order a human reads them.

The framing that lands

Chunking a PDF is a parsing problem before it is a splitting problem, and your chunking strategy is downstream of parse quality. Candidates who jump to chunk sizes have skipped the place where the failure actually happens.

In most enterprise RAG projects, more engineering time should go into this document than into everything in Track B combined
The library version

Somebody has photographed every page of every book and thrown away the books. The photographs are perfectly sharp, and they contain no information about which marks are a chapter title, which are a footnote, and which two columns are separate thoughts.

Your first job is not to catalogue the library. It is to reconstruct what the pages actually said
The pipeline that fixes itfour stages, and the third removes two failures outright
Layout detection

Reads page geometry, emits typed blocks with a reading order from the layout.

Skip it → columns interleave, and no chunk size recovers it
Strip the furniture

Detects repeated headers, footers and page numbers, moves them to metadata.

Skip it → every embedding carries the same noise and the whole corpus looks self-similar
Route by block type

Prose to the chunker, tables to table handling, figures to caption handling.

Skip it → the splitter cuts a table and a number loses its labels
Emit canonical form

One structure regardless of source format, persisted rather than passed through.

Skip it → twelve chunkers, and every future migration re-crawls the sources
The splitter never sees a table

That is the point of the router, and it is the sentence to say out loud. Two of the four classic failure modes disappear the moment block type decides the code path.

Tableswhere the numbers live, and numbers are what people ask about

Three serialisations, and the right answer is usually more than one of them at the same time.

Whole table as Markdown

Under about twenty rows.

Keeps within-table comparison possible — “how does South compare with North?”
Row-level, header repeated

Above twenty rows.

Every row carries its own labels, so a number can never arrive orphaned
A summary chunk — always

The caption, the introducing sentence, the column names.

Discovery queries match no individual row. This is the chunk they hit
WorkedLayout block → decide serialisation → chunk A (Markdown table + section path + caption) and chunk B (summary) → rows also written to a databaseand the rows go to the warehouse whether RAG needs them or not
The diagnosis worth memorising

“Users say the bot gets numbers wrong from our financial reports.” Walk the pipeline backwards, starting with what the retrieved chunk actually contains. Nine times out of ten the table was flattened into prose or split mid-way, so the number has lost its row and column labels.

That is an ingestion bug, not a model bug, and no amount of prompt work or model upgrading will fix it. Then add an ingestion test asserting specific known values are retrievable — volunteering the regression test is what separates the answer
The routing decision to state explicitly

Once tables are extracted as structured rows, you are one step from loading them into a warehouse. The mature architecture stores every table twice: as chunks for semantic lookup, and as rows in a database for aggregation, with a classifier routing aggregation-shaped queries to SQL.

Saying you would persist the structured form even though RAG does not need it shows you are thinking about the next requirement — and the aggregation requirement always arrives
Scanned documents, and the tier decisionthe errors are permanent

A scanned PDF has no text layer at all, so extraction returns nothing and you have to OCR. That is a different problem from parsing, and it has a property none of the others do.

1
Text extractionclean, single-column, table-free documents
~0.05 s / doc
2
Layout modelanything with columns, tables or real structure — 40×
~2 s / doc
3
OCRno text layer at all — 300×
~15 s / doc
Workeda 70 / 25 / 5 tier mix blends to ~1.29 s per document · 1M documents tiered is ~357 compute-hours, against ~4,167 if everything goes through OCRwhich is the whole argument for triage
OCR quality is an ingestion SLO, not a detail

If your OCR is 94 per cent accurate on numbers, your financial assistant is 94 per cent accurate at best, no matter how good the retrieval and the model are.

That ceiling is set at ingest and cannot be recovered afterwards — which is also the argument that reframes “why is parsing expensive” into “parsing is the accuracy budget”
Triage is a routing decision, not a score

Page-level confidence, the proportion of characters not in the dictionary, the ratio of digits to letters against what the document type predicts — any of these separates the obviously bad pages from the rest.

The goal is not a precise score. It is: which pages does a human need to look at?
Figures, charts and imagesfour answers with genuinely different cost profiles
Caption-based

Index the caption plus surrounding narrative, with a pointer to the image.

Cost: none
When: the baseline. Captions are usually descriptive and this covers most needs
Vision description at ingest

Send the image to a multimodal model, store its description as chunk text.

Cost: one call per figure, plus a permanent hallucination surface
When: charts whose caption says “Figure 7” and nothing else
Multimodal embeddings

Embed the image itself into the same space as the text.

Cost: another model, and a whole second evaluation problem
When: diagram-heavy corpora such as engineering documentation
Pass the image at generation

Retrieve on the caption; hand the actual image to a vision-capable model when answering.

Cost: tokens at generation time only
When: often the best quality per unit of effort
What to say when asked “how do you handle charts?”

“Baseline is caption plus surrounding text with a pointer to the image, and pass the image at generation time if the model is multimodal. I would only add vision-generated descriptions at ingest if I measured that caption-based retrieval was missing figure-related queries — because it is a model call per figure and it introduces a hallucination surface that is permanent once indexed.”

One canonical form, twelve sourcesthe abstraction that stops ingestion rotting

Confluence pages, Word documents, native and scanned PDFs, SharePoint lists, HTML exports, email, Markdown in Git, spreadsheets, chat threads, tickets. The naive path is one chunking implementation per format, and it makes ingestion the least maintainable part of the system.

Stable identity

A section path, whatever the source was.

Atomic tables

An explicit block type, so the chunker never has to guess.

ACL enforcement

Group tags attached at parse time by the connector that understands the source’s permission model.

Parent–child

A section hierarchy.

Citations

Page numbers and anchors carried on every block.

The tradeoff to name before you are asked

Normalisation is lossy. Converting a Confluence page drops macros and embedded dashboards; converting a spreadsheet drops formulas. So the canonical schema is a real design decision: too thin and you lose information you later need, too rich and every parser becomes complex.

Start deliberately thin — heading, paragraph, table, list, figure — and extend only when a measured retrieval failure traces back to something you dropped
And the failure mode to watch for

If adding a source forces a change to the chunker, the abstraction is wrong — and you are on your way back to twelve code paths.

Storing the canonical layer is text in object storage: the cheapest tier in the whole system, orders of magnitude below the RAM the vectors occupy. Against that, it removes re-crawling from every future migration
Answer it out loudthe ninety-second version, then five questions
“A PDF only records where ink went, so the first job is reconstructing what the page said. I use a layout model that emits typed blocks with a reading order from the geometry, strip the repeated headers and footers into metadata, and then route by block type — prose to the chunker, tables handled atomically, figures by caption. The splitter never sees a table. Tables get serialised three ways depending on size: whole as Markdown under about twenty rows, row-level with headers repeated above that, and always a summary chunk, because discovery queries do not match any individual row. The structured rows also go to a database, because the aggregation requirement always arrives. And I tier it: text extraction for clean documents, a layout model where there is structure, OCR only where there is no text layer — which takes a million documents from four thousand compute-hours to about three hundred and fifty.”

Five questions, and the spine of each answer

Architect

Users say the bot gets numbers wrong from our financial reports.

  • Walk the pipeline backwards, starting with what the retrieved chunk actually contains
  • Nine times out of ten the table was flattened into prose or split mid-way
  • The number has lost its row and column labels
  • That is an ingestion bug, not a model bug
No amount of prompt work or model upgrading fixes it. And I would add an ingestion test asserting specific known values are retrievable from specific documents, so it cannot regress silently.
Architect

How do you handle tables?

  • Atomically — the splitter never sees one
  • Under ~20 rows: whole table as Markdown, so within-table comparison still works
  • Above: row-level with the header repeated on every row
  • Always a summary chunk carrying the caption and column names
Plus the structured rows into a database. Persisting the structured form even though RAG does not need it is what shows you are thinking about the next requirement.
Architect

Can I just use a text extractor and accept the errors?

  • For clean, single-column, table-free documents — yes, and you should
  • It is 300× cheaper than OCR and 40× cheaper than a layout model
  • That is the whole point of tiering
  • What you cannot do is assume the corpus is that shape without checking
Because the failure is silent: text comes out, it just is not the text that was on the page.
Eng manager

How much of the team’s time should ingestion get?

  • More than anyone expects — and I would say so at planning, not discover it at week six
  • The pilot exposes parsing problems, not retrieval problems
  • The retrieval stack is largely commodity; the ingestion stack is entirely specific to your corpus
Concretely: one engineer on connectors and parsing for the whole first quarter, and I would resist the pull towards retrieval tuning until ingestion quality is measured. Tuning retrieval on badly parsed content looks like progress while optimising the wrong thing.
Eng manager

How do you justify the parsing spend on an “AI project”?

  • With the ceiling argument and one number
  • If OCR is 94% accurate on digits, the assistant is at best 94% accurate on any question about a number
  • That ceiling is set at ingest and cannot be recovered later
  • So parsing is not a preliminary to the AI work — it is the accuracy budget
Then show the tiering arithmetic, because it reframes the conversation from “why is this expensive” to “which five per cent of documents deserve the expensive path” — a decision the business can participate in.
Fast answersthe nine that come up most

QHow do I detect running heads and footers automatically?

Position plus repetition. Collect text blocks in the top and bottom bands of every page, normalise digits to a placeholder so page numbers collapse together, and drop any line appearing in the same band on more than about half the pages. Keep the page number as metadata — it is what a citation points at.

QWhat is the right row threshold for whole-table versus row-level?

Around twenty rows is a reasonable default, but treat it as configurable and be ready to justify it. It is really set by how much of the context window one chunk may occupy, and whether within-table comparison matters for this corpus.

QMarkdown or a typed block tree for the canonical form?

A block tree internally, serialised to Markdown when handing content to the model. Markdown is simple and models read it natively, but it cannot carry page numbers, anchors or block-level confidence scores — exactly what citation and OCR triage need.

QIs storing the canonical layer expensive?

No. It is text in object storage — the cheapest tier in the whole system, orders of magnitude below the RAM the vectors occupy. Against that, it removes re-crawling from every future migration. Close to the best cost-to-benefit ratio in the ingestion design.

QHow do I handle a document that is one enormous table?

Row-level chunks with the header repeated, a summary chunk, and the structured rows into a database. Then check whether this corpus should be in the RAG system at all — if most queries are lookups by key or aggregations, text-to-SQL beats semantic retrieval on both accuracy and cost.

QDo I need per-block OCR confidence if the vendor does not expose it?

Use what you can get and construct a proxy where you cannot — page-level confidence, out-of-dictionary character proportion, digit-to-letter ratio. The goal is a routing decision, not a precise score.

QOur PDFs come from Word via a known template. Does any of this apply?

Much less, and you should exploit that. A known generator means a known structure, so a template-specific extractor beats a general layout model on both accuracy and cost. Tier that source at tier 1 by configuration rather than by triage.

QHow do I test the ingestion pipeline?

Golden-output tests on a small fixed set — store the expected canonical form and diff on every change. Plus value-level assertions for the corpus-specific things that matter: “the Q2 South revenue figure is retrievable from the FY24 annual report”. Those two catch nearly everything.

QHow big should the parser gold set be?

20–30 representative documents — deliberately including the ugly ones. It is small enough to build in a day and it is what turns a parser bake-off from an opinion into a measurement.

Parsing is the accuracy budget, not a preliminary to it. If OCR is 94 per cent accurate on digits, the assistant is 94 per cent accurate at best on any question about a number — and that ceiling is set at ingest and cannot be recovered by any model, any prompt or any amount of retrieval tuning. So the pipeline is: typed blocks with a reading order, furniture stripped to metadata, routed by block type so the splitter never sees a table, emitted into one persisted canonical form. And tier the expensive parts: 70/25/5 takes a million documents from four thousand compute-hours to three hundred and fifty.

tiered · ~357 hours
all through OCR · ~4,167 hours
Where this goes next

Questions to ask them

What proportion of the corpus is PDF, and what proportion of that is scanned?

Are the PDFs generated from a known template, or arbitrary?

How important are tables — are people asking about numbers?

What parser is in place today, and has anyone measured its table fidelity?

Is the intermediate representation persisted, or is the vector store the only copy?

← 01 · Chunking Read the full chapter → 03 · Identity →