Chunking Text for RAG: Sizes, Overlap and Where to Split

Chunking looks like a preprocessing detail and is usually the single largest determinant of whether a retrieval system works. Get it wrong and no amount of prompt engineering or model upgrading compensates β€” the model cannot reason about a passage it was never shown. This page covers the decisions that matter and how to make them.

Sensible defaults

400–800 tokens per chunk, 10–20% overlap, split on document structure rather than character count, and prepend a short contextual summary to each chunk before embedding. Measure and adjust from there β€” those defaults are a starting point, not an answer.

Why chunk at all

Three separate reasons, and they pull in different directions:

  • Embedding models have input limits, typically 512 to 8,192 tokens. A long document simply will not fit.
  • Precision. An embedding is a single vector representing everything in its input. A whole chapter averages into a vague vector that matches many queries weakly and none strongly.
  • Context budget. You can only pass a limited amount to the model, so retrieving whole documents wastes the window on irrelevant material.

πŸ’‘ Why big chunks embed badly

An embedding is roughly a summary of meaning in a fixed number of dimensions. A chunk covering five topics produces a vector that sits somewhere between all five β€” close to none of them.

This is the core tension. Smaller chunks give sharper vectors that match precisely. Larger chunks give more complete passages that read better. You cannot maximise both, and the whole craft of chunking is managing that trade.

Choosing a size

SizeRetrieval precisionContext preservedSuits
100–200Very highPoorFAQs, definitions, product specs
300–500HighModerateDocumentation, knowledge bases
500–800GoodGoodGeneral prose β€” the default
800–1,500ModerateStrongNarrative, reports, legal text
1,500+WeakVery strongRarely the right choice

The right size depends as much on your questions as your documents. Narrow factual questions β€” "what is the refund window?" β€” favour small chunks. Questions requiring synthesis β€” "how does the escalation process work?" β€” favour larger ones that hold a complete explanation.

If your queries are mixed, index at two sizes and search both. Storage is cheap relative to the cost of failed retrieval.

Splitting strategies

Fixed size β€” the fallback

// Split every N characters. Simple, and structurally blind. function fixedChunks(text, size = 2000, overlap = 200) { const out = []; for (let i = 0; i < text.length; i += size - overlap) { out.push(text.slice(i, i + size)); } return out; } // Cuts mid-sentence, mid-word, mid-table. // Use only when the text has no structure at all.

Recursive β€” the sensible default

Try to split on the largest natural boundary available, and fall back to smaller ones only when a piece is still too big:

// Try each separator in order of preference const SEPARATORS = [ "\n## ", // heading "\n\n", // paragraph "\n", // line ". ", // sentence " ", // word ]; function recursiveSplit(text, maxTokens, depth = 0) { if (countTokens(text) <= maxTokens) return [text]; if (depth >= SEPARATORS.length) return hardSplit(text, maxTokens); const parts = text.split(SEPARATORS[depth]); return mergeToLimit(parts, maxTokens) .flatMap(p => recursiveSplit(p, maxTokens, depth + 1)); }

The mergeToLimit step matters and is often omitted. After splitting on paragraphs you have many small pieces; combining adjacent ones up to the size limit produces full chunks rather than a scatter of one-sentence fragments.

Structural β€” the best where available

Markdown, HTML and well-formed documents carry explicit structure. Use it.

// Split on headings, keeping the hierarchy as metadata { text: "Refunds are processed within 14 days…", metadata: { h1: "Customer Policies", h2: "Returns and Refunds", h3: "Processing Times", source: "policies.md", position: 14 } }

Keeping the heading path is valuable twice over: it can be prepended to the chunk text so the embedding knows what the passage is about, and it gives the model a citation when it answers.

Semantic β€” sometimes worth it

Embed each sentence, measure similarity between consecutive sentences, and split where similarity drops β€” the point where the topic changes.

It produces genuinely coherent chunks and costs an embedding call per sentence at index time. Worth it for high-value corpora with poor structural markup; unnecessary when headings already tell you where the boundaries are.

Overlap

Overlap exists to stop a passage that straddles a boundary being lost from both chunks.

// Without overlap β€” the answer is split and neither half matches Chunk 1: "…the escalation path depends on severity. For" Chunk 2: "severity 1 incidents, page the on-call lead…" // With overlap β€” chunk 2 carries the lead-in Chunk 2: "…escalation path depends on severity. For severity 1 incidents, page the on-call lead…"
OverlapEffect
0%Boundary passages lost
10–20%Sensible default
30–50%Near-duplicates crowd the results; storage inflates

The failure mode at high overlap is subtle: your top five results become five overlapping views of the same passage, and genuinely different relevant material never surfaces. If your retrieval returns repetitive results, check overlap before blaming the embedding model.

Note that overlap is largely unnecessary with structural splitting β€” a chunk that ends at a section boundary is not cutting anything mid-thought.

Contextual retrieval

This technique fixes more retrieval failures than any amount of size tuning, and it is worth implementing before anything else.

The problem: a chunk extracted from the middle of a document loses everything the surrounding text established.

// The raw chunk β€” what does this even refer to? "The rate increased to 4.5% in the second quarter, driven primarily by the renewal cohort." // A question that should match it: "What happened to Acme's churn rate in Q2 2026?" // It matches poorly. The chunk never says // Acme, churn, or 2026.

The fix is to generate a short contextual preamble for each chunk β€” using a cheap model, at index time β€” and prepend it before embedding:

// Prompt used once per chunk at index time "Here is a document: {whole_document} Here is a chunk from it: {chunk} Write 1-2 sentences situating this chunk within the document, for search purposes. Answer only with the context." // Result, prepended to the chunk before embedding: "From Acme Corp's Q2 2026 retention report, in the section on customer churn. The rate increased to 4.5% in the second quarter, driven primarily by the renewal cohort."

The chunk now contains the entities the question uses. Published results on this approach report large reductions in retrieval failure rate, and it is straightforward to add to an existing pipeline.

βœ… It is cheaper than it sounds

The cost is one small-model call per chunk, paid once at indexing. With prompt caching on the document β€” which is resent for every chunk from the same document β€” the marginal cost per chunk drops substantially. Against the cost of a retrieval system that returns the wrong passages, it is not a close call.

Tables, code and lists

ContentRule
TablesNever split from the header. If it must split, repeat the header row in each part.
CodeSplit on function or class boundaries. Include the signature and any imports it needs.
ListsKeep whole where possible; carry the introducing sentence into each part.
DefinitionsTerm and definition must stay together.
Q&A pairsOne chunk per pair β€” they are naturally sized.

A table fragment without its header is genuinely worse than nothing: it contains numbers with no indication of what they measure, and the model will confidently interpret them wrongly.

// Splitting a long table β€” repeat the header function splitTable(header, rows, maxTokens) { const chunks = []; let current = [header]; for (const row of rows) { if (countTokens([...current, row].join('\n')) > maxTokens) { chunks.push(current.join('\n')); current = [header]; // header again } current.push(row); } chunks.push(current.join('\n')); return chunks; }

Retrieve small, return large

A pattern that gets much of the benefit of both sizes.

Index small chunks for precise matching, but store a pointer from each to a larger surrounding passage. Search the small ones; return the large ones to the model.

{ id: "c_4821", text: "Refunds are processed within 14 days.", // embedded parentId: "p_312" // returned } // Search matches the precise sentence. // The model receives the full section it came from.

You get the retrieval precision of a 150-token chunk and the contextual completeness of an 800-token one. The main cost is a second store and slightly more complex retrieval code.

Evaluating it

Chunking cannot be tuned by intuition. Build a small evaluation set β€” thirty to fifty questions with the passages that should answer them β€” and measure.

MetricQuestion it answers
Recall@kIs the correct passage in the top k results?
MRRHow highly is it ranked when it is found?
Context precisionWhat fraction of retrieved tokens are actually relevant?
Answer accuracyThe one that matters β€” end to end

Recall@k is the diagnostic to start with. If the right passage is not being retrieved, nothing downstream can fix it β€” a better model will simply be confidently wrong using the wrong source. Fix retrieval first, then tune generation.

Preparing documents for indexing?

Convert HTML to Markdown for cleaner chunking, or extract and split PDF pages β€” all in your browser.

Open the HTML to Markdown Converter β†’

Summary

  • Chunk size is a precision-versus-context trade-off. 400–800 tokens suits most prose.
  • Split on structure β€” headings and paragraphs β€” before falling back to character counts.
  • 10–20% overlap. More returns near-duplicates that crowd out real results.
  • Contextual retrieval fixes the most failures. Prepend a situating summary before embedding.
  • Never split a table from its header or a function from its signature.
  • Retrieve small, return large to get precision and completeness together.
  • Keep heading paths as metadata β€” they improve embeddings and provide citations.
  • Measure recall@k first. Retrieval failures cannot be fixed downstream.

Frequently Asked Questions

What is the best chunk size for RAG?

There is no universal answer, but 400 to 800 tokens suits most prose. Smaller chunks give sharper embeddings and more precise retrieval while losing surrounding context; larger chunks preserve context but produce diluted embeddings that match many queries weakly. The right size depends on your documents and questions, and should be measured rather than assumed.

How much overlap should chunks have?

Typically 10 to 20% of the chunk size β€” about 50 to 100 tokens for a 500-token chunk. Overlap exists so that a passage spanning a boundary is not lost entirely from both chunks. Too much overlap wastes storage and returns near-duplicate results that crowd out genuinely different passages.

Should I split on characters or on structure?

Structure, wherever the document has any. Splitting on headings, paragraphs and sections produces chunks that are individually coherent, which both embeds better and reads better when passed to the model. Fixed-size character splitting is a fallback for unstructured text, not a default.

Why does my RAG system retrieve the wrong chunks?

The most common cause is chunks that lost their context. A passage saying 'the rate increased to 4.5%' embeds without any indication of which rate, which product or which year, so it matches poorly against a specific question. Prepending a short contextual summary to each chunk before embedding fixes a large share of these failures.

How do I chunk tables and code?

Do not split them mid-structure. A table fragment without its header row is meaningless, and a function split across two chunks is worse than useless. Keep tables and code blocks whole where they fit, and where they do not, repeat the header or signature in each part so every chunk stands alone.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.