TAI BUI
Back to curriculum
CapstonePython

Chunking Strategies, Compared

Chunking sets the ceiling on what your retriever can surface. Cut in the wrong place and no embedding model, reranker, or LLM downstream can repair it.

TIME: ~90 minutesPREREQUISITES: Phase 11 lessons 04 (embeddings), 06 (RAG), 07 (advanced RAG); Phase 19 Track B foundations (lessons 20-29)

Learning Objectives

  • Implement five chunking strategies from scratch: fixed-window, sentence, recursive-split, semantic clustering, and structural markdown headers.
  • Measure recall@k on a fixture corpus with gold-labeled answer spans and read where the five strategies separate (recall@1) and where they tie (recall@3 and @5).
  • Read a chunk-length distribution and recognize the failure modes each strategy injects: orphan sentences, mid-symbol cuts, header-only chunks, semantic drift.
  • Pick a default for a new corpus without running the benchmark by inspecting two properties: document type (including whether the format carries explicit structure) and average paragraph length.

The Problem

Chunking is cutting a source document into pieces small enough for an embedding model to take and large enough that each piece carries a self-contained idea. Every RAG pipeline starts there, and where you cut is not a hyperparameter. It is the upper bound on what the retriever can ever return.

Take a query like "what does the budget abort threshold look like". It can only succeed if the chunk holding the abort threshold is reachable. Suppose a fixed-window splitter separated the threshold value from its surrounding context. The embedding lands in a different cluster, the BM25 score drops, the rerankers see noise, and the LLM generates a wrong answer. The 2024 paper "LongRAG: Enhancing Retrieval-Augmented Generation with Long-context LLMs" measured a 35 percent absolute swing in retrieval recall purely from the chunking choice. Follow-up work in 2025 on contextual chunk headers narrowed the gap but did not close it.

Here you build five strategies side by side, run them against a fixture corpus with gold-labeled answer spans, and read the recall numbers yourself.

Warm-up0/3
1. In a RAG pipeline, what does the retriever do with a user's question?Warm-up
2. Why is a long document split into pieces before it is embedded?Warm-up
3. Two embedding vectors point in nearly the same direction. What is their cosine similarity?Warm-up
Check: The Problem0/3
1. What is chunking in a RAG pipeline?Define
2. Why does the lesson say that where you cut a document is not just a hyperparameter?Explain
3. A fixed-window pipeline answers 'what is the abort threshold' wrongly. One chunk holds only the value, and the previous chunk holds the words 'abort threshold per bucket'. What is the likely diagnosis?Analyze

The Concept

Mermaid diagram

Fixed-window

Fixed-window chunking cuts every N characters. It is the brute-force baseline. You can add overlap, so a sentence cut at position N appears whole inside the chunk that starts at position N - overlap. It is fast and deterministic, and it is terrible at boundaries. I'd use it as a control, not a default.

Sentence

Sentence chunking splits on sentence boundaries, using a regex or a simple state machine, and packs one or more sentences into a chunk up to a target character budget. That ends mid-word cuts, but it still cuts mid-paragraph and mid-section. Many early RAG pipelines defaulted to it, and it is a reasonable choice for prose with no other structure.

Recursive split

Recursive split is a hierarchy strategy, popularized by 2023-era libraries. It tries the strongest separator first (double newline, paragraph), falls back to the next (single newline), then to sentences, then to spaces (word boundaries), and stops recursing once a chunk fits the budget. Because it adapts per region, it does well on documents with inconsistent structure.

Semantic clustering

Semantic clustering embeds every sentence and groups contiguous sentences that share a topic centroid. A cut happens when the running similarity to the centroid drops below a threshold, so the boundaries follow meaning rather than character counts. A hard character cap (max_chars, 900 in the code) also forces a cut when a chunk grows too long. It is slower to build and depends on the embedding model, but it holds up on documents that switch topics inside a paragraph.

Structural markdown headers

Structural chunking applies to documents with explicit structure (markdown, reStructuredText, RFC-style numbered sections) and cuts at heading boundaries. Each chunk is the heading plus everything underneath it, down to the next heading of any level. You get the smallest chunks per topic, but only when the corpus is well-formed.

How recall@k measures the boundary choice

Recall@k is the fraction of queries where any of the top-k retrieved chunks overlaps the gold answer span. A gold-labeled query carries the exact character offsets of that span inside the source document. After chunking, you check whether any of the top-k chunks the retriever returned overlaps it. If one does, recall@k for that query is 1. If none does, it is 0. Average across the query set, run the same evaluation for each strategy, and the spread shows which boundary policy survives the corpus you have.

Check: The Concept0/7
1. What does recall@k measure in this lesson?Define
2. What is chunk overlap?Define
3. How does the structural markdown strategy decide where to cut?Define
4. What is a semantic chunker?Define
5. What triggers a cut in semantic chunking?Define
6. Why does adding overlap help fixed-window chunking?Explain
7. How does recursive split adapt to a document with inconsistent structure?Explain

Build It

code/main.py implements:

  • fixed_window(doc_id, text, size, overlap) - the baseline.
  • sentence_chunks(doc_id, text, target) - simple sentence packer.
  • recursive_split(doc_id, text, target, separators) - hierarchical recursion.
  • semantic_chunks(doc_id, text, similarity_threshold, max_chars) - centroid-based clustering on top of a deterministic mock embedding.
  • structural_markdown(doc_id, text) - header-aware splitter.
  • mock_embed(text, dim) - a hash-based embedding so the loop runs offline.
  • DenseIndex - the same shape used in Phase 19 Track B's hybrid retrieval lesson.
  • eval_recall(chunker, fixture, ks) - the comparison loop.
  • A main() that runs every strategy on the fixture corpus and prints a recall@k table.

Run it:

bash
python3 code/main.py

The output is a small table with one row per strategy and one column per k, followed by chunk counts. On the fixture (three short documents, nine queries), every strategy reaches 1.00 at recall@3 and recall@5, so the separation shows up at recall@1. Recursive and structural score 1.00, fixed-window 0.89, sentence 0.78, and semantic is lowest at 0.67. Semantic also emits the most chunks (20, against 6 to 9 for the others), because the hash-based mock embedding makes neighboring sentences look dissimilar and triggers many cuts. A corpus this small cannot crown a winner, so read it as a demonstration of the harness, then run your own corpus through it.

Check: Build It0/10
1. On the fixture every strategy scores 1.00 at recall@3 and recall@5 but they differ at recall@1. What should you conclude?Explain
2. A text is 100 characters long. fixed_window runs with size=40 and overlap=10. How many chunks does it return?Apply
3. fixed_window is called with size=40 and overlap=40. What happens?Apply
4. Chunk.overlaps is called on a chunk covering offsets 10 to 20 with a gold span of 20 to 30. What does it return and why?Apply
5. split_sentences runs on 'One is first. two is lower. Three here! 4 now.' How many sentences does it return?Apply
6. recursive_split runs with target=500 on a text of three paragraphs separated by blank lines. Each paragraph is under 310 characters, and any two adjacent paragraphs together exceed 500. How many chunks result?Apply
7. structural_markdown runs on '# A\n\n## B\n\ntext'. What does it return?Apply
8. semantic_chunks runs with similarity_threshold=-1 and max_chars=900 on a document whose sentences total fewer than 900 characters. How many chunks result?Apply
9. In the Build It run on the fixture, which strategy has the lowest recall@1?Apply
10. Semantic chunking scores the lowest recall@1 and emits 20 chunks, the most on the fixture. Which explanation does the lesson give?Analyze

Failure modes the table will not hide

Orphan sentences. Sentence packing can produce chunks that miss the topic sentence, and the embedding then points at the wrong cluster.

Mid-symbol cuts. Fixed-window inside code or YAML splits an identifier in half, and the two halves embed to noise.

Header-only chunks. Structural markdown can emit a chunk containing nothing but ## Title. Filter those out, or attach the next chunk's first paragraph.

Semantic drift. Semantic clustering under-cuts when the corpus is uniformly on topic. A 5000-character chunk packs many specific answers into one diffuse embedding. Pair semantic with a hard character cap (the code defaults to max_chars=900).

Stale embeddings. Semantic clustering uses an embedding model, so changing the model changes the chunks. Pin the chunk model separately from the retrieval model, or rebuild the index together.

Check: Failure modes the table will not hide0/5
1. Why does fixed-window chunking fail badly on code or YAML?Explain
2. What stale-embedding problem does semantic chunking add that the other four strategies avoid?Explain
3. A sentence-packed chunk begins mid-paragraph and lacks the topic sentence, and retrieval for that topic points at a different cluster. Which failure mode is this?Analyze
4. A structural markdown index contains several chunks that hold only a line such as '## Title'. What does the lesson suggest?Analyze
5. A semantic chunker on a uniformly on-topic corpus emits 5000-character chunks and retrieval turns vague. What fixes it?Analyze

Choosing a default without running the benchmark

Two properties decide the default chunker for a new corpus: the document type and the paragraph length.

PropertyValueDefault
Document typeProse with no structureRecursive split, target 500
Document typeMarkdown / RFC / API docsStructural markdown
Document typeCodeAST-aware (out of scope; see Phase 19 lesson 02)
Paragraph lengthLong, single topicSentence, target 500
Paragraph lengthShort, mixed topicsSemantic, threshold 0.55

When in doubt, pick recursive split. It is the strongest single-strategy baseline.

Check: Choosing a default without running the benchmark0/3
1. A new corpus is API docs in markdown with consistent ## headings. Which default does the lesson's table pick?Design
2. You inherit an unfamiliar corpus and have no time to run the benchmark. Which starting point does the lesson back?Design
3. Your corpus is source code. Which row of the lesson's table applies, and why not a character-window splitter?Design

Use It

Production patterns:

  • Run the eval before you ship a new pipeline, and don't trust the strategy your library defaults to.
  • Re-run the eval whenever the embedding model or the corpus mix changes, since the winner is corpus-dependent.
  • Persist the strategy name in each chunk's metadata so you can attribute regressions later.
Check: Use It0/2
1. Why should the chunking eval be re-run when the corpus mix changes, instead of trusting the library default?Explain
2. You will swap the embedding model under a semantic chunker and want regressions attributable later. Which plan fits the lesson?Design

Ship It

The Track F end-to-end RAG system in lesson 69 uses the chunker you select here as its first stage. The eval harness in lesson 68 reads recall@k from the same shape that eval_recall returns here. Pick the strategy that wins on your corpus and feed it forward.

Exercises

  1. Add a sixth strategy: token-window using tiktoken instead of character counts. Compare it against fixed-window on the same fixture.
  2. Inject a 30 percent fraction of code blocks into the prose fixture and re-run the table. Explain why every strategy except structural markdown loses recall.
  3. Replace the deterministic embedding with the one from your project's real provider. Measure the semantic-clustering recall delta, and report whether the spread between strategies widens or narrows.
  4. Add a summary field per chunk: a one-sentence centroid description. Re-run the eval with the summary appended to the chunk body, and measure the recall lift.

Key Terms

TermWhat people sayWhat it actually means
Recall@k"Did we get the right chunk?"Fraction of queries where any of the top-k chunks overlaps the gold answer span
Chunk overlap"Sliding window"Re-include the last N characters of the previous chunk in the next chunk
Structural splitter"Header-aware chunks"Cut at H1-H6 boundaries; the heading text is part of the chunk
Semantic chunker"Topic-aware chunks"Embed sentences, cluster by centroid similarity, cut on drift
Centroid drift"Topic shift"Cosine similarity between the running mean and the next sentence drops past a threshold
Author's Note

On a corpus I haven't seen, I'd start with recursive split and treat fixed-window as the control. My bet is that when retrieval misses, the boundary policy deserves the first look, ahead of the embedding model or the reranker, because a cut in the wrong place caps what everything after it can return.

I'd also persist the strategy name in each chunk's metadata from day one, so a regression can be attributed later. And if I ever changed the embedding model under a semantic chunker, I'd rebuild the chunks and the index together rather than assume the old boundaries still hold.

Further Reading

Code