Overview
Chunking decides what the retriever can see. A chunk that splits a sentence or drops its heading hides the answer from the embedding model and from the user. These rules set the boundary policy, the size window, the metadata, and the context each chunk carries. For retrieval over the chunks see rag-retrieval; for the embedding step see embeddings.
Split on structure first, size second
Walk the document structure, then enforce a token budget. A 500-token chunk that starts mid-sentence loses to a 700-token chunk that respects the section.
- Markdown: split on
#,##,###, blank lines, code fences, then sentences as a last resort. - HTML: split on
<h1>,<h2>,<section>,<article>; strip navigation and footers first. - Code: split on top-level declarations (function, class, module), not braces.
- Count tokens with a real tokenizer, ideally the embedding model’s own. Character heuristics undercount code and overcount English.
Start at 200 to 800 tokens and tune on recall
These sizes are a starting range, not a rule. Very small chunks lose the context that makes them retrievable; very large chunks blur many topics into one vector. Test 2 or 3 sizes against recall@k on your golden set (see rag-eval).
- 200 to 400 tokens: FAQ entries, definitions, snippets.
- 400 to 800 tokens: documentation sections, API reference entries.
- Longer: split.
Overlap by about 10 to 20 percent
Overlap keeps a fact that straddles a boundary retrievable from both chunks.
def chunk_with_overlap(tokens, size=512, overlap=64):
return [tokens[i:i + size] for i in range(0, len(tokens), size - overlap)]Heavier overlap embeds the same text twice and makes chunks compete with themselves at query time.
Give every chunk context before embedding it
A chunk like “revenue grew 3% over the previous quarter” is unretrievable without knowing the company and quarter. Contextual retrieval (Anthropic, 2024) has a model write a short, chunk-specific description of where the chunk sits in its document, prepends it to the chunk, and uses the result for both the embedding and the BM25 index. Anthropic reported that this cut top-20 retrieval failures by 35 percent with contextual embeddings alone and 49 percent with contextual BM25 added, and by 67 percent when combined with reranking, on its own test sets.
- Prompt caching makes it affordable: Anthropic estimated about $1.02 per million document tokens for the one-time contextualization.
- Measure on your corpus; the numbers above are from Anthropic’s datasets, not yours.
- Alternative:
voyage-context-4embeds each chunk with awareness of its surrounding document, with no generated text. See embeddings.
Attach metadata to every chunk
Metadata is the second retrieval axis; filters run before the vector step.
- Required:
source_url,title,heading_path,chunk_index,last_updated. - Useful:
lang,tenant_id,doc_type,tags. - Store the heading path as one string (
"Postgres > Indexes > BRIN") so it renders in citations and filters cleanly. - Store dates as epoch integers if your store only range-filters on numbers (ChromaDB does); see chromadb.
The keyword arm of hybrid retrieval needs the same chunks; see postgres-full-text-search.
Consider propositions for dense reference text
For encyclopedic text with one fact per sentence, have a small model rewrite each paragraph as standalone sentences, embed those, and return the parent paragraph. Skip it for narrative or argumentative prose, where the flow is the meaning, and keep it only if recall improves on your eval.
Retrieve small, return big when the answer needs more
The chunk is the retrieval unit, but the answer unit may be larger. For question answering, prompt with the chunks and cite them (rag-citations). For summaries, retrieve chunks and return the parent document. For briefings, expand each hit to its parent section.
Re-chunk and re-embed together
Chunk size interacts with the embedding model’s context window and training. When you change models, re-test the size range, re-ingest the whole corpus, and record the chunker version in collection metadata. Never mix chunk sizes in one collection. A chunking change without a re-embed is a silent regression.