Document Chunking for RAG

Four deterministic strategies. Blocks are never split, tables keep their contents, and every chunk knows where it came from.

Chunking Decides What Is Findable

An embedding has a finite window. Text past it is not represented at all — not ranked lower, not weakly matched. Absent. Whatever falls outside the window cannot be retrieved by any query, no matter how well the rest of the pipeline works.

That failure is quiet, which is what makes it expensive. Measured on a real 65,000-message mail archive: appending PDF attachment text to message bodies and embedding the first 3,000 characters left 14 million characters of successfully-extracted text outside every vector. The parser did its job. The text was in the output. It simply was not in any embedding, and so it was invisible to search.

AILANG Parse chunks the typed Block ADT, not raw text. Boundaries fall on real document structure — a heading, a slide, a sheet, an attachment — instead of an arbitrary character offset that lands mid-sentence or mid-table.

Four Strategies v0.27+

StrategyBoundaryBest for
Auto Picked from the document's actual composition The default when you do not want to think about it
FixedSize Token budget, with configurable overlap Flat prose with no headings to lean on
ByStructure A new chunk at each heading Contracts, reports, specs — anything with a hierarchy
BySection One chunk per slide, sheet, or attachment PPTX, XLSX, and emails with attachments

Auto inspects what the document actually contains rather than trusting its file extension: tabular and slide formats go to BySection, documents with headings go to ByStructure, and everything else falls back to FixedSize.

Why there is no BySimilarity

Semantic chunking is a reasonable idea and it is deliberately absent. It needs an AI round-trip, and a chunker that silently costs money on every document is the wrong default. Chunking here stays deterministic and free — same input, same chunks, no API key, no per-document charge.

What It Guarantees

Blocks Are Never Split

A table stays whole even when it exceeds the token budget. An oversized intact table beats two halves of one — half a payment schedule is not half as useful, it is misleading.

Tables Keep Their Contents

Chunk text uses a chunk-specific renderer rather than the summary renderer, which prints a table as the placeholder [table]. Without this, a chunk would claim to cover a payment schedule while containing none of it.

Deterministic

No model call, no sampling, no variability. The same document produces the same chunks on every run, so your index is reproducible and diffable.

Free

Chunking adds no cost to a parse. It is pure computation over blocks you already have.

The table-rendering guarantee is measurable: tables.docx went from 10 to 61 estimated tokens once cell contents were included in chunk text. The earlier number was a chunk that pointed at a table without carrying any of it.

Citation Provenance

A retrieved passage is much less useful when the reader cannot see which clause it came from. Every chunk carries the context needed to cite it:

{
  "id": 3,
  "text": "The Seller shall indemnify the Buyer against all claims...",
  "tokenCount": 184,
  "startBlockIndex": 12,
  "endBlockIndex": 15,
  "headingContext": "7. Indemnities",
  "sectionKind": "",
  "hasTable": false
}

headingContext is the nearest heading above the chunk, so a passage lifted out of a contract can be shown with the clause heading it sits under. startBlockIndex and endBlockIndex map the chunk back to the exact block range in the parsed document, and hasTable lets a retrieval layer treat tabular chunks differently — rendering them as a grid, say, rather than as a wall of prose.

Token Counts Are Estimates

Counts come from words × 1.3. There is no tokenizer in the parser and no model call — that is the trade that keeps chunking free and deterministic, and it is accurate enough to size chunks sensibly.

It is not accurate enough to sit flush against a hard model limit. If you are packing chunks into a context window with no slack, leave headroom rather than treating tokenCount as exact.

Try It

Chunking operates on a parsed document, so it composes with every format the parser supports — the same call chunks a DOCX contract, a PPTX deck, or an email with a PDF attachment.

import docparse/services/chunker (chunkDocument, Auto, ByStructure, Chunk)

-- Let the document decide
let chunks = chunkDocument(doc, Auto({maxTokens: 512, overlap: 64}))

-- Or pin a strategy for a known corpus
let chunks = chunkDocument(doc, ByStructure({maxTokens: 512, groupUnderHeadings: true}))

FAQ

How do I chunk documents for a RAG pipeline?

Parse the document, then call chunkDocument with a strategy. Because chunking runs over the typed Block ADT rather than raw text, boundaries land on real structure. It is deterministic and free — no AI call.

Does chunking split tables in half?

No. Blocks are never split. A table stays whole even when it exceeds the token budget, because an oversized intact table is more useful than two halves of one.

Are chunk token counts exact?

No — they are estimates (words × 1.3). There is no tokenizer and no model call. Leave headroom when sizing against a hard model limit.

Can I cite the source of a retrieved chunk?

Yes. Each chunk carries headingContext, sectionKind, a start and end block index, and hasTable, so a passage can be shown with the clause it came from.

Why is there no semantic (embedding-based) chunking?

It needs an AI round-trip, and a chunker that silently costs money on every document is the wrong default. Chunking stays deterministic and free.

Format Guides

Chunking runs on any parsed document. See what gets extracted first:

DOCX Parsing →  ·  PDF Parsing →  ·  Email Parsing →