Chunking documents well
Search finds chunks, not whole documents. How you cut the text decides what can be found. Learn size, overlap, splitting by headings and useful metadata.
In 60 seconds
Chunking documents well
Search finds chunks, not whole documents. How you cut the text decides what can be found. Learn size, overlap, splitting by headings and useful metadata.
Size: the main trade-off
| Chunks that are… | Good | Bad |
|---|---|---|
| Too small (a sentence or two) | Precise matches | Missing context: "This is allowed only once a year". What is "this"? |
| Too big (many pages) | Plenty of context | One vector must cover many topics, so it matches nothing well; costs more tokens |
| About right (often a few paragraphs, one idea) | Makes sense alone and matches clearly | Needs testing on your data |
Overlap
Split by structure, not only by length
- 1
Find the structure
Headings, clause numbers, FAQ questions, slide titles. - 2
Split by section
Each section becomes a candidate chunk. - 3
Split long sections
Cut by paragraph, then by length with a small overlap. - 4
Merge tiny pieces
Join very short sections with their neighbour so no chunk is just a heading. - 5
Add context and metadata
Put the document title and section heading on every chunk.
def split_sections(text):
"""Split text into (heading, body) pairs at lines starting with '#'."""
sections, heading, lines = [], "Untitled", []
for line in text.splitlines():
if line.startswith("#"):
if lines:
sections.append((heading, " ".join(lines)))
heading, lines = line.lstrip("#").strip(), []
elif line.strip():
lines.append(line.strip())
if lines:
sections.append((heading, " ".join(lines)))
return sections
def chunk(text, doc_id, title, max_words=80, overlap=15):
chunks = []
for heading, body in split_sections(text):
words = body.split()
start = 0
while True:
piece = " ".join(words[start:start + max_words])
chunks.append({
"id": f"{doc_id}#{len(chunks)}",
"doc_id": doc_id,
"section": heading,
# Title and heading go into the text that gets embedded.
"text": f"{title} > {heading}: {piece}",
})
if start + max_words >= len(words):
break
start += max_words - overlap
return chunks
policy = """# Leave policy
Staff get 12 days of casual leave each year.
# Carry forward
Unused casual leave cannot be carried to the next year."""
for c in chunk(policy, "hr-leave-v3", "HR Leave Policy 2025"):
print(c["id"], "|", c["text"])HR Leave Policy 2025 > Carry forward: Unused casual leave…. Without the title and heading, the chunk "Unused casual leave cannot be carried…" would not say which policy or which organisation it belongs to.Metadata: the labels on each chunk
| Field | Why it helps |
|---|---|
| Document id and chunk id | Citations, updates and deletions |
| Title, section, page number | Show the source; add context to the chunk |
| Date and version | Prefer the newest; filter out old versions |
| Language | Route queries; pick the right model or re-ranker |
| Type (policy, FAQ, manual, email) | Filter searches; show different answer styles |
| Access groups | Permission filtering at search time (see Securing RAG) |
- Fixed 500 characters, cutting words and tables in half
- No title or heading on the chunk
- No page number, so citations cannot point anywhere
- The same chunk size for FAQs and 200-page manuals
- Split by headings or clauses, then by length
- Title and heading added to each chunk's text
- Page, section and version stored as metadata
- Settings chosen by testing on real questions
Watch and read more
Lab
A chunker that splits by headings, adds context, and prints chunks you have checked by eye.
The problem
Use the code from the lesson (Python 3, standard library only).
- Write a made-up rules document of 6–10 sections with
#headings, for example a housing society's rules. Make one section long (150+ words) and one section only one line. - Run the chunker with
max_words=80, overlap=15. Read every chunk. Does each make sense alone? - Add a step that merges any section shorter than 15 words into the next section.
- Add a
pageorversionfield to each chunk's metadata. - Write three questions a resident might ask, and for each, which chunk id should answer it.
You are done when
Hard questions
Try to answer before you reveal. If you can answer these, you understood the lesson.
Q1Why put the title and heading into the text that gets embedded, and not only into metadata?Reveal
The embedding model only sees the text you give it. If "Carry forward" and "HR Leave Policy" appear only in metadata, the vector for the chunk does not include that meaning, so a question like "can I carry forward casual leave?" may match it less well. Metadata is good for filters and citations; context that helps matching must be in the embedded text.
Q2An FAQ page has 40 short question-and-answer pairs. How would you chunk it, and why?Reveal
Make each question with its answer one chunk. The pair is a complete idea, and the question text often matches how users ask. Splitting by a fixed length could cut a question away from its answer or join two unrelated answers. Add the page title and a category as context, and very short pairs can stay short because they are complete.
Questions people ask
Should I count chunk size in words, characters or tokens?
Tokens are what the embedding model and the language model actually count, so they are the most accurate. Words are a simple approximation to start with. Note that the same text in many Indian scripts often uses more tokens than in English, so a "word" budget may not match across languages.
What about chunks that are only a heading?
Merge them with the next section. A chunk that says only "Annexure B" matches many questions weakly and answers none.
Can a model do the chunking for me?
Yes, some teams ask a model to find topic boundaries or write a one-line summary for each chunk. This can help with messy text, but it costs more and can make mistakes. Compare it with simple structure-based splitting on your test set.
How do I know if my chunking is good?
Measure retrieval on a set of real questions with known right answers, then change one setting at a time. The last lesson in this track shows how.
Lesson test
5 questions. Get 3 right (60%) to pass and complete this lesson.