Your RAG system can only find what you chunked well. Learn fixed-size, recursive, structure-aware, semantic and parent-document chunking, with real numbers and Python.
Intro
The Answer Was in the Document
Your RAG bot has the answer sitting in its database, word for word, and still replies that it does not know. Nothing is wrong with the model, the embeddings, or the index. The document was simply cut in the wrong place. Today we fix that, because chunking quietly decides how good your whole system can be.
- Answer exists in the corpus
- Bot says: I don't know
- Culprit: bad chunk boundaries
Where We Are in the Series
In the last episode we built the full RAG architecture: embed the question, retrieve the nearest pieces of text, and hand them to the model as context. That diagram had a box labelled chunks, and we skipped over how it gets filled. In this lesson we open that box and look at every common way to fill it.
- Last time: RAG architecture
- Today: the chunks box
- Strategies, trade-offs, code
What You Will Learn
We will cover fixed-size, sentence, recursive, structure-aware and semantic chunking, plus parent-document retrieval and contextual headers. We will do a real counting example with a fifty-page document, discuss tables, PDFs and OCR, and then implement three chunkers in Python and compare them. We finish with the mistakes that cause most failures.
- Six core strategies
- Worked numeric example
- Python comparison
- Common mistakes
Why Chunking Matters
The Retrieval Unit
A vector index does not store documents, it stores chunks. Each chunk becomes one vector, and retrieval can only return whole chunks. So the chunk is the retrieval unit, and whatever you put inside it is the most the model can ever receive from that hit. The unit you choose determines what is findable.
- One chunk, one vector
- Retrieval returns whole chunks
- Chunk = unit of findability
Too Big: Blurry Vectors
Imagine a chunk that covers three topics: refunds, shipping and warranty. Its embedding is a blend of all three, an average that sits between topics and matches none of them sharply. A question about shipping then scores only moderately against it. Big chunks dilute meaning, and they also waste context tokens on irrelevant text.
- Mixed topics blur the vector
- Weak similarity to any query
- Wasted prompt tokens
Too Small: Missing Context
Now the opposite. Cut so small that a chunk reads, it must be returned within thirty days. Returned what? Within which policy? The pronoun lost its noun, and the chunk is precise but useless. Tiny chunks lose context, so the model gets a fragment it cannot interpret.
- Pronouns lose their referents
- Precise but uninformative
- Fragments confuse the model
Fixed-Size and Sentence Splitting
Fixed-Size Chunking
The simplest strategy is fixed-size chunking. Pick a size, say five hundred tokens, and slice the text every five hundred tokens regardless of content. It is fast, predictable, and trivial to implement. Its weakness is that it is blind to meaning, so cuts land wherever the counter happens to hit the limit.
- Cut every N tokens
- Simple and predictable
- Blind to meaning
Why Overlap Helps
To soften bad cuts, we add overlap. With chunk size five hundred and overlap fifty, each new chunk starts fifty tokens before the previous one ended. A sentence cut at one boundary then appears whole in the neighbouring chunk. Overlap is cheap insurance, but it duplicates text, which costs storage and embedding compute.
- Overlap repeats boundary text
- Chunk 500, overlap 50
- Cost: duplicated tokens
Worked Example: Counting Chunks
Take a fifty-page document at roughly five hundred tokens per page, so twenty-five thousand tokens. With no overlap, five hundred token chunks give exactly fifty chunks. With overlap fifty, each step advances four hundred fifty tokens. We need the first chunk plus enough steps to cover the remaining twenty-four thousand five hundred tokens.
- 50 pages, about 25,000 tokens
- Stride = 500 minus 50 = 450
- Remaining after first: 24,500
Finishing the Count
Twenty-four thousand five hundred divided by four hundred fifty is about fifty-four point four, and we round up to fifty-five more steps. Add the first chunk and we get fifty-six chunks. That is twenty-eight thousand tokens to embed, about twelve percent more than the original twenty-five thousand. The formula is ceiling of total minus size over stride, plus one.
- 24,500 / 450 ≈ 54.4, round up to 55
- 55 + 1 = 56 chunks
- 28,000 tokens embedded, about 12% extra
Sentence and Paragraph Splitting
A smarter baseline respects natural boundaries. Split into sentences or paragraphs first, then pack whole sentences into a chunk until adding another would exceed the limit. Now no sentence is ever cut in half. Chunk sizes vary a little, but each chunk reads as coherent text, which embedding models handle far better.
- Split at sentence boundaries
- Pack sentences up to the limit
- Chunks stay readable
Recursive and Structure-Aware
Recursive Character Splitting
Recursive splitting is the popular default in many libraries. You give it an ordered list of separators: double newline, single newline, space, then empty string. It tries the biggest separator first. If a piece is still too large, it recurses into that piece using the next separator. Paragraphs are kept whole when they fit.
- Separators: paragraph, line, space, char
- Try the coarsest first
- Recurse only on oversized pieces
Why Recursive Works Well
The result is chunks that follow the document's own shape as closely as the size limit allows. Most cuts land at paragraph breaks, some at line breaks, and word-level cuts happen only as a last resort. It needs no model and no tuning beyond size and overlap, so it is a strong first choice for plain prose.
- Respects natural breaks
- No model needed
- Strong default for prose
Structure-Aware: Headings
Many documents carry explicit structure. In Markdown, headings mark sections, and a section is usually a self-contained idea. A heading-aware chunker splits at headings, keeps each section together, and attaches the heading path, like Billing then Refunds, as metadata. Only oversized sections get split further, using a recursive pass inside them.
- Split on heading boundaries
- Keep the heading path as metadata
- Subdivide only long sections
HTML and Code by AST
HTML offers the same signal through tags: headings, lists, tables and paragraphs. For source code, splitting by lines is risky because it can cut a function in half. Better is parsing code into an abstract syntax tree and chunking by function or class, so each chunk is a complete, meaningful unit with its name and signature.
- HTML tags reveal structure
- Code: chunk by function or class
- Parse with an AST
Structure Beats Guessing
When structure exists, use it. Authors already told you where ideas begin and end, and ignoring that to cut by token count throws information away. The practical rule is to parse structure first, then enforce size limits second. Size limits protect the embedding model, but structure protects meaning.
- Structure first, size second
- Authors mark idea boundaries
- Size limits protect the model
Semantic and Parent-Document
Semantic Chunking
Semantic chunking lets the content choose the boundaries. Embed each sentence, then compare neighbouring sentences with cosine similarity. While similarity stays high, the topic continues. When it drops sharply, you have likely crossed a topic change, so you start a new chunk there. It is the same cosine similarity from our embeddings lessons, used as a cutting tool.
- Embed every sentence
- Compare neighbours by cosine
- Cut where similarity drops
Semantic Chunking: Worked Numbers
Suppose six sentences give neighbour similarities of zero point eight two, zero point eight, zero point four one, zero point seven eight, and zero point seven nine. With a threshold of zero point six, only the third value falls below it. So we cut once, between sentences three and four, giving two chunks. Thresholds are often set from a percentile of the observed drops.
- Similarities: 0.82, 0.80, 0.41, 0.78, 0.79
- Threshold: 0.6
- One cut after sentence 3
Semantic Caveats
Semantic chunking costs an embedding call per sentence at indexing time, and the results can be unstable, with chunk sizes ranging wildly. Studies and practitioner reports on its benefit over simple recursive splitting are mixed, so do not assume it wins. Test it on your own data, and add minimum and maximum size limits.
- Extra indexing cost
- Unpredictable chunk sizes
- Benefit is data-dependent
Small-to-Big Retrieval
Here is a trick that resolves the size dilemma. Small chunks match queries precisely, but large chunks give the model context. So index small child chunks, say two hundred tokens, but link each to a larger parent, like its full section. Search finds the child, and you send the parent to the model. This is parent-document, or small-to-big, retrieval.
- Embed small children
- Return the larger parent
- Precision plus context
Contextual Headers and Summaries
Another fix for lost context is to enrich each chunk before embedding it. Prepend the document title, the section path, and sometimes a one or two sentence summary of where the chunk sits. Then it returned within thirty days becomes part of the Acme Electronics refund policy, and it embeds far more usefully. Generating the summary with an LLM adds indexing cost, so measure the gain.
- Prepend title and section path
- Optional LLM-written context line
- Embed the enriched text
Size Trade-offs and Math
The Size Dial
Chunk size is a dial with four forces pulling on it. Smaller chunks raise precision but can hurt recall of answers spread across passages. Larger chunks carry more context but dilute embeddings. The embedding model has a token limit, and the language model has a context budget that you pay for on every call.
- Precision versus recall
- Embedding model token limit
- LLM context cost
Embedding Token Limits
Every embedding model has a maximum input length. Anything beyond that limit is truncated by many systems, so the tail of an oversized chunk is never represented in its vector. Always check your model's limit and count tokens with that model's own tokenizer, because characters and words are only rough guides.
- Check the model's max tokens
- Overflow is truncated
- Count with the real tokenizer
Context Cost of Retrieval
Now the cost side. If you retrieve the top five chunks of five hundred tokens, that is twenty-five hundred tokens of context per question. Move to two-thousand-token chunks and the same top five becomes ten thousand tokens. At thousands of questions a day, that difference shows up in your bill and in latency, as we will study in the latency and cost lesson.
- 5 x 500 = 2,500 tokens
- 5 x 2,000 = 10,000 tokens
- 4x more cost per question
Choosing a Starting Point
There is no universal best size. A reasonable starting range for prose is a few hundred tokens, often between two hundred fifty and eight hundred, with ten to fifteen percent overlap. Treat that as a hypothesis, not a rule. Factual lookup questions favour smaller chunks, while questions needing explanation favour larger ones.
- Start: a few hundred tokens
- Overlap about 10 to 15 percent
- Tune on real questions
Overlap Is Not Free
Back to our fifty-page example. Overlap of fifty gave fifty-six chunks. Raise overlap to two hundred and the stride drops to three hundred, so we need ceiling of twenty-four thousand five hundred over three hundred, which is eighty-two, plus one, giving eighty-three chunks. That is forty-one thousand five hundred tokens embedded. Heavy overlap also returns near-duplicate results.
- Overlap 200, stride 300
- 82 + 1 = 83 chunks
- 41,500 tokens, near-duplicate hits
Messy Documents
Tables Break Naive Chunkers
Tables are where chunkers fail loudly. A row cut away from its header row is just numbers with no meaning. The fix is to keep tables whole when they fit, or split by rows and repeat the header in every piece. Often it helps to convert the table to Markdown or add a sentence describing what it shows.
- Never cut a table mid-row
- Repeat header row in each piece
- Add a short description
PDFs and Layout
A PDF is a drawing format, not a text format. Extraction can scramble multi-column layouts, interleaving left and right columns, and headers, footers and page numbers leak into the body text. Clean those out, use a layout-aware parser, and inspect the extracted text by eye before you chunk. Garbage in, garbage chunks.
- PDF stores positions, not flow
- Columns can interleave
- Strip headers, footers, page numbers
OCR Pitfalls
Scanned documents need optical character recognition, and OCR makes errors: the letter l becomes the digit one, and broken words appear at line ends. Those errors change tokens and weaken matching. Spot-check samples, repair hyphenated line breaks, record confidence where the tool provides it, and always keep page numbers as metadata so answers can cite the source page.
- OCR confuses similar glyphs
- Fix hyphenated line breaks
- Keep page numbers as metadata
Code and Comparison
The Sample Text
Now let us code. Use plain Python and a small Markdown sample with a title, two headed sections, and a few paragraphs about a refund policy. To keep it simple, we approximate tokens by words, though in production you should use the real tokenizer. We will write three functions and compare how each one cuts the same text.
- Small Markdown sample
- Tokens approximated by words
- Three chunkers, one text
Fixed Chunker in Code
The fixed chunker converts text to a list of words, then loops with a step equal to size minus overlap. At each position it takes a slice of size words and joins them back into a string. That is only a few lines, and it matches our stride formula exactly. Note that it will happily cut in the middle of a sentence.
- words = text.split()
- step = size - overlap
- slice words[i : i + size]
Recursive and Heading-Aware Chunkers
The recursive chunker takes a list of separators. If the text fits, return it. Otherwise split on the first separator, then pack the pieces greedily, and recurse into any piece still too long using the next separator. The heading-aware chunker uses a regular expression to split before each line starting with hash marks, tracks the current heading path, and stores it as metadata.
- Recursive: separators list, greedy pack
- Heading-aware: regex on # lines
- Store heading path as metadata
Comparing the Outputs
Run all three on the sample and print each chunk. The fixed chunker produces equal sizes but breaks a sentence about the thirty-day window across two chunks. The recursive chunker mostly ends at paragraph breaks. The heading-aware chunker gives one chunk per section, each tagged with its path, though one section is much longer than the others.
- Fixed: equal size, broken sentence
- Recursive: paragraph-aligned
- Heading-aware: tagged, uneven size
Mistakes and Evaluation
Common Mistakes
Here are the mistakes to avoid. Splitting mid-sentence or mid-table. Throwing away metadata like source, page, section and date, which you need for filtering and citations. Using one chunk size for every document type, when a legal contract, a chat log and a code repository each deserve their own strategy. And never looking at your actual chunks.
- Mid-sentence or mid-table cuts
- Losing metadata
- One size for all documents
- Never inspecting chunks
Evaluate With Hit-Rate
How do you know a strategy is better? Write a set of real questions and mark which source passage answers each one. Build the index with each chunker, retrieve the top five for every question, and count how often the right passage appears. That fraction is the hit rate at five. Change only one thing at a time, and the winner on your data is your answer.
- Questions with known source passages
- Retrieve top 5 per chunker
- Hit rate = hits / questions
A Practical Recipe
Put it together. Parse structure first. Split long sections recursively with modest overlap. Attach heading paths and source metadata. Consider small-to-big retrieval for context. Then measure hit rate on real questions and iterate. Complete evaluation of retrieval and generation comes in a later episode, but even this quick check beats guessing.
- Parse, split, attach metadata
- Consider small-to-big
- Measure, then iterate
Recap
To recap: the chunk is the retrieval unit, so it limits what can be found. Fixed-size is simple, sentence and recursive splitting respect text, structure-aware uses headings and syntax trees, and semantic chunking cuts at topic shifts. Parent-document retrieval and contextual headers restore context. A fifty-page document at five hundred tokens with fifty overlap gives fifty-six chunks. Tables, PDFs and OCR need care.
- Chunk = retrieval unit
- Strategies and trade-offs
- 50 pages gives 56 chunks
Next: Retrieval and Query Rewriting
Good chunks are only half the story, because the user's question is often vague or phrased nothing like your documents. In the next episode we cover retrieval and query rewriting: how to turn messy questions into queries that find the right chunks.
- Next: retrieval and query rewriting
- Messy questions to good queries
- Subscribe for episode 17