Why Split Documents?
LLMs have a fixed context window (e.g. 128 k tokens for GPT-4o). Embedding models also have limits — typically 512 to 8 192 tokens. Documents longer than these limits must be split into chunks before indexing.
chunk_size
Maximum characters (or tokens) per chunk. Controls granularity.
chunk_overlap
Characters repeated between adjacent chunks. Preserves context at boundaries.
separators
Ordered list of split points. Tries larger separators first.
Golden rule: chunks should be large enough to contain a complete thought, but small enough that the embedding captures one topic. Start with chunk_size=1000 and chunk_overlap=200, then tune based on your retrieval quality.
RecursiveCharacterTextSplitter
The recommended default for most use cases. It tries a list of separators in order (\n\n, \n, , "") and recurses until chunks are small enough.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
length_function=len, # character count
add_start_index=True, # adds 'start_index' to metadata
)
# Split raw text
text = "LangChain is a framework for building LLM-powered applications..."
chunks = splitter.split_text(text)
print(len(chunks)) # list[str]
# Split Documents (preserves metadata)
from langchain_community.document_loaders import PyPDFLoader
pages = PyPDFLoader("paper.pdf").load()
docs = splitter.split_documents(pages)
print(docs[0].metadata) # {'source': 'paper.pdf', 'page': 0, 'start_index': 0}
CharacterTextSplitter
Splits on a single separator only. Simpler than recursive, but can produce uneven chunks if the separator is rare.
from langchain_text_splitters import CharacterTextSplitter
splitter = CharacterTextSplitter(
separator="\n\n", # only split on double newlines
chunk_size=1000,
chunk_overlap=100,
)
chunks = splitter.split_text(long_text)
Use this when your text has a clear, consistent structure — like paragraphs separated by blank lines.
TokenTextSplitter
Splits by token count rather than character count. Essential when your embedding model or LLM has a token limit (not a character limit).
# pip3 install tiktoken
from langchain_text_splitters import TokenTextSplitter
splitter = TokenTextSplitter(
encoding_name="cl100k_base", # same tokenizer as GPT-4 / text-embedding-3
chunk_size=512, # tokens per chunk
chunk_overlap=50,
)
chunks = splitter.split_text(text)
# Alternatively, use model name directly
splitter = TokenTextSplitter.from_tiktoken_encoder(
model_name="gpt-4o",
chunk_size=512,
chunk_overlap=50,
)
When to use: When you're embedding with text-embedding-3-small (8 192 token limit) or passing chunks directly to an LLM with a tight context window. Token count is more reliable than character count for limit enforcement.
MarkdownHeaderTextSplitter
Splits Markdown documents on header boundaries and adds header text to each chunk's metadata. Ideal for documentation sites and wikis.
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
docs = splitter.split_text(markdown_text)
# Each doc contains header info in metadata
# metadata: {'h1': 'Introduction', 'h2': 'Installation'}
# page_content: the section text under that header
print(docs[0].metadata)
# Chain with recursive splitter for large sections
from langchain_text_splitters import RecursiveCharacterTextSplitter
secondary = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
final_docs = secondary.split_documents(docs)
Splitter Comparison
| Splitter | Best For | Split Unit |
|---|---|---|
| RecursiveCharacterTextSplitter | General text (default choice) | Characters |
| CharacterTextSplitter | Structured paragraphs | Characters |
| TokenTextSplitter | LLM / embedding token limits | Tokens |
| MarkdownHeaderTextSplitter | Markdown docs / wikis | Headers |
| HTMLHeaderTextSplitter | HTML pages | HTML tags |
| SemanticChunker | Meaning-based splits | Embedding similarity |
Full Load → Split Pipeline
Splitting sits between loading and embedding in a RAG pipeline. Start by reading source files with Document Loaders, split the resulting documents here, then turn each chunk into a vector with Embeddings before storing them for retrieval.
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# 1. Load
loader = PyPDFLoader("handbook.pdf")
pages = loader.load()
print(f"Loaded {len(pages)} pages")
# 2. Split
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True,
)
chunks = splitter.split_documents(pages)
print(f"Split into {len(chunks)} chunks")
# 3. Embed and store
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
print("Indexed successfully")
LangChain Text Splitters FAQ
What chunk size should I use for text splitting in RAG?
Start with a chunk_size of 1000 characters and a chunk_overlap of 200, then tune from there. Chunks that are too large dilute the embedding so precise facts get lost; chunks that are too small lose surrounding context. A good chunk holds one complete thought while staying focused on a single topic so the embedding stays sharp.
What is chunk overlap and why does it matter?
chunk_overlap is the number of characters or tokens repeated between adjacent chunks. It prevents an answer from being split across a chunk boundary and lost, because the overlapping window keeps shared context in both neighbouring chunks. A common starting point is 10 to 20 percent of chunk_size, such as 200 overlap for a 1000 character chunk.
What is the difference between RecursiveCharacterTextSplitter and CharacterTextSplitter?
RecursiveCharacterTextSplitter tries an ordered list of separators (paragraphs, then lines, then spaces, then characters) and recurses until each chunk fits, so it rarely cuts mid-sentence and is the recommended default. CharacterTextSplitter splits on a single separator only, which is simpler but can produce very uneven chunks when that separator is rare in the text.
Should I split text by tokens or by characters?
Split by characters with RecursiveCharacterTextSplitter for general prose because it is fast and predictable. Split by tokens with TokenTextSplitter when you must respect a model token limit, such as an embedding model with an 8192 token window or an LLM with a tight context window, since token count enforces those limits far more reliably than character count.
How do I split code or Markdown documents in LangChain?
For Markdown use MarkdownHeaderTextSplitter to split on header boundaries and store each heading in metadata, then optionally re-split large sections with RecursiveCharacterTextSplitter. For source code, use RecursiveCharacterTextSplitter.from_language with the right Language enum so it splits on function and class boundaries instead of arbitrary characters, which keeps code blocks intact.
Why is chunking so important for RAG retrieval quality?
Retrieval matches the query embedding against chunk embeddings, so the way you chunk directly controls what the model can find. Well sized, topically focused chunks produce sharp embeddings and precise retrieval, while oversized or fragmented chunks blur the signal and surface irrelevant passages. Tuning chunk_size, chunk_overlap, and the splitter is often the highest leverage fix for poor RAG accuracy.