TAI BUI
Back to curriculum
BuildPython

Chunking Strategies for RAG

Chunking configuration influences retrieval quality as much as the choice of embedding model does (Vectara, NAACL 2025). Get it wrong and no amount of reranking saves you.

TIME: ~60 minutesPREREQUISITES: Phase 5 · 14 (Information Retrieval), Phase 5 · 22 (Embedding Models)

Learning Objectives

  • Name the six strategies beyond plain fixed-size splitting: recursive, semantic, sentence, parent-document, late chunking, and contextual retrieval, and the tradeoff each one makes.
  • Implement fixed, recursive, semantic, and parent-document chunking from scratch in Python, then evaluate any strategy yourself with a recall@k harness instead of trusting a blog post's default.
  • Match chunk size to query type (factoid, analytical or multi-hop, whole-section comprehension) instead of defaulting to one size for every corpus.
  • Catch the pitfalls that quietly wreck retrieval: missing min/max size enforcement, overlap added out of habit rather than evidence, and chunks that span two documents.

The Problem

You feed a 50-page contract into a RAG system and ask it, "What is the termination clause?" The retriever hands back the cover page. Why? Because the chunker cut the document into 512-token pieces, and the termination clause sits 20 pages in, split across a page break, with no local keyword tying it to the query.

The fix isn't a better embedding model. It's chunking: how big, how much overlap, where to split, how much surrounding context to carry along.

Feb 2026 benchmarks turn up some genuine surprises:

  • Vectara's 2026 study: recursive 512-token chunking beat semantic chunking, 69% to 54% accuracy.
  • SPLADE + Mistral-8B on Natural Questions: overlap provided zero measurable benefit.
  • Context cliff: response quality drops sharply around 2,500 tokens of context.

The "obvious" answer, semantic chunking with 20% overlap and 1000-token chunks, is often wrong. This lesson builds intuition for seven strategies and tells you when to reach for each one.

Warm-up0/3
1. In a retrieval-augmented generation system, what does the retriever do?Warm-up
2. Two sentences have each been turned into an embedding vector. Which measure is the usual way to score how similar their meanings are?Warm-up
3. In information retrieval, what does recall measure?Warm-up
Check: The Problem0/2
1. What is the 'context cliff' in RAG?Define
2. A RAG system answers 'What is the termination clause?' with the cover page of a 50-page contract. The clause sits 20 pages in, split across a page break. Which change addresses the root cause?Analyze

The Concept

Chunking is the step that splits a document into smaller pieces, chunks, before you embed and index them for retrieval. Get the split wrong and nothing downstream, not a bigger embedding model, not a reranker, recovers the context that got cut off.

Five of the seven chunking strategies visualized on one passage

Fixed chunking. Split every N characters or tokens. It's the simplest baseline you can build, and it shows: chunks break mid-sentence with no regard for meaning. Good compression, bad coherence.

Recursive. LangChain's RecursiveCharacterTextSplitter tries splitting on \n\n first, then \n, then ., then space, falling back cleanly whenever a level doesn't produce a clean cut. It's the 2026 default for good reason.

Semantic. Embed each sentence, compute the cosine similarity between adjacent sentences, and split where that similarity drops below a threshold. It preserves topic coherence better than any fixed rule can, but it's slower, and without a floor it can produce tiny 40-token fragments that hurt retrieval more than they help.

Sentence. Split on sentence boundaries, either one sentence per chunk or a window of N sentences. Up to roughly 5k tokens, it matches semantic chunking's results at a fraction of the compute cost.

Parent-document. Store small child chunks for retrieval and the larger parent chunk for context, then retrieve by child and return the parent. It degrades gracefully: even a mediocre child chunk still points to a reasonable parent.

Late chunking (2024). Embed the whole document at the token level first, then pool those token embeddings into chunk embeddings afterward. Because the embedding step sees the whole document, cross-chunk context survives the split. It needs a long-context embedder (BGE-M3, Jina v3) and more compute than the alternatives.

Contextual retrieval (Anthropic, 2024). Prepend each chunk with an LLM-generated summary of where it sits in the document ("This chunk is section 3.2 of the termination clauses..."). Anthropic's own benchmark found a 35-50% retrieval improvement from this alone, though it's expensive to index: every chunk costs an extra LLM call before it ever reaches the vector store.

The rule that beats every default

Match chunk size to query type:

Query typeChunk size
Factoid ("what is the CEO's name?")256-512 tokens
Analytical / multi-hop512-1024 tokens
Whole-section comprehension1024-2048 tokens

That's NVIDIA's 2026 benchmark speaking, and the rule underneath it is simple: a chunk needs to be big enough to hold the answer plus its local context, and small enough that the retriever's top-K results stay focused on the answer instead of drowning it in surrounding noise.

Check: The Concept0/14
1. What does LangChain's RecursiveCharacterTextSplitter try, in order?Define
2. What is a chunk in a RAG pipeline?Define
3. What is overlap in chunking?Define
4. How does semantic chunking decide where to split a document?Define
5. What does parent-document retrieval do?Define
6. Your corpus has long documents with heavy cross-references between sections, and simple chunking loses that connective context. Both late chunking and contextual retrieval fit this use case; which one embeds the full document at the token level before splitting, instead of adding an LLM-written summary?Define
7. What does sentence chunking do?Define
8. Which chunk size does NVIDIA's 2026 benchmark associate with factoid queries?Define
9. Why is chunking strategy as important as the embedding model in RAG?Explain
10. Why does fixed chunking give less coherent chunks than recursive chunking?Explain
11. Why does parent-document retrieval degrade gracefully when a child chunk is mediocre?Explain
12. Why should chunks be larger for whole-section comprehension than for factoid queries?Explain
13. What tradeoff does contextual retrieval make?Explain
14. You retrieve a small, precise chunk for a query, but it's too short to actually answer the question without more surrounding text. Which pattern fixes this without simply making every chunk bigger?Analyze

Build It

Step 1: fixed and recursive chunking

python
def chunk_fixed(text, size=512, overlap=0):
    step = size - overlap
    return [text[i:i + size] for i in range(0, len(text), step)]
 
 
def chunk_recursive(text, size=512, seps=("\n\n", "\n", ". ", " ")):
    if len(text) <= size:
        return [text]
    for sep in seps:
        if sep not in text:
            continue
        parts = text.split(sep)
        chunks = []
        buf = ""
        for p in parts:
            if len(p) > size:
                if buf:
                    chunks.append(buf)
                    buf = ""
                chunks.extend(chunk_recursive(p, size=size, seps=seps[1:] or (" ",)))
                continue
            candidate = buf + sep + p if buf else p
            if len(candidate) <= size:
                buf = candidate
            else:
                if buf:
                    chunks.append(buf)
                buf = p
        if buf:
            chunks.append(buf)
        return [c for c in chunks if c.strip()]
    return chunk_fixed(text, size)

Step 2: semantic chunking

python
def chunk_semantic(text, encoder, threshold=0.6, min_chars=200, max_chars=2048):
    sentences = split_sentences(text)
    if not sentences:
        return []
    embs = encoder.encode(sentences, normalize_embeddings=True)
    chunks = [[sentences[0]]]
    for i in range(1, len(sentences)):
        sim = float(embs[i] @ embs[i - 1])
        current_len = sum(len(s) for s in chunks[-1])
        if sim < threshold and current_len >= min_chars:
            chunks.append([sentences[i]])
        else:
            chunks[-1].append(sentences[i])
 
    result = []
    for group in chunks:
        text_group = " ".join(group)
        if len(text_group) > max_chars:
            result.extend(chunk_recursive(text_group, size=max_chars))
        else:
            result.append(text_group)
    return result

Tune threshold per domain: set it too high and you get fragments, too low and everything collapses into one giant chunk.

Step 3: parent-document

python
def chunk_parent_child(text, parent_size=2048, child_size=256):
    parents = chunk_recursive(text, size=parent_size)
    mapping = []
    for p_idx, parent in enumerate(parents):
        children = chunk_recursive(parent, size=child_size)
        for child in children:
            mapping.append({"child": child, "parent_idx": p_idx, "parent": parent})
    return mapping
 
 
def retrieve_parent(child_query, mapping, encoder, top_k=3):
    child_embs = encoder.encode([m["child"] for m in mapping], normalize_embeddings=True)
    q_emb = encoder.encode([child_query], normalize_embeddings=True)[0]
    scores = child_embs @ q_emb
    top = np.argsort(-scores)[:top_k]
    seen, parents = set(), []
    for i in top:
        if mapping[i]["parent_idx"] not in seen:
            parents.append(mapping[i]["parent"])
            seen.add(mapping[i]["parent_idx"])
    return parents

Key insight: dedupe parents. Multiple children often map to the same parent, and returning all of them wastes context for no benefit.

Step 4: contextual retrieval (Anthropic pattern)

python
def contextualize_chunks(document, chunks, llm):
    context_prompts = [
        f"""<document>{document}</document>
Here is the chunk to situate: <chunk>{c}</chunk>
Write 50-100 words placing this chunk in the document's context."""
        for c in chunks
    ]
    contexts = llm.batch(context_prompts)
    return [f"{ctx}\n\n{c}" for ctx, c in zip(contexts, chunks)]

Index the contextualized chunks, and retrieval benefits from that extra surrounding signal at query time.

Step 5: evaluate

python
def recall_at_k(queries, corpus_chunks, encoder, k=5):
    chunk_embs = encoder.encode(corpus_chunks, normalize_embeddings=True)
    hits = 0
    for q_text, gold_idxs in queries:
        q_emb = encoder.encode([q_text], normalize_embeddings=True)[0]
        top = np.argsort(-(chunk_embs @ q_emb))[:k]
        if any(i in gold_idxs for i in top):
            hits += 1
    return hits / len(queries)

Always benchmark it yourself. The "best" strategy for your corpus won't necessarily match any blog post.

Check: Build It0/10
1. In the recall@k evaluation, when does a query count as a hit?Define
2. What does Anthropic's 'contextual retrieval' add to each chunk before indexing?Define
3. chunk_fixed is called with size=10 and overlap=5 on a 20-character string. What does it return?Apply
4. chunk_recursive gets a 1,000-character string with no newlines, no '. ' and no spaces, and size=512. What does it return?Apply
5. In chunk_semantic (threshold=0.6, min_chars=200), the current chunk holds 80 characters and the next sentence has similarity 0.3 to the previous sentence. What happens to that sentence?Apply
6. retrieve_parent runs with top_k=3 and the top three children have parent_idx values 4, 4 and 7. What does it return?Apply
7. contextualize_chunks is called on a document that was split into 200 chunks. How many LLM prompts does it build, and where does each context go?Apply
8. recall_at_k runs on 4 queries, and a gold chunk shows up in the top k results for exactly 3 of them. What does it return?Apply
9. A semantic chunker with a threshold tuned on one corpus now returns a few giant chunks on a new corpus. Which adjustment fits?Analyze
10. You compare recursive, semantic, and parent-document chunking on your own corpus and recursive wins, even though a blog post ranked semantic chunking first. What does this tell you?Analyze

Pitfalls

  • Chunking evaluated only on factoid queries. Multi-hop queries reward a completely different strategy. Stratify your eval set by query type, or you'll crown the wrong winner.
  • Semantic chunking without a minimum size. Left unchecked, it produces 40-token fragments that hurt retrieval more than they help. Always enforce min_tokens.
  • Overlap as cargo cult. 2026 studies find overlap often provides zero benefit while still inflating the index (20% overlap means about 25% more chunks to embed and store). Measure it, don't assume it.
  • No min/max enforcement. Chunks of 5 tokens and chunks of 5,000 tokens both break retrieval. Clamp both ends.
  • Cross-doc chunking. Never let a chunk span two documents. Chunk per document, then merge.
Check: Pitfalls0/5
1. Which finding about chunk overlap contradicts the conventional wisdom?Explain
2. What does the 'cross-doc chunking' pitfall warn against?Explain
3. Why does the lesson warn against evaluating a chunking strategy only on factoid queries?Explain
4. Your semantic chunker keeps producing 40-token fragments that never seem to match a real query. What's missing from your chunking config?Analyze
5. A team added overlap to every index out of habit. The index grew and recall@5 barely moved. What is the best conclusion?Analyze

Use It

The 2026 default stack:

SituationStrategy
First build, unknown corpusRecursive, 512 tokens, no overlap
Factoid QARecursive, 256-512 tokens
Analytical / multi-hopRecursive, 512-1024 tokens + parent-document
Heavy cross-reference (contracts, papers)Late chunking or contextual retrieval
Conversational / dialog corpusTurn-level chunks + speaker metadata
Short utterances (tweets, reviews)One document = one chunk

Start with recursive chunking at 512 tokens, measure recall@5 on a 50-query eval set, and tune from there.

Check: Use It0/3
1. You index research papers whose sections constantly reference each other. You have a long-context embedder and cannot afford an extra LLM call per chunk. Which strategy fits?Design
2. You are indexing support-chat transcripts, where the answer is usually one speaker's turn. Which design fits?Design
3. You must ship a first RAG build over an unknown corpus and you have a 50-query eval set. Which plan fits the lesson's approach?Design

Ship It

Save as outputs/skill-chunker.md:

markdown
---
name: chunker
description: Pick a chunking strategy, size, and overlap for a given corpus and query distribution.
version: 1.0.0
phase: 5
lesson: 23
tags: [nlp, rag, chunking]
---
 
Given a corpus (document types, avg length, domain) and query distribution (factoid / analytical / multi-hop), output:
 
1. Strategy. Recursive / sentence / semantic / parent-document / late / contextual. Reason.
2. Chunk size. Token count. Reason tied to query type.
3. Overlap. Default 0; justify if >0.
4. Min/max enforcement. `min_tokens`, `max_tokens` guards.
5. Evaluation plan. Recall@5 on 50-query stratified eval set (factoid, analytical, multi-hop).
 
Refuse any chunking strategy without min/max chunk size enforcement. Refuse overlap above 20% without an ablation showing it helps. Flag semantic chunking recommendations without a min-token floor.
Author's Note

The thing that surprised me most building RAG systems is how often overlap turns out to be a superstition people copy from a tutorial without ever measuring it. I used to default to 20% overlap on every index because it felt like free insurance against splitting an answer in half. It isn't free. At 20% overlap each chunk advances by only 80% of its size, so you embed and store about a quarter more, and on most of the corpora I've tested it moved recall@5 by less than a point.

Semantic chunking has the same problem from the other direction. It sounds like the smart choice because it respects meaning, but without a hard floor on chunk size it will happily hand you a 40-token fragment with no real chance of matching a query. Recursive chunking with a sane size and zero overlap is a boring default. Boring is correct until your own eval set proves otherwise.

Exercises

  1. Easy. Chunk one 20-page document with fixed(512, 0), recursive(512, 0), and recursive(512, 100). Compare chunk counts and boundary quality.
  2. Medium. Build a 30-query eval set over 5 documents. Measure recall@5 for recursive, semantic, and parent-document. Which wins? Does it match the blog posts?
  3. Hard. Implement contextual retrieval. Measure MRR improvement over baseline recursive. Report index cost (LLM calls) vs accuracy gain.

Key Terms

TermWhat people sayWhat it actually means
ChunkA piece of a docSub-document unit that gets embedded, indexed, and retrieved.
OverlapSafety marginN tokens shared between adjacent chunks; often useless in 2026 benchmarks.
Semantic chunkingSmart chunkingSplit where adjacent-sentence embedding similarity drops.
Parent-documentTwo-level retrievalRetrieve small children, return larger parents.
Late chunkingChunk after embeddingEmbed full doc at token level, pool into chunk vectors.
Contextual retrievalAnthropic's trickLLM-generated summary prepended to each chunk before indexing.
Context cliff2500-token wallQuality drop observed around 2.5k context tokens in RAG (Jan 2026).

Further Reading

Code

Ships With