AnyLearn
All lessons
AIadvanced

RAG Chunking Strategies: From Fixed-Size to Late Chunking

A deep dive into how you split documents for retrieval-augmented generation — fixed-size, recursive, semantic, hierarchical, and late chunking — with concrete trade-offs and code for each approach.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 12

Why Chunking Is the Hidden Variable in RAG Quality

Most RAG tutorials spend 80% of their time on embedding models and vector databases. Chunking gets a footnote. That's backwards.

Your retrieval pipeline can only return what it indexed. If a chunk straddles a topic boundary, the embedding is muddied. If chunks are too large, the relevant sentence drowns in noise. If they're too small, you lose the context that makes the sentence meaningful. The embedding model is not magic — garbage in, garbage out.

Five strategies cover most production use-cases:

  • Fixed-size — simplest baseline, character or token count
  • Recursive — structure-aware splitting on a priority list of separators
  • Semantic — split on embedding similarity shifts
  • Hierarchical — dual-granularity index (summary + detail)
  • Late chunking — embed the whole document first, then pool

Each makes a different bet about where meaning lives in your corpus.

Full lesson text

All 12 steps on one page — for reading, reference, and search.

Show

1. Why Chunking Is the Hidden Variable in RAG Quality

Most RAG tutorials spend 80% of their time on embedding models and vector databases. Chunking gets a footnote. That's backwards.

Your retrieval pipeline can only return what it indexed. If a chunk straddles a topic boundary, the embedding is muddied. If chunks are too large, the relevant sentence drowns in noise. If they're too small, you lose the context that makes the sentence meaningful. The embedding model is not magic — garbage in, garbage out.

Five strategies cover most production use-cases:

  • Fixed-size — simplest baseline, character or token count
  • Recursive — structure-aware splitting on a priority list of separators
  • Semantic — split on embedding similarity shifts
  • Hierarchical — dual-granularity index (summary + detail)
  • Late chunking — embed the whole document first, then pool

Each makes a different bet about where meaning lives in your corpus.

2. Fixed-Size Chunking: The Honest Baseline

Fixed-size chunking slices text every N tokens (or characters) with an optional overlap. It is fast, deterministic, and model-agnostic.

from langchain.text_splitter import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    encoding_name="cl100k_base",  # tiktoken encoder for GPT-4 / text-embedding-3
)
chunks = splitter.split_text(document)

Why overlap exists: A sentence that starts at token 510 would otherwise be split between two chunks, severing the context. Overlap of 10–15% of chunk size is the common heuristic.

Gotcha: Character-based splitting and token-based splitting diverge badly on non-Latin scripts and code. Always use the same tokenizer as your embedding model — a 512-token budget for text-embedding-3-small is exactly 512 cl100k tokens, not 512 characters.

Fixed-size works surprisingly well on homogeneous corpora (e.g., news articles, product descriptions). It breaks down on structured documents where a logical section is 50 tokens or 2 000 tokens depending on where you happen to cut.

3. Recursive Chunking: Respecting Natural Structure

Recursive chunking tries a priority-ordered list of separators and only falls back to the next one when the resulting chunk would still be too large. LangChain's RecursiveCharacterTextSplitter defaults to ["\n\n", "\n", " ", ""] — paragraphs first, then lines, then words, then characters.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=100,
    separators=["\n\n", "\n", ".", " ", ""],
)
chunks = splitter.split_documents(docs)

For code, swap the separator list for language-specific tokens:

from langchain.text_splitter import Language

code_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1500,
    chunk_overlap=150,
)

This keeps function and class boundaries intact where possible. The practical win over fixed-size is meaningful: internal benchmarks from LlamaIndex (2023) showed a ~10% improvement in answer faithfulness on technical docs just from switching to recursive over character-based splitting.

4. How Recursive Chunking Falls Back

flowchart TD
    A["Full document text"] --> B["Split on \\n\\n (paragraphs)"]
    B --> C{"Chunk <= max size?"}
    C -- "Yes" --> G["Emit chunk"]
    C -- "No" --> D["Split on \\n (lines)"]
    D --> E{"Chunk <= max size?"}
    E -- "Yes" --> G
    E -- "No" --> F["Split on space / char"]
    F --> G

5. Semantic Chunking: Let Embeddings Draw the Boundaries

Instead of splitting on syntax, semantic chunking embeds every sentence, then looks for cosine-similarity drops between adjacent sentences. A big drop means a topic shift — that's where you cut.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

chunker = SemanticChunker(
    OpenAIEmbeddings(model="text-embedding-3-small"),
    breakpoint_threshold_type="percentile",   # or "standard_deviation", "interquartile"
    breakpoint_threshold_amount=95,           # cut at similarities below the 95th-percentile drop
)
chunks = chunker.create_documents([text])

Three threshold strategies:

StrategyCuts whenBest for
percentilesimilarity < Nth percentilepredictable chunk count
standard_deviationdrop > mean - k*stdvariable-density docs
interquartiledrop in the IQR tailoutlier-resistant

Cost: Every sentence gets embedded at index time, roughly 3-5x the API cost of fixed-size. Chunk count is non-deterministic, which complicates infrastructure planning. Worth it for heterogeneous documents like legal briefs or research papers where topic shifts are meaningful signals.

6. Hierarchical Chunking: Index at Two Resolutions

A single chunk size forces a trade-off between precision (small chunks) and context (large chunks). Hierarchical chunking sidesteps it by building two layers: small child chunks for retrieval, large parent chunks for generation.

The retrieval flow:

  1. Embed and search the small child chunks (high precision)
  2. When a child chunk is a hit, fetch its parent chunk instead
  3. Pass the full parent to the LLM (rich context)
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain.vectorstores import Chroma
from langchain.text_splitter import RecursiveCharacterTextSplitter

child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)

retriever = ParentDocumentRetriever(
    vectorstore=Chroma(embedding_function=embeddings),
    docstore=InMemoryStore(),
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)
retriever.add_documents(docs)

A variant is summary-then-detail: embed an LLM-generated summary of each section as the search target, return the original section as context. This costs more at index time but retrieves the right section even when the query uses vocabulary absent from the raw text.

7. Hierarchical Retrieval Flow

flowchart LR
    A["User query"] --> B["Embed query"]
    B --> C["Search child chunk index"]
    C --> D["Top-k child chunks"]
    D --> E["Lookup parent ID"]
    E --> F["Fetch parent chunk from docstore"]
    F --> G["LLM generates answer"]

8. Late Chunking: Embed First, Chunk Later

Late chunking flips the usual order. Instead of chunking the document and then embedding each chunk independently, you pass the entire document through the embedding model in one forward pass, collecting per-token contextual embeddings. Only after that do you pool tokens into chunk vectors.

This preserves cross-chunk context during embedding — a pronoun at token 600 carries information from its antecedent at token 100 because they shared the same attention computation.

# Pseudo-code for late chunking with a long-context encoder (e.g., jina-embeddings-v3)
import numpy as np

def late_chunk_embed(text: str, chunk_spans: list[tuple[int,int]], model) -> list[np.ndarray]:
    token_embeddings = model.encode_tokens(text)   # shape: (seq_len, dim)
    chunk_vectors = []
    for start, end in chunk_spans:
        pooled = token_embeddings[start:end].mean(axis=0)   # mean-pool span
        chunk_vectors.append(pooled / np.linalg.norm(pooled))
    return chunk_vectors

The catch: your encoder needs a context window large enough to hold the document. Jina AI's jina-embeddings-v3 (8 192 tokens) and nomic-embed-text-v1.5 (8 192 tokens) are practical options. For book-length documents you still need to pre-split into sections before applying late chunking within each section.

Jina AI published a benchmark (2024) showing late chunking outperformed fixed-size by 5–12 percentage points on BEIR retrieval tasks.

9. Choosing a Strategy: A Decision Framework

No single strategy wins everywhere. Here's how to pick:

ConditionRecommended strategy
Homogeneous short docs (< 1 000 tokens)Fixed-size — overhead isn't worth it
Mixed content: prose + code + tablesRecursive with language-aware separators
Long heterogeneous docs (legal, academic)Semantic or Hierarchical
Domain with consistent structure (API docs, manuals)Hierarchical (parent/child)
Long-context encoder available, doc fits in windowLate chunking
Cost is primary constraintFixed-size or Recursive

Chunk size heuristics (for embedding models with 512-token input limit):

  • Small retrieval chunks: 256–400 tokens
  • Parent/generation chunks: 1 500–2 500 tokens
  • Overlap: 10–15% of chunk size

Heads up: Many embedding models silently truncate input beyond their maximum. A 2 000-token chunk fed to a 512-token model embeds only the first 512 tokens — the rest is invisible. Always verify model.max_seq_length before setting chunk size.

10. Metadata Enrichment and Chunk Hygiene

A chunk without metadata is a dead end. At minimum, store:

{
  "chunk_id": "doc-42-chunk-7",
  "doc_id": "doc-42",
  "source_url": "https://docs.example.com/api/auth",
  "section_title": "OAuth 2.0 Flow",
  "page": 3,
  "token_count": 387,
  "created_at": "2025-11-01T09:14:00Z"
}

This enables:

  • Source attribution — tell users which document answered their question
  • Metadata pre-filtering — restrict vector search to chunks from a specific doc or date range before scoring
  • Deduplication — detect and skip near-identical chunks from re-ingested documents

Chunk hygiene rules that move the needle in practice:

  • Strip boilerplate headers/footers before chunking (page numbers, nav bars, cookie banners)
  • Normalize whitespace — double spaces and \r\n cause separator mismatches
  • For PDFs, run a PDF-to-Markdown converter (pymupdf4llm, marker) rather than raw text extraction — table structure is preserved and column-ordering artifacts are fixed

11. Evaluating Your Chunking Choices

The only reliable signal is retrieval quality on a held-out question set. Build one early.

RAGAS metrics are the standard toolkit:

from ragas import evaluate
from ragas.metrics import context_precision, context_recall, faithfulness

result = evaluate(
    dataset=qa_dataset,           # HuggingFace Dataset with question / contexts / ground_truth
    metrics=[context_precision, context_recall, faithfulness],
)
print(result.to_pandas())
  • Context precision — of the retrieved chunks, what fraction are actually relevant?
  • Context recall — of the ground-truth answer's supporting facts, how many were covered by the retrieved chunks?
  • Faithfulness — does the LLM's answer stick to what the chunks say, or hallucinate?

A chunking change that improves context recall without hurting precision is almost always a net win. Run ablations: same corpus, same embedding model, same retriever — only change the chunking strategy. Typical evaluation set size: 100–300 questions is enough to detect a 5% difference with statistical significance.

12. Production Gotchas and Operational Considerations

A few things that bite teams after the prototype stage:

Re-chunking on schema change. If you decide to change chunk size or strategy, you must re-embed and re-index the entire corpus. Plan your vector store schema (Pinecone namespace, Qdrant collection, pgvector table) to allow a blue-green index swap with zero downtime.

Multimodal documents. PDFs with figures and tables don't chunk well as plain text. Consider storing figure descriptions generated by a vision model as separate chunks with a content_type: figure metadata tag, then route queries that need visual context to those chunks.

Chunk size vs. LLM context window. With 128 k-token LLM context windows now common, there's a temptation to just pass everything in. This works for single-document Q&A but fails at scale: retrieving 50 large parent chunks × 2 000 tokens = 100 000 tokens per request. Cost and latency compound quickly.

Incremental ingestion. Appending new documents to an existing index is fine. But if a source document is updated, you must detect which chunks changed (hash the source), delete stale chunks by doc_id, and re-index only the updated sections — not the whole corpus.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. Which chunking strategy preserves cross-chunk contextual signals during embedding by running the full document through the encoder before splitting?
    • Recursive chunking
    • Semantic chunking
    • Late chunking
    • Hierarchical chunking
  2. You are chunking a 900-token product description to embed with `text-embedding-3-small` (512-token max). Your chunk size is set to 800 tokens. What is the most likely result?
    • The model raises an exception and returns no embedding
    • The chunk embeds normally because 800 < 900
    • The model silently truncates the chunk to 512 tokens, losing the last 288 tokens
    • The chunk is automatically split at 512 tokens by the OpenAI API
  3. In a ParentDocumentRetriever setup, why are small child chunks used for the vector index rather than the parent chunks?
    • Child chunks are cheaper to embed
    • Small chunks produce higher-precision retrieval because their embeddings are less diluted by irrelevant content
    • The vector store cannot store chunks larger than 512 tokens
    • Parent chunks have no metadata and cannot be searched
  4. Which RAGAS metric specifically measures whether the LLM's generated answer is grounded in the retrieved context rather than in its parametric knowledge?
    • context_precision
    • context_recall
    • faithfulness
    • answer_relevancy
  5. Semantic chunking has a significantly higher indexing cost than fixed-size chunking. What is the primary reason?
    • Semantic chunking requires a reranker model in addition to an embedding model
    • Every sentence must be embedded individually to detect similarity drops between adjacent sentences
    • Semantic chunks are larger on average, requiring more API calls to the LLM
    • Semantic chunking parses the document twice: once for structure and once for content

Related lessons

AI
intermediate

RAG Evaluation in Production: Metrics, Tools, and Cadence

Learn how to systematically evaluate Retrieval-Augmented Generation systems in production using RAGAS, TruLens, and Phoenix — covering golden sets, retrieval drift, embedding drift, and cost-aware eval scheduling.

12 steps·~18 min·audio
AI
advanced

Loop engineering: verification, orchestration, and anti-patterns

Make loops trustworthy. Adversarial verification panels, sub-agent orchestration, loop-until-dry for unbounded discovery, loop-until-budget for paid depth, multi-modal sweeps with diverse prompts, eval-driven cap selection, and the five anti-patterns — silent caps, infinite plans, drift, premature termination, thrashing — that ship to prod more than they should.

9 steps·~14 min
AI
intermediate

Loop engineering in production: state, errors, and budgets

Once an agent loop runs past a few iterations the failure surface shifts: state bloats, tool calls fail in three distinct ways, retries multiply if you stack them wrong, and unbounded budgets rack up four-figure bills overnight. The state, errors, and budgets you need to make the loop survive contact with reality.

8 steps·~12 min
AI
beginner

Loop engineering basics: the agent control loop

How LLM agents actually run: the iterative prompt-action-observation loop, the ReAct shape, the smallest tool-calling loop in twelve lines, why you always stack three termination layers, what the model sees on iteration N, and when a single prompt is the better answer.

8 steps·~12 min