Text Splitters
Text splitters chunk documents into pieces that fit an LLM context window AND make sense semantically. Wrong-sized chunks ruin RAG retrieval — this is the chunking step most teams under-engineer.
Recursive, markdown, code, sentence
EXAMPLE
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter,
HTMLHeaderTextSplitter,
PythonCodeTextSplitter,
Language,
TokenTextSplitter,
NLTKTextSplitter,
SpacyTextSplitter,
CharacterTextSplitter,
)
from langchain_core.documents import Document
# 1) Recursive character — sensible default
splitter = RecursiveCharacterTextSplitter(
chunk_size = 1000, # characters
chunk_overlap = 200, # 10-20% overlap is typical
length_function = len,
separators = ['\n\n', '\n', '. ', ' ', ''], # try in order
add_start_index = True,
)
chunks = splitter.split_text(long_text)
# Or with Documents:
chunks = splitter.split_documents(docs)
# 2) Token-based — what the LLM actually counts
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
encoding_name = 'cl100k_base', # GPT-4 / 3.5
chunk_size = 500, # tokens
chunk_overlap = 100,
)
# Or pure token splitter:
ts = TokenTextSplitter(chunk_size=500, chunk_overlap=100)
# 3) Markdown — split on heading boundaries
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on = [
('#', 'h1'),
('##', 'h2'),
('###', 'h3'),
],
strip_headers = False, # keep headers in the chunk
)
md_chunks = md_splitter.split_text(markdown_text)
# Each chunk has metadata: {'h1': '...', 'h2': '...', 'h3': '...'}
# Common combo: split by headings first, then by chars to enforce a max size
rct = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
final_chunks = rct.split_documents(md_chunks)
# 4) HTML — split on element boundaries
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=[('h1','h1'),('h2','h2'),('h3','h3')])
html_chunks = html_splitter.split_text(html_string)
# 5) Code — language-aware
py_splitter = PythonCodeTextSplitter(chunk_size=2000, chunk_overlap=200)
py_chunks = py_splitter.split_text(python_source)
# More languages
for lang in [Language.JS, Language.TS, Language.GO, Language.RUST, Language.JAVA]:
sp = RecursiveCharacterTextSplitter.from_language(language=lang, chunk_size=2000, chunk_overlap=200)
# 6) Sentence-level — NLTK / spaCy
nltk = NLTKTextSplitter(chunk_size=1000, chunk_overlap=100)
# pip install nltk; python -m nltk.downloader punkt
sp = SpacyTextSplitter(pipeline='en_core_web_sm', chunk_size=1000, chunk_overlap=100)
# 7) Custom — for domain-specific structure (e.g. legal clauses, dialogue turns)
class DialogueSplitter:
def split_text(self, text):
return text.split('\nSPEAKER:')
# 8) Best practices — chunk-size decision tree
# - Token-based always more reliable than char-based for LLMs
# - Chunk size 500-1000 tokens for most prose
# - Overlap 10-20% of chunk size — prevents losing concepts at boundaries
# - For code: bigger chunks (1500-2000) to keep functions together
# - For Q&A pairs: small chunks (300-500); chunks should be self-contained
# 9) Preserve metadata across splits
for c in chunks:
print(c.metadata)
# split_documents copies parent metadata to each chunk. Use it for source filtering at search time.
# 10) Hybrid splitting — combine for production RAG
# Step 1: split by markdown headings (semantic boundaries)
md_chunks = MarkdownHeaderTextSplitter(headers_to_split_on=[('#','h1'),('##','h2')]).split_text(text)
# Step 2: enforce a max token size within each header section
tok = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=500, chunk_overlap=80)
final = tok.split_documents(md_chunks)
# Each chunk is bounded in size AND retains semantic context.
# 11) Edge cases
# Very long code blocks inside markdown — keep them together if they exceed chunk_size
# Tables — splitters mangle them; preprocess to keep rows together
# PDFs with footers / page numbers — strip noise before splitting
# Legal contracts with numbered sections — split on numbering pattern
# 12) Evaluate splits — sanity check
import tiktoken
enc = tiktoken.get_encoding('cl100k_base')
for c in chunks[:5]:
tokens = len(enc.encode(c.page_content))
print(f'{tokens} tokens: {c.page_content[:80]}...')
# Watch for:
# - Chunks that end mid-sentence
# - Tiny chunks (10 tokens) — splitter granularity too small
# - Huge chunks (3000+ tokens) — splitter not actually splitting
# 13) Retrieval-aware tuning
# - Bigger chunks → fewer chunks → cheaper to embed + retrieve, less precise
# - Smaller chunks → more chunks → more precise retrieval, more tokens to LLM context
# Test BOTH on your eval set; pick the chunk size where recall@k stops improving.
# 14) When NOT to chunk
# Short documents (under 1000 tokens) — embed whole
# Tabular data — load as a tool / SQL, not as text chunks
# Structured KB — store as records, retrieve by ID + assemble at query time
# 15) Modern alternatives
# - Semantic Chunking (LlamaIndex, langchain experimental): use embeddings to find natural boundaries
# - Late chunking: embed long text in one pass, then split the embeddings
# - Parent Document Retriever: small chunks for search, big chunks for context
# Late chunking — newer approach for top retrieval quality
# 1. Embed the FULL document
# 2. Extract per-token embeddings for each chunk's tokens
# 3. Mean-pool per chunk → embeddings with full-doc context
# Available via the jinaai/jina-embeddings-v3 model and similar.
# 16) Best practices summary
# • Default: RecursiveCharacterTextSplitter.from_tiktoken_encoder(500, 100)
# • Markdown/HTML: split by headings first, then enforce max size
# • Code: language-aware splitter with bigger chunks
# • Preserve + enrich metadata (source, section, last_modified)
# • Evaluate on a real eval set; tune chunk size + overlap together
Why it matters
Recursive token-based splitting with 500-1000 tokens + 10-20% overlap is the safe default. For markdown/HTML, split by headings first, then enforce a max size — preserves semantic context AND respects the LLM’s window.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150) chunks = splitter.split_documents(docs)Try it Yourself »
Discussion
Loading…