Chunking Strategies for RAG: What Actually Works
Chunking is the most underrated decision in RAG system design. This post benchmarks the main strategies and gives you a framework for choosing and evaluating them.
Why Chunking Decisions Matter More Than Model Choice
The retrieval step in RAG can only return what you have indexed. If your chunking creates units that are too small, each chunk lacks context. Too large, and retrieval precision suffers - the relevant passage is buried in noise. The right chunking strategy determines your retrieval recall and precision before any model runs.
The Five Main Strategies
1. Fixed-Size with Overlap
pythonfrom langchain.text_splitter import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=512, # tokens per chunk chunk_overlap=64, # tokens of overlap between adjacent chunks separators=["\n\n", "\n", ". ", " "], # preferred split points ) chunks = splitter.split_text(document_text)
Pros: Simple, reproducible, works well for uniform text. Cons: Splits semantic units arbitrarily. A paragraph about one concept may be split across two chunks. Best for: Web articles, documentation, general prose.
2. Sentence-Based
pythonimport spacy nlp = spacy.load("en_core_web_sm") def sentence_chunks(text: str, max_sentences_per_chunk: int = 5) -> list[str]: doc = nlp(text) sentences = [sent.text.strip() for sent in doc.sents] chunks = [] for i in range(0, len(sentences), max_sentences_per_chunk): chunks.append(" ".join(sentences[i:i + max_sentences_per_chunk])) return chunks
Pros: Respects sentence boundaries. Chunks make semantic sense. Cons: Chunk sizes vary widely. Very long or very short sentences create uneven chunks. Best for: Conversational text, emails, support tickets.
3. Semantic (Topic-Aware)
Split on semantic similarity discontinuities - when adjacent sentences discuss different topics.
pythonfrom sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity import numpy as np embed_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") def semantic_chunking(text: str, breakpoint_threshold: float = 0.5) -> list[str]: sentences = text.split(". ") embeddings = embed_model.encode(sentences) # Find breakpoints where cosine similarity drops breakpoints = [0] for i in range(1, len(embeddings)): sim = cosine_similarity([embeddings[i-1]], [embeddings[i]])[0][0] if sim < breakpoint_threshold: breakpoints.append(i) breakpoints.append(len(sentences)) chunks = [] for i in range(len(breakpoints) - 1): chunk = ". ".join(sentences[breakpoints[i]:breakpoints[i+1]]) if chunk.strip(): chunks.append(chunk) return chunks
Pros: Chunks correspond to coherent topics. Better precision for topic-specific queries. Cons: Slower (requires embedding sentences). Threshold requires tuning. Best for: Long technical documents, research papers, multi-topic content.
4. Document Structure-Based
pythonimport re def header_based_chunking(markdown_text: str) -> list[dict]: """Split on markdown headers, preserving hierarchy.""" sections = re.split(r'\n(#{1,3} .+)\n', markdown_text) chunks = [] current_header = "Introduction" for i, section in enumerate(sections): if re.match(r'^#{1,3} ', section): current_header = section.strip('# ').strip() elif section.strip(): chunks.append({ "text": section.strip(), "header": current_header, "level": section.count('#') if section.startswith('#') else 0 }) return chunks
Best for: Markdown documentation, HTML content with clear structure, structured reports.
5. Parent-Child (Small-to-Big)
Index small chunks for precise retrieval. Return the parent chunk (containing the small chunk) for context.
pythondef create_parent_child_chunks(text: str, parent_size: int = 1024, child_size: int = 256) -> list[dict]: parent_splitter = RecursiveCharacterTextSplitter(chunk_size=parent_size, chunk_overlap=0) child_splitter = RecursiveCharacterTextSplitter(chunk_size=child_size, chunk_overlap=32) chunks = [] parents = parent_splitter.split_text(text) for parent_id, parent in enumerate(parents): children = child_splitter.split_text(parent) for child_id, child in enumerate(children): chunks.append({ "child_text": child, # What gets embedded and searched "parent_text": parent, # What gets returned to the LLM "parent_id": parent_id, "child_id": child_id, }) return chunks
Best for: Dense technical content where small passages are often retrieved but need surrounding context.
Evaluating Chunking Quality
pythondef evaluate_chunking( chunking_fn, eval_queries: list[dict], # [{"query": "...", "expected_content": "..."}] embed_model, top_k: int = 5 ) -> float: """Measure what fraction of queries retrieve the expected content.""" hits = 0 for case in eval_queries: chunks = chunking_fn(case["document"]) chunk_embeddings = embed_model.encode([c["text"] for c in chunks]) query_emb = embed_model.encode([case["query"]])[0] sims = cosine_similarity([query_emb], chunk_embeddings)[0] top_chunks = [chunks[i] for i in np.argsort(sims)[::-1][:top_k]] if any(case["expected_content"] in c["text"] for c in top_chunks): hits += 1 return hits / len(eval_queries)
Practical Recommendations
- Start with: Fixed-size (512 tokens, 64 overlap) - works for 80% of use cases
- Switch to semantic when you see retrieval misses on topic-specific queries
- Use parent-child when retrieved chunks are too short to contain enough context for the LLM
- Always measure on your specific eval set before declaring a strategy "better"
The right chunk size is different for every corpus. Measure, do not guess.
Common Mistakes
Splitting mid-sentence at chunk boundaries. Fixed-size character splits do not respect sentence boundaries. A chunk that ends mid-sentence loses syntactic coherence, which degrades both embedding quality and the model's ability to reason over the retrieved text. Always use a recursive or sentence-aware splitter that prioritizes natural break points over exact size targets.
Using fixed character splits on markdown that breaks code blocks. Markdown code blocks are a single semantic unit; splitting inside a fenced block produces chunks that contain incomplete code snippets with mismatched delimiters. The LLM cannot use broken code effectively. Use a markdown-aware splitter that treats fenced blocks and headers as structural boundaries.
Never measuring retrieval recall after changing chunk size. Chunk size is the single most impactful hyperparameter in a RAG pipeline, yet many teams tune it by intuition. Without measuring recall@k before and after a change you cannot know whether you improved or degraded retrieval quality. Treat chunk size as a tunable hyperparameter with a defined metric.
What to Practice Next
- Implement the LangChain (or equivalent) recursive character text splitter and inspect five random chunks from a real document; verify that no chunk starts or ends mid-sentence.
- Measure recall@5 on your evaluation set with chunk sizes of 256, 512, and 1024 tokens and plot the results to find the inflection point for your corpus.
- Experiment with semantic chunking (split on embedding similarity drops) on the same document and compare recall@5 against fixed-size chunking.
Stay in the loop
Get new ML/AI lessons in your inbox.
No account needed. We will send curriculum updates, launch notes, and practical learning resources.
Related Posts
More postsFine-Tuning and Post-Training: LoRA, SFT, DPO, and Reasoning RL
What actually happens after pretraining, and when you should do any of it yourself. Parameter-efficient fine-tuning with LoRA, supervised fine-tuning data, preference optimization, and the reinforcement learning recipe behind reasoning models, with a decision framework and a project you can run on one GPU.
LLM Context Windows: What They Mean for System Design
Context window size shapes every architectural decision in LLM applications. This post covers how to reason about context allocation, the limits that still matter even with large windows, and the patterns that scale.
Common ML Architectures Reference: CNN, RNN, Transformer, MoE
A concise technical reference for the neural network architectures that power modern ML - what each one does, how it works, when to use it, and what to watch out for.