,

Chunking strategies that actually matter for retrieval quality

LEARN · RAG & AGENTIC DEVELOPMENT

Why chunking deserves more attention

If you’ve experimented with embedding models, vector databases, rerankers, or prompts, but your Retrieval-Augmented Generation (RAG) system still misses obvious answers, the bottleneck may be much simpler: chunking.

Chunking determines the units your retriever can return. If the chunks are poorly constructed, retrieval quality suffers before the language model ever sees the context.

This guide explains the chunking strategies that have the biggest practical impact, when to use each one, how to evaluate them, and includes a runnable comparison between fixed-size and paragraph-aware chunking. The emphasis is on measurable retrieval quality rather than intuition.


Before documents are embedded, they are split into smaller retrieval units called chunks.

Instead of generating one embedding for an entire document, you generate many embeddings—one per chunk. During retrieval, the vector index searches those chunk embeddings.

The goal is simple:

  • Each chunk should represent one coherent topic.

  • Each chunk should contain enough context to answer a question independently.

  • Each chunk should avoid mixing unrelated ideas.

Good chunking improves:

  • Recall

  • Ranking quality

  • Context precision

  • Downstream answer quality

Poor chunking can make an excellent embedding model appear ineffective.


These two approaches are often confused.

Character-based chunking

Character-based chunking splits after a fixed number of characters.

Advantages:

  • Extremely simple

  • Very fast

  • No tokenizer dependency

Disadvantages:

  • Doesn’t correspond to embedding model tokenization

  • May exceed token limits unexpectedly

  • Can split sentences awkwardly


Token-based chunking

Token-based chunking measures chunk size using the tokenizer associated with the embedding model.

Advantages:

  • Predictable embedding sizes

  • Better alignment with model context limits

  • More consistent retrieval behavior across documents

For production RAG systems, token-based chunking is generally the preferred baseline because embedding models consume tokens rather than characters.

Throughout this article, the educational chunking implementations use character counts to keep the examples dependency-light and easy to understand. In production, replace those character limits with token counts using the tokenizer for your chosen embedding model.


Imagine a documentation page with sections covering:

  • Installation

  • Authentication

  • Rate limits

  • Error handling

  • Pricing

A user asks:

How do I authenticate?

If all five sections exist inside one chunk, the embedding represents several unrelated concepts simultaneously.

Now consider the opposite extreme:

  • “To authenticate”

  • “send your API key”

  • “inside the Authorization header”

Those chunks are so small that none contains enough information on its own.

The sweet spot lies between these extremes.

Well-designed chunks are:

  • Self-contained

  • Focused on one topic

  • Large enough to preserve meaning

  • Small enough to avoid unrelated information


Fixed-size chunking splits text after a predetermined number of characters or tokens.

It remains one of the strongest baselines because it is predictable, easy to implement, and scales well.

from pathlib import Path

text = Path("document.txt").read_text(encoding="utf-8")

CHUNK_SIZE = 800

chunks = [
    text[i:i + CHUNK_SIZE]
    for i in range(0, len(text), CHUNK_SIZE)
]

print(f"Created {len(chunks)} chunks")
print(chunks[0][:250])

Best for

  • Log files

  • Large text corpora

  • OCR output

  • Chat exports

  • Uniformly formatted documents

Drawbacks

  • May split sentences

  • Ignores document structure

  • Can separate explanations from examples

  • May cut through code blocks

Despite those limitations, fixed-size chunking is an excellent benchmark because every more sophisticated strategy should outperform it on retrieval metrics.


A simple improvement is respecting paragraph boundaries.

Instead of cutting arbitrarily, paragraphs are grouped until a target size is reached.

This preserves much more of the author’s intended structure.

from pathlib import Path

text = Path("document.txt").read_text(encoding="utf-8")

paragraphs = [
    p.strip()
    for p in text.split("\n\n")
    if p.strip()
]

MAX_LENGTH = 1200

chunks = []
current = []

for paragraph in paragraphs:
    if len(paragraph) > MAX_LENGTH:
        if current:
            chunks.append("\n\n".join(current))
            current = []

        for i in range(0, len(paragraph), MAX_LENGTH):
            chunks.append(paragraph[i:i + MAX_LENGTH])

        continue

    candidate = "\n\n".join(current + [paragraph])

    if len(candidate) <= MAX_LENGTH:
        current.append(paragraph)
    else:
        if current:
            chunks.append("\n\n".join(current))
        current = [paragraph]

if current:
    chunks.append("\n\n".join(current))

print(f"Created {len(chunks)} paragraph-aware chunks")

Although this example measures size in characters for simplicity, the same algorithm can use token counts in production.

Paragraph-aware chunking often improves retrieval for documentation, blogs, and reports because paragraphs usually correspond to coherent ideas.


Many production RAG pipelines go one step further by preserving document structure instead of relying purely on size.

Examples include:

Content Preferred boundary
Markdown Headings
HTML Sections
API documentation Endpoints
Source code Functions and classes
Books Chapters and sections
Chat transcripts Speaker turns
JSON Complete objects
SQL Individual statements

The guiding principle is simple:

Keep related information together because users tend to ask questions about logical units, not arbitrary slices of text.


One of the easiest ways to reduce retrieval quality is breaking code in the middle of a function.

Avoid creating chunks like this:

Chunk A

def authenticate():
    token = create_token()

Chunk B

    return token

Instead, keep the entire function intact.

The same recommendation applies to:

  • JSON

  • YAML

  • SQL

  • XML

  • Configuration files

  • Markdown code fences

Broken syntax weakens embeddings because meaningful relationships disappear when code is fragmented.


Many chunkers duplicate part of the previous chunk into the next one.

For example:

  • Chunk 1 contains lines 1–100

  • Chunk 2 contains lines 91–190

The duplicated region helps when relevant information sits near a boundary.

A practical starting point is:

  • 500–1,000 tokens per chunk

  • Around 10–20% overlap

These are starting points rather than universal rules.


A surprising result from many RAG evaluations is that increasing overlap indefinitely rarely improves retrieval.

Large overlaps create many nearly identical embeddings.

Instead of retrieving complementary evidence, the retriever may return multiple copies of essentially the same passage.

The result can include:

  • Larger vector indexes

  • Higher embedding costs

  • Slower ingestion

  • Less diverse retrieved context

If retrieval quality is disappointing, improving chunk boundaries is often more effective than continuously increasing overlap.


Rather than comparing chunk counts, compare retrieval quality.

The example below includes a tiny self-contained document so it runs without requiring external files.

import numpy as np
from sentence_transformers import SentenceTransformer

DOCUMENT = """
# Installation

Install the package using pip.

# Authentication

Authenticate by sending your API key in the Authorization header.

# Rate limits

The API allows 100 requests per minute.

# Error handling

Errors are returned as JSON with an HTTP status code.
"""

queries = [
    ("How do I authenticate?", "Authorization header"),
    ("What are the rate limits?", "100 requests per minute"),
    ("How are errors returned?", "JSON")
]


def fixed_chunks(text, size=120):
    return [
        text[i:i + size]
        for i in range(0, len(text), size)
    ]


def paragraph_chunks(text, max_length=180):
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]

    chunks = []
    current = []

    for paragraph in paragraphs:
        candidate = "\n\n".join(current + [paragraph])

        if len(candidate) <= max_length:
            current.append(paragraph)
        else:
            chunks.append("\n\n".join(current))
            current = [paragraph]

    if current:
        chunks.append("\n\n".join(current))

    return chunks


model = SentenceTransformer("BAAI/bge-small-en-v1.5")


def recall_at_k(chunks, k=3):
    embeddings = model.encode(chunks, normalize_embeddings=True)
    hits = 0

    for query, expected in queries:
        q = model.encode(query, normalize_embeddings=True)
        scores = embeddings @ q
        top = np.argsort(scores)[::-1][:k]

        if any(expected.lower() in chunks[i].lower() for i in top):
            hits += 1

    return hits / len(queries)


fixed = fixed_chunks(DOCUMENT)
paragraph = paragraph_chunks(DOCUMENT)

print("Fixed Recall@3:", recall_at_k(fixed))
print("Paragraph Recall@3:", recall_at_k(paragraph))

This benchmark is intentionally small, but it demonstrates the correct evaluation mindset:

Measure retrieval quality instead of assuming one strategy is better.


Chunking should be evaluated like any other retrieval component.

A practical workflow looks like this:

  1. Build a representative set of user questions.

  2. Record the expected supporting passage for each question.

  3. Run retrieval.

  4. Compare retrieved chunks with the expected passages.

  5. Repeat after changing chunking parameters.

Useful metrics include:

  • Recall@k

  • Precision@k

  • Mean Reciprocal Rank (MRR)

  • NDCG

Even a benchmark containing 100–200 realistic user queries often reveals weaknesses that casual manual testing misses.

When evaluating, change only one variable at a time.

For example:

  • Chunk size

  • Overlap

  • Boundary strategy

  • Embedding model

  • Reranker

Otherwise, it becomes impossible to identify which modification actually improved retrieval.


Every dataset behaves differently, but these defaults work well for many applications.

Content Recommended strategy
Markdown documentation Heading-based
API references Section-based
Blogs Paragraph-aware
PDFs Paragraph-aware after extraction
Source code Function or class boundaries
Meeting transcripts Speaker turns
Logs Fixed-size
FAQs One question per chunk

If you’re starting a new RAG project, a strong baseline is:

  • Token-based chunk sizing

  • Structure-aware boundaries whenever available

  • Approximately 10–20% overlap

  • Complete code blocks

  • Retrieval evaluation before changing embedding models


Avoid these frequent pitfalls:

  • Splitting sentences unnecessarily

  • Breaking code blocks

  • Mixing multiple unrelated topics into one chunk

  • Creating chunks that are too small to answer questions independently

  • Creating oversized chunks containing several sections

  • Assuming larger overlap always improves retrieval

  • Optimizing embedding models before measuring retrieval quality

  • Evaluating only chunk counts instead of retrieval metrics

Many retrieval failures originate in chunk construction rather than the vector database itself.


Chunking is one of the highest-leverage optimizations in a RAG pipeline because it determines what the retriever can actually return.

The principles are straightforward:

  • One chunk should represent one coherent idea.

  • Prefer token-based sizing in production.

  • Preserve headings, paragraphs, and code boundaries whenever possible.

  • Use overlap sparingly instead of duplicating large portions of text.

  • Measure Recall@k, MRR, or NDCG with realistic queries.

  • Tune chunking before replacing your embedding model.

Small improvements in chunk construction often produce larger retrieval gains than changing prompts or increasing context windows.


Take one real knowledge base from your own project, implement both fixed-size and structure-aware chunking, and benchmark them using Recall@k or MRR against realistic user questions. The results will tell you far more than intuition—and they’ll help you improve retrieval quality based on evidence rather than guesswork.