Text splitting means cutting a long document into smaller pieces called chunks. In a RAG (Retrieval-Augmented Generation) app, you embed and search chunks, not whole files. How you cut the text decides whether the right facts are found later, so chunking is one of the most important choices in a RAG pipeline.
Overview: why we split and how it works
There are three reasons to split documents:
- Context limits. An LLM can only read a limited number of tokens at once. A 300-page PDF will not fit.
- Retrieval quality. An embedding turns a text into one vector. If a chunk covers ten topics, its vector is a blurry average and matches nothing well. A focused chunk gives a sharper vector.
- Cost and speed. Sending only the three most relevant chunks to the model is cheaper and faster than sending everything.
Chunks that are too small lose context (a sentence like "It was fixed in version 2" is useless alone). Chunks that are too large dilute meaning and waste prompt space. Good chunking is a balance, and you tune it with real questions.
The recursive idea
The most-used splitter in LangChain is RecursiveCharacterTextSplitter. It takes an ordered list of separators, by default ["\n\n", "\n", " ", ""]. Internally it works like this:
- Try to split the text on the first separator (paragraph breaks).
- Pieces that are already smaller than
chunk_sizeare merged back together, as many as fit. - Any piece still too big is split again, using the next separator (line breaks), and so on down to single words and finally single characters.
The result is that paragraphs stay together when possible, then sentences or lines, then words. Meaning is broken only when there is no other choice.
Chunk size and overlap
chunk_size is the maximum length of a chunk. By default length is counted in characters. chunk_overlap is how much text the end of one chunk shares with the start of the next. Overlap helps when an important sentence sits exactly on a boundary: it appears in both chunks, so neither loses it. Too much overlap creates duplicate content and a bigger index. A common starting point is an overlap of about 10 to 20 percent of the chunk size.
Syntax and key concepts
Splitters live in the separate package langchain-text-splitters. They need no API key and work fully offline.
pip install langchain-text-splitters
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=['\n\n', '\n', ' ', ''],
add_start_index=True,
)
chunks = splitter.split_text(text) # list of str
docs = splitter.split_documents(documents) # list of Document
| Part | Meaning |
|---|---|
chunk_size |
Maximum chunk length, measured by length_function (characters by default). |
chunk_overlap |
Shared text between neighbouring chunks. Must not be larger than chunk_size. |
separators |
Ordered list of split points, tried from first to last. |
add_start_index |
Adds a start_index to each chunk's metadata: its character position in the original text. |
split_text() |
Takes a string, returns a list of strings. |
split_documents() |
Takes Document objects, returns smaller Document objects that keep (copy) the original metadata. |
Examples
Example 1: Recursive splitting by paragraph
A small text with three paragraphs and a chunk size of 60 characters.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = 'LangChain splits text.\n\nEach chunk stays small.\n\nOverlap keeps context.'
splitter = RecursiveCharacterTextSplitter(chunk_size=60, chunk_overlap=0)
for chunk in splitter.split_text(text):
print(len(chunk), repr(chunk))
Output:
47 'LangChain splits text.\n\nEach chunk stays small.'
22 'Overlap keeps context.'
The paragraphs are 22, 23 and 22 characters long. The first two fit together (22 + 2 + 23 = 47). Adding the third would make 71, which is over 60, so a new chunk starts. The splitter never cut inside a paragraph because it did not need to.
Example 2: Seeing overlap clearly
Here we use CharacterTextSplitter, which splits on one separator only. Splitting on spaces makes the overlap easy to see.
from langchain_text_splitters import CharacterTextSplitter
text = 'one two three four five six seven eight nine ten'
splitter = CharacterTextSplitter(separator=' ', chunk_size=20, chunk_overlap=8)
for i, chunk in enumerate(splitter.split_text(text)):
print(i, chunk)
Output:
0 one two three four
1 four five six seven
2 seven eight nine ten
The word four ends chunk 0 and starts chunk 1; seven does the same for chunks 1 and 2. The splitter keeps whole words from the end of the previous chunk while their total length is at most the overlap (8 characters). Overlap is therefore approximate: it is made of whole pieces, not an exact character count.
Example 3: Splitting Documents and keeping metadata
In real pipelines you split Document objects from a loader. Metadata is copied to every chunk, and add_start_index records where each chunk began.
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = 'Alpha beta gamma delta.\n\nEpsilon zeta eta theta iota.'
doc = Document(page_content=text, metadata={'source': 'demo.txt'})
splitter = RecursiveCharacterTextSplitter(
chunk_size=50, chunk_overlap=0, add_start_index=True
)
for d in splitter.split_documents([doc]):
print(d.metadata, repr(d.page_content))
Output:
{'source': 'demo.txt', 'start_index': 0} 'Alpha beta gamma delta.'
{'source': 'demo.txt', 'start_index': 25} 'Epsilon zeta eta theta iota.'
The two paragraphs are 23 and 28 characters, which together with the break would be 53, over the limit of 50. The second chunk begins at character 25 of the original text (23 characters plus the two newlines). Metadata like source lets you show citations later.
Example 4: Structure-aware splitting for Markdown
For Markdown, splitting on headers keeps each section together and stores the headings as metadata.
from langchain_text_splitters import MarkdownHeaderTextSplitter
md = '# Guide\nIntro line.\n## Install\nRun pip install.\n## Usage\nCall the function.'
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=[('#', 'h1'), ('##', 'h2')])
for doc in splitter.split_text(md):
print(doc.metadata, repr(doc.page_content))
Output:
{'h1': 'Guide'} 'Intro line.'
{'h1': 'Guide', 'h2': 'Install'} 'Run pip install.'
{'h1': 'Guide', 'h2': 'Usage'} 'Call the function.'
The header text is moved into metadata, so the retriever can filter by section or show it to the user. A common pattern is to run this splitter first, then run a RecursiveCharacterTextSplitter on the results to cap very long sections.
Under the hood: choosing a length function
Models count tokens, not characters. A token is a piece of a word; English averages roughly four characters per token, but this varies by language and tokenizer. If you must respect a token limit (for example, an embedding model's input limit), measure chunks in tokens. The from_tiktoken_encoder constructor does this and needs the tiktoken package.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=256, chunk_overlap=32
)
chunks = splitter.split_text('Your long document text goes here. ' * 100)
print(len(chunks))
Here chunk_size=256 means 256 tokens. The number printed depends on the tokenizer, so it is not shown. Other splitters exist for code (RecursiveCharacterTextSplitter.from_language), HTML, JSON and sentence-based splitting. There are also experimental semantic splitters that cut where the meaning changes. Check the current LangChain documentation for the full list.
Common Mistakes
1. Overlap larger than chunk size
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Wrong: overlap is bigger than the chunk
splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=150)
# ValueError: Got a larger chunk overlap (150) than chunk size (100), should be smaller.
Overlap is a part of a chunk, so it cannot be bigger than the chunk. Fix: use something like chunk_size=100, chunk_overlap=20.
2. Using split_text and losing metadata
pieces = splitter.split_text(doc.page_content) # plain strings, source is lost
docs = splitter.split_documents([doc]) # Documents keep the metadata
With split_text you get bare strings. Later you cannot say which file or page an answer came from. Prefer split_documents (or create_documents with a metadatas list) when you index real files.
3. Choosing sizes without testing
Copying chunk_size=1000 from a tutorial and never checking results is a very common error. Different content needs different sizes: FAQ answers want small chunks, legal contracts want larger ones. Always inspect a sample of chunks and test with real questions.
4. Counting characters when the limit is in tokens
A 2000-character chunk may exceed a model's token limit for non-English text or code. Use a token-based length function when the limit matters.
Best Practices
- Start with
RecursiveCharacterTextSplitter; it is a strong default for plain prose. - Begin with a moderate size (a few hundred to about a thousand characters) and an overlap of 10 to 20 percent, then tune using retrieval tests.
- Use structure-aware splitters (Markdown, HTML, code) when the format has natural boundaries.
- Turn on
add_start_indexand keep rich metadata: source, page, section title. - Print and read a sample of chunks before embedding thousands of them. Cutting mid-sentence is easy to spot by eye.
- Keep the splitting settings in one config so you can re-index consistently when you change them. Changing chunking means you must re-embed.
- Do not over-engineer: measure answer quality first, then try fancier strategies only if needed.
Practice Exercises
- Split a text of about five paragraphs with
chunk_size=80andchunk_overlap=0. Print each chunk's length and confirm none is above 80. Hint: uselen(chunk). - Take the word-splitting example and change the overlap to 0, then to 12. Predict the chunks before running, then compare.
- Write a Markdown string with three
##sections. Split it withMarkdownHeaderTextSplitter, then pass the result throughRecursiveCharacterTextSplitter.split_documentswith a smallchunk_size. Check that the header metadata is kept on the smaller chunks.
Summary
- Chunking makes documents fit model limits and makes retrieval more precise.
RecursiveCharacterTextSplittertries paragraphs, then lines, then words, then characters, keeping meaning together.chunk_sizeis the maximum size;chunk_overlapshares text across boundaries and must not exceed the size.- Use
split_documentsto keep metadata, andadd_start_indexto track position. - Use Markdown or code splitters for structured content, and token-based lengths when token limits matter.
- Tune chunking with real questions, and re-index whenever you change it.
