Blog/rag

Does chunk overlap help? Measured at 0, 10, 20 and 40 percent

On the FastAPI docs, 40% overlap raised BM25 strict hit@5 by 13 to 20 points for fixed chunks, but cost 1.6x the index. 10% overlap gave 2 to 5 points.

In this post

What this post covers
  1. Setup
  2. Results
  3. What the numbers say
  4. Caveats
  5. Copyable code
  6. Rule of thumb
How this was testedhardware, versions, data

FastAPI English docs (152 Markdown files, 1.46M characters), 781 heading queries, BM25 (k1=1.5, b=0.75), fixed-size character windows only, Python 3, standard library only. No embeddings and no heading-aware chunkers were tested. Script and raw output are in the Experiment files box.

Does overlapping fixed-size chunks improve retrieval, and is it worth the bigger index? I swept overlap at 0, 10, 20 and 40% of the chunk size, at four chunk sizes, using the same harness as the chunk size sweep.

Short answer: yes, and more overlap kept helping up to 40%. But it is a trade: 40% overlap meant about 1.6x the chunks and characters to index, and the gain was biggest on the strict metric, which rewards “clean” chunks. On the lenient metric the gain was 1 to 7 points. Overlap patches bad cut points; it does not replace cutting at headings.

New to chunking? The free lesson on ingestion and chunking covers the basics.

Setup

  • Corpus: 152 FastAPI doc files, 1.46M characters. (The docs change upstream, so the numbers differ slightly from older posts. Don’t compare cell by cell.)
  • Queries: 781 h2/h3 headings with 2+ words and at least 300 characters of body.
  • Retriever: BM25, top 5.
  • Chunker: fixed windows of 200, 400, 800 or 1,200 characters, stepping by size minus overlap.
  • Strict hit: a retrieved chunk lies at least 50% inside the target section. Lenient hit: a retrieved chunk shares at least 100 characters with it.

Results

Size Overlap Chunks Index size vs 0% Strict hit@1 Strict hit@5 MRR Lenient hit@1 Lenient hit@5
200 0% 7,388 1.00x 0.543 0.786 0.641 0.543 0.786
200 10% 8,192 1.11x 0.592 0.821 0.683 0.592 0.821
200 20% 9,182 1.25x 0.608 0.867 0.713 0.608 0.867
200 40% 12,167 1.66x 0.648 0.939 0.776 0.648 0.939
400 0% 3,730 1.00x 0.466 0.702 0.559 0.656 0.873
400 10% 4,126 1.11x 0.498 0.755 0.600 0.695 0.908
400 20% 4,607 1.24x 0.517 0.786 0.630 0.729 0.942
400 40% 6,070 1.65x 0.554 0.900 0.705 0.726 0.936
800 0% 1,903 1.00x 0.392 0.581 0.467 0.711 0.922
800 10% 2,090 1.11x 0.387 0.602 0.472 0.729 0.950
800 20% 2,331 1.24x 0.419 0.671 0.526 0.722 0.939
800 40% 3,028 1.63x 0.449 0.775 0.589 0.738 0.942
1,200 0% 1,292 1.00x 0.279 0.417 0.338 0.699 0.921
1,200 10% 1,418 1.10x 0.298 0.455 0.362 0.745 0.944
1,200 20% 1,571 1.23x 0.311 0.499 0.391 0.729 0.935
1,200 40% 2,024 1.57x 0.318 0.549 0.415 0.732 0.937

“Index size” is total characters in all chunks divided by the 0% run at the same size, so it is what you pay in embedding calls and storage.

What the numbers say

Strict hit@5 rose steadily with overlap. At 40% overlap the gain over 0% was +15.3 points at 200 characters, +19.8 at 400, +19.4 at 800 and +13.2 at 1,200. Strict MRR rose by 0.077 to 0.146.

10% overlap, the common default, gave a small gain. Strict hit@5 went up 3.5, 5.3, 2.1 and 3.8 points across the four sizes. Strict hit@1 moved between -0.5 and +4.9 points. With 781 queries, a difference of about 2 points is within noise (roughly one standard error), so the 800-character hit@1 change of -0.5 points means “no effect”, not “overlap hurts”.

The lenient metric shows much smaller gains. Lenient hit@5 improved by 3.5 to 6.9 points at 400, 1.7 to 2.8 at 800 and 1.4 to 2.3 at 1,200 characters. That is the honest reading: the section was usually already found at 0% overlap (lenient hit@5 was 0.873 to 0.922 for sizes 400 and up). What overlap changed was whether the hit chunk was mostly about the target section. At 200 characters the two metrics are identical, since a 200-character chunk that shares 100 characters is also at least half inside.

It is not free. 40% overlap cost 1.57x to 1.66x the index. Compare it with the other lever: going from 400 to 200 characters with no overlap raised strict hit@5 from 0.702 to 0.786 (+8.4 points) at 1.0x the characters, though it doubled the chunk count (7,388 vs 3,730), and each chunk is a separate vector.

Caveats

  • Strict metric favours overlap. Overlap adds more chunks that sit mostly inside the target section, so there are more chances to match. Part of the strict gain is that effect, not better retrieval of the same text.
  • Duplicate text in the top 5. With high overlap, neighbouring chunks share text and can fill several of the five slots with near-copies. I did not measure how much this hurts answer diversity.
  • BM25 and heading queries. Embedding retrieval or natural-language questions may behave differently. I did not test either.
  • Fixed windows only. The heading-aware chunker in the earlier sweep had zero boundary crossings, so there is little for overlap to repair there. I did not test overlap on it.
  • Retrieval, not answer quality. Check answers on your own golden set.

Copyable code

def fixed(text, size, pct):
    step = size - size * pct // 100
    out, i = [], 0
    while i < len(text):
        out.append((i, min(len(text), i + size)))
        if out[-1][1] == len(text):
            break
        i += step
    return out

The full script, with BM25 and scoring, is in the Experiment files box.

Rule of thumb

  1. If you must use fixed windows, use 10 to 20% overlap. It bought 2 to 9 points of strict hit@5 for 1.1x to 1.25x the index.
  2. Go to 40% only if recall matters more than index cost. It gave the largest gains here (up to 19.8 points of strict hit@5) at about 1.6x the cost.
  3. Prefer fixing the boundaries. Splitting at headings removed boundary crossings entirely in the size sweep, which overlap can only soften.
  4. Measure on your corpus. Gains of 2 points or less here were within noise.
Learn it step by step · free

Go deeper in the RecallRun courses

All 11 free courses →

Experiment files

The exact script and raw output behind every number in this post. Download and rerun them.

Want the production version of this?

A production RAG + MCP starter kit for FastAPI: hybrid search, validated citations, evals in CI, Docker, 32 tests. It runs offline in about 60 seconds.

See ShipRAG  or get the free RAG checklist first.

Written and measured by

Alby Tomy

Python/FastAPI Engineer | Data Engineer | AI Engineer building RAG and MCP systems.

X · GitHub · Contact & enquiries