LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnCore LLM FoundationsChunking Strategies
🔍MediumRAG & Retrieval

Chunking Strategies

Turn clean documents into retrieval units that preserve answers, citations, and measurable search quality.

18 min read
Learning path
Step 58 of 196 in the full curriculum
File Ingestion for AILLM Benchmarks & Limitations

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

In File Ingestion for AI, you already turned incident-runbook-v3.pdf into a located, checksummed record:

incident-runbook-v3.pdf, page 7: Rollback failure: page on-call within 15 minutes with deploy ID.

That sentence is evidence only while its pieces stay together. Split it so one chunk has "Rollback failure" and another has "15 minutes with deploy ID," and a search hit can retrieve the condition without the deadline, or the deadline without the identifier the on-call must include.

Chunking is the cut that turns those records into searchable evidence units. In a retrieval-augmented generation (RAG) system, a retriever selects passages from an external index and the generator answers from those passages.[1]Reference 1Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks.https://arxiv.org/abs/2005.11401 Your chunk boundaries decide what a retrieved passage can prove.

Rollback-failure runbook compared across a broken fixed cut and a section boundary: the fragment keeps the condition but drops the 15-minute deadline and deploy ID, while the section chunk keeps condition, deadline, ID, and the page=7 locator.
The same page=7 line, two cuts. The fixed fragment can rank, but it can't prove the paging rule. The section chunk keeps condition, deadline, identifier, and locator together.

What a useful chunk must preserve

Before comparing splitters, define success in answer terms. Suppose an engineer asks, "A rollback failed after deploy pay-742. When do I page, and what ID do I include?"

Candidate retrieval unitSearchable?Answerable?Problem
Rollback failure: page on-call withinyesnodeadline and required ID are missing
15 minutes with deploy ID. Routine deploy notes: archive within 14 days.yesnocondition is missing and a competing rule is present
Rollback failure: page on-call within 15 minutes with deploy ID.yesyespreserves condition, deadline, and required ID

The first engineering requirement isn't "make chunks small." It's "make each retrieved chunk a defensible piece of evidence."

check-answerable-chunks.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class Chunk: 5 chunk_id: str 6 text: str 7 source_id: str 8 locator: str 9 10def is_answerable(chunk: Chunk) -> bool: 11 required = ["rollback failure", "15 minutes", "deploy id"] 12 lowered = chunk.text.lower() 13 return all(phrase in lowered for phrase in required) 14 15chunks = [ 16 Chunk("broken-condition", "Rollback failure: page on-call within", "incident-runbook-v3.pdf", "page=7"), 17 Chunk("broken-window", "15 minutes with deploy ID. Routine deploy notes archive within 14 days.", "incident-runbook-v3.pdf", "page=7"), 18 Chunk("complete", "Rollback failure: page on-call within 15 minutes with deploy ID.", "incident-runbook-v3.pdf", "page=7"), 19] 20 21results = [is_answerable(chunk) for chunk in chunks] 22for chunk, ok in zip(chunks, results): 23 print(f"{chunk.chunk_id}: answerable={ok}") 24assert results == [False, False, True]
Output
1broken-condition: answerable=False 2broken-window: answerable=False 3complete: answerable=True

A substring check is only a stand-in for a harder test. If this chunk is the only passage the generator sees, can it cite the 15-minute paging rule without guessing?

So what happens if you ignore the heading ingestion already preserved and cut the page by word count instead?

Fixed windows show the boundary failure

Start with a mechanical baseline: windows of tokens. If a window has size NNN and overlap OOO, the next window starts after N−ON - ON−O tokens. Overlap repeats boundary text, which can help continuity, but it can't guarantee that a complete policy rule survives.

The labs below use whitespace-separated words as visible token stand-ins. A production pipeline should measure length with the tokenizer used by its embedding model and respect that model's max_seq_length. Word counts aren't embed tokens. Overflow past the model limit truncates meaning silently at encode time.

Predict the first window before running the splitter. The runbook line has 17 words. With N=6N = 6N=6, that first window reads Rollback failure page on-call within 15. The deadline starts at word 7 (minutes), so this cut already loses the paging rule.

split-fixed-windows.py
1def fixed_windows(text: str, size: int, overlap: int) -> list[str]: 2 if size <= 0 or overlap < 0 or overlap >= size: 3 raise ValueError("require size > overlap >= 0") 4 words = text.split() 5 step = size - overlap 6 return [ 7 " ".join(words[start : start + size]) 8 for start in range(0, len(words), step) 9 if words[start : start + size] 10 ] 11 12policy = ( 13 "Rollback failure page on-call within 15 minutes with deploy ID. " 14 "Routine deploy notes archive within 14 days." 15) 16 17for index, chunk in enumerate(fixed_windows(policy, size=6, overlap=2)): 18 print(f"{index}: {chunk}")
Output
10: Rollback failure page on-call within 15 21: within 15 minutes with deploy ID. 32: deploy ID. Routine deploy notes archive 43: notes archive within 14 days. 54: days.

Window 3 (notes archive within 14 days.) exposes a second failure: an orphan tail can lose the condition that gives a value meaning. The final days. fragment makes that loss especially obvious. A production fallback should merge an undersized tail into the previous window or send it for review instead of indexing it blindly.

Why shouldn't the trailing notes archive within 14 days. window enter the index by itself?

Answer

It no longer says that the archive window belongs to routine deploy notes. A retriever could surface the number without the runbook condition needed to cite or apply it correctly.

Now measure what overlap bought you. Predict the outcome first: more repeated words should increase indexed text, but a rule becomes useful only when one window carries all three required pieces. Size 7 looks generous on a 17-word page. It still never holds the full rollback rule.

measure-overlap-cost-and-coverage.py
1def fixed_windows(text: str, size: int, overlap: int) -> list[str]: 2 words = text.split() 3 step = size - overlap 4 return [ 5 " ".join(words[start : start + size]) 6 for start in range(0, len(words), step) 7 if words[start : start + size] 8 ] 9 10def carries_rollback_rule(text: str) -> bool: 11 lowered = text.lower() 12 return all(term in lowered for term in ["rollback failure", "15 minutes", "deploy id"]) 13 14policy = "Rollback failure page on-call within 15 minutes with deploy ID. Routine deploy notes archive within 14 days." 15for overlap in [0, 2, 4]: 16 chunks = fixed_windows(policy, size=7, overlap=overlap) 17 complete = sum(carries_rollback_rule(chunk) for chunk in chunks) 18 indexed_words = sum(len(chunk.split()) for chunk in chunks) 19 print(f"overlap={overlap}: chunks={len(chunks)} indexed_words={indexed_words} complete={complete}") 20 assert complete == 0
Output
1overlap=0: chunks=3 indexed_words=17 complete=0 2overlap=2: chunks=4 indexed_words=23 complete=0 3overlap=4: chunks=6 indexed_words=35 complete=0
Overlap experiment on size-7 rollback-runbook windows: indexed words rise from 17 to 23 to 35 as overlap goes 0, 2, then 4, while complete rollback rules stay at 0 in every setting.
On this runbook, raising overlap from 0 to 4 stores 17, then 23, then 35 words. Complete rollback rules stay at zero. Cost moved. The missing condition didn't.

Treat overlap as a knob you measure, not a default you copy from a tutorial.

🔬 Research insight: On Natural Questions with a sparse retriever, Bennani and Moslonka found that 10-20% overlap didn't move BERTScore or exact match, while chunk count grew by about 1/(1−r)1/(1-r)1/(1−r).[2]Reference 2A Systematic Analysis of Chunking Strategies for Reliable Question Answering.https://arxiv.org/abs/2601.14123 That's Wikipedia plus SPLADE, not your headed runbook. The lab above is the matching local test: more overlap, same missing rule.

Ingestion already kept headings and source locations. If word windows can't reconstruct the rule, those headings give us a better boundary to test.

Prefer policy structure when you have it

Overlap made more copies but never restored the rule. Use the source's own structure when it tells you which text belongs together.

LangChain's RecursiveCharacterTextSplitter is a common generic-text baseline. It tries separators in order, defaulting to ["\n\n", "\n", " ", ""], so paragraphs stay together before sentences, then words.[3]Reference 3RecursiveCharacterTextSplitterhttps://docs.langchain.com/oss/python/integrations/splitters/recursive_text_splitter

That splitter measures size with length_function, which defaults to Python len (characters), not embedding tokens. Start with explicit heading boundaries when the source provides them. Use the smaller-cut fallback only for sections that still exceed your size limit, and measure that limit with the embedding tokenizer.

split-markdown-policy-sections.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class PolicyChunk: 5 heading: str 6 body: str 7 indexed_text: str 8 locator: str 9 10def headed_chunks(markdown: str, source_locator: str) -> list[PolicyChunk]: 11 chunks: list[PolicyChunk] = [] 12 heading = "Document" 13 body: list[str] = [] 14 15 def flush() -> None: 16 if body: 17 body_text = " ".join(body) 18 chunks.append( 19 PolicyChunk( 20 heading, 21 body_text, 22 f"{heading}\n{body_text}", 23 f"{source_locator}#{heading.lower().replace(' ', '-')}", 24 ) 25 ) 26 body.clear() 27 28 for line in markdown.strip().splitlines(): 29 if line.startswith("## "): 30 flush() 31 heading = line[3:] 32 elif line.strip(): 33 body.append(line.strip()) 34 flush() 35 return chunks 36 37policy = """## Rollback failure 38Page on-call within 15 minutes with deploy ID. 39## Routine deploy notes 40Archive within 14 days after release.""" 41 42for chunk in headed_chunks(policy, "page=7"): 43 print(chunk.indexed_text.replace("\n", " | "), "|", chunk.locator)
Output
1Rollback failure | Page on-call within 15 minutes with deploy ID. | page=7#rollback-failure 2Routine deploy notes | Archive within 14 days after release. | page=7#routine-deploy-notes

Each chunk's indexed_text includes its heading, so searchable text keeps the condition attached to the deadline. The locator separately preserves the path back to original evidence.

Tables create the next boundary test. Splitting a row away from its column names turns exact information into ambiguous fragments.

keep-policy-table-with-header.py
1table = [ 2 "| Condition | Window | Required evidence |", 3 "| --- | --- | --- |", 4 "| Rollback failure | 15 minutes | Deploy ID |", 5 "| Routine deploy notes | 14 days | Release summary |", 6] 7 8def table_chunk(lines: list[str], heading: str) -> dict[str, str]: 9 return { 10 "heading": heading, 11 "text": "\n".join(lines), 12 "quality_check": "header_present" if lines[0].startswith("| Condition |") else "review", 13 } 14 15chunk = table_chunk(table, "Runbook windows") 16print(chunk["heading"], chunk["quality_check"], f"rows={len(table) - 2}") 17print("15 minutes" in chunk["text"] and "Deploy ID" in chunk["text"]) 18assert chunk["quality_check"] == "header_present"
Output
1Runbook windows header_present rows=2 2True

For a long section, split inside it while carrying its heading and original locator into every child chunk. Merge a tiny final window back into the preceding child so the fallback doesn't emit an orphan fragment.

split-long-section-with-provenance.py
1def section_windows( 2 text: str, heading: str, source: str, size: int, min_tail_words: int 3) -> list[dict[str, str]]: 4 words = text.split() 5 windows = [words[start : start + size] for start in range(0, len(words), size)] 6 if len(windows) > 1 and len(windows[-1]) < min_tail_words: 7 windows[-2].extend(windows.pop()) 8 9 return [ 10 { 11 "text": " ".join(window), 12 "heading": heading, 13 "source": source, 14 "chunk_id": f"{source}#{heading.lower().replace(' ', '-')}-{index}", 15 } 16 for index, window in enumerate(windows) 17 ] 18 19children = section_windows( 20 "Page on-call within 15 minutes with deploy ID. Include the service name and rollback attempt.", 21 heading="Rollback failure", 22 source="incident-runbook-v3.pdf:page=7", 23 size=7, 24 min_tail_words=4, 25) 26 27for child in children: 28 print(child["chunk_id"], "|", child["heading"], "|", child["text"])
Output
1incident-runbook-v3.pdf:page=7#rollback-failure-0 | Rollback failure | Page on-call within 15 minutes with deploy 2incident-runbook-v3.pdf:page=7#rollback-failure-1 | Rollback failure | ID. Include the service name and rollback attempt.

The heading and locator on a child record are provenance, not a substitute for answerable text. The first child's text still severs deploy from ID, so it fails the earlier answerability check if you embed that field alone. Before indexing children:

  1. Prepend the section heading (and any required condition) into the string you embed, as in the prepend-section-context lab below, and/or
  2. Treat incomplete children as search candidates only when parent expansion is mandatory on every hit.

Never ship orphan spans that fail answerability alone and rely on metadata fields the retriever never sees.

The headed chunks look right on the page. That still doesn't prove a retriever will select them or that selected text can answer.

Evaluate boundaries with labeled questions

Once a chunk looks coherent, test whether retrieval can find it. A chunking strategy isn't good because it sounds fancy. It works when labeled questions retrieve complete supporting evidence.

Start with a tiny, transparent score before involving a vector database. For the query terms {rollback, failure, deploy, minutes}, the complete rollback-runbook chunk matches all four. A fragment that contains only {rollback, failure} may rank, but it can't answer the question.

ChunkMatching query termsContains answer?
complete rollback rule4 / 4yes
rollback-condition fragment2 / 4no
routine archive rule1 / 4no

This lexical scorer is deliberately simple. It isolates the effect of boundaries. Replace its score with real embeddings after the fixture and expected evidence are stable.

retrieve-complete-evidence.py
1import re 2 3def terms(text: str) -> set[str]: 4 return set(re.findall(r"[a-z0-9]+", text.lower())) 5 6def retrieve(query: str, chunks: list[dict[str, str]]) -> dict[str, str]: 7 query_terms = terms(query) 8 return max(chunks, key=lambda chunk: len(query_terms & terms(chunk["text"]))) 9 10query = "rollback failure deploy minutes" 11chunks = [ 12 {"id": "rollback-rule", "text": "Rollback failure: page on-call within 15 minutes with deploy ID."}, 13 {"id": "routine-rule", "text": "Routine deploy notes: archive within 14 days."}, 14] 15 16hit = retrieve(query, chunks) 17print(hit["id"], hit["text"]) 18assert hit["id"] == "rollback-rule"
Output
1rollback-rule Rollback failure: page on-call within 15 minutes with deploy ID.

Now compare a broken fixed-window configuration against a section-aware configuration using the same question and the same expected evidence phrase.

compare-chunking-configurations.py
1import re 2 3def terms(text: str) -> set[str]: 4 return set(re.findall(r"[a-z0-9]+", text.lower())) 5 6def top_chunk(query: str, chunks: list[str]) -> str: 7 query_terms = terms(query) 8 return max(chunks, key=lambda text: len(query_terms & terms(text))) 9 10query = "rollback failure deploy id" 11expected = "Rollback failure: page on-call within 15 minutes with deploy ID." 12configs = { 13 "broken-fixed": [ 14 "Rollback failure: page on-call within", 15 "15 minutes with deploy ID. Routine deploy notes: archive within 14 days.", 16 ], 17 "section-aware": [ 18 "Rollback failure: page on-call within 15 minutes with deploy ID.", 19 "Routine deploy notes: archive within 14 days.", 20 ], 21} 22 23for name, chunks in configs.items(): 24 hit = top_chunk(query, chunks) 25 print(f"{name}: complete={expected in hit}")
Output
1broken-fixed: complete=False 2section-aware: complete=True

One question proves little. Ship a small labeled set containing policy exceptions, tables, and boundary failures, then measure each candidate splitter on exactly that set.

run-chunk-regression-suite.py
1import re 2 3def terms(text: str) -> set[str]: 4 return set(re.findall(r"[a-z0-9]+", text.lower())) 5 6def retrieve(query: str, chunks: list[dict[str, str]]) -> dict[str, str]: 7 query_terms = terms(query) 8 return max(chunks, key=lambda chunk: len(query_terms & terms(chunk["text"]))) 9 10chunks = [ 11 {"id": "rollback", "text": "Rollback failure: page on-call within 15 minutes with deploy ID."}, 12 {"id": "routine", "text": "Routine deploy notes: archive within 14 days."}, 13 {"id": "commander", "text": "Commander handoff requires the active incident channel."}, 14] 15cases = [ 16 ("rollback failure deploy id", "rollback", "15 minutes"), 17 ("routine deploy archive", "routine", "14 days"), 18 ("commander incident channel", "commander", "active incident"), 19] 20 21passed = 0 22for query, expected_id, evidence in cases: 23 hit = retrieve(query, chunks) 24 ok = hit["id"] == expected_id and evidence in hit["text"] 25 passed += int(ok) 26 print(query, "PASS" if ok else "FAIL") 27print(f"summary={passed}/{len(cases)}") 28assert passed == len(cases)
Output
1rollback failure deploy id PASS 2routine deploy archive PASS 3commander incident channel PASS 4summary=3/3

When you replace this lexical baseline with embeddings, the assertions stay useful: retrieve the correct source and retain text sufficient to answer.

Sometimes the right search unit is smaller than the unit a faithful answer needs. That's a different failure from a broken boundary, and it needs a different design.

Search small, answer with enough context

Some questions match a narrow sentence, while a faithful answer needs its surrounding section. A parent-child design indexes small children for search and stores a pointer to the larger source section returned for generation.[4]Reference 4ParentDocumentRetrieverhttps://reference.langchain.com/python/langchain-classic/retrievers/parent_document_retriever/ParentDocumentRetriever

The parent can be a section or the whole document. For this runbook, expand to the section, not the entire handbook.

Diagram showing Clean page record, Rollback runbook section, split for index, and Small indexed child.
Clean page record, Rollback runbook section, split for index, and Small indexed child.
expand-child-match-to-parent-context.py
1parents = { 2 "rollback": "Rollback failure: page on-call within 15 minutes with deploy ID. Include the service name.", 3 "routine": "Routine deploy notes: archive within 14 days after release.", 4} 5children = [ 6 {"text": "15 minutes with deploy ID", "parent_id": "rollback"}, 7 {"text": "14 days after release", "parent_id": "routine"}, 8] 9 10match = next(child for child in children if "deploy ID" in child["text"]) 11print(match["text"]) 12print(parents[match["parent_id"]]) 13assert "Rollback failure" in parents[match["parent_id"]]
Output
115 minutes with deploy ID 2Rollback failure: page on-call within 15 minutes with deploy ID. Include the service name.

Parent expansion can over-pack. In the same Natural Questions study, generation quality dropped once the filled context went past about 2.5k tokens on that QA setup.[2]Reference 2A Systematic Analysis of Chunking Strategies for Reliable Question Answering.https://arxiv.org/abs/2601.14123 Keep a generation budget in the regression suite, and don't expand a child hit into the whole PDF.

Child windows inside a longer section also benefit from their section label. Embedding a child with a contextual header is a cheap hypothesis to test against your labeled set, not a promise of improvement.

prepend-section-context.py
1def indexed_text(heading: str, text: str, source: str) -> str: 2 return f"Source: {source}\nSection: {heading}\n{text}" 3 4child = indexed_text( 5 heading="Rollback failure", 6 text="Page on-call within 15 minutes with deploy ID.", 7 source="Incident Runbook", 8) 9print(child) 10assert "Section: Rollback failure" in child
Output
1Source: Incident Runbook 2Section: Rollback failure 3Page on-call within 15 minutes with deploy ID.

Some leftover failures aren't a size problem. The paragraph switches topic, or a local span is unreadable without a name that appeared pages earlier. Those are the cases that justify a more expensive splitter, and only after the baseline has been measured.

Escalate only when the baseline exposes a gap

Structure-aware chunks handle many handbooks and policy pages. Some corpora force different choices:

Failure after measuring baselineCandidate experimentWhat must still be checked
one paragraph shifts between multiple topicssemantic boundary detectionhard size cap and labeled-query score
tiny match lacks surrounding explanationparent-child expansiondeduplication and generation budget
short child is ambiguous without its sectioncontextual headerretrieval comparison against no-header baseline
meaning depends on far-away document contextlate chunkingmodel support, latency, and measured retrieval gain

On unstructured Wikipedia pages, sentence-preserving splits matched semantic chunking up to roughly 5k generation tokens.[2]Reference 2A Systematic Analysis of Chunking Strategies for Reliable Question Answering.https://arxiv.org/abs/2601.14123 A headed runbook is a different document. Use the headings first, and treat semantic splits as an experiment for leftover topic shifts.

Semantic chunking proposes boundaries where neighboring sentence representations change sharply. The next lab uses transparent topic vectors, so you can see the boundary without trusting an external embedding service.

Count whole words, not substrings. Otherwise id matches incident, and the cosine stops meaning what you think.

find-semantic-boundary-candidates.py
1import math 2import re 3 4def vector(sentence: str) -> list[float]: 5 words = set(re.findall(r"[a-z0-9]+", sentence.lower())) 6 return [ 7 float(sum(word in words for word in ["rollback", "deploy", "minutes", "id"])), 8 float(sum(word in words for word in ["commander", "handoff", "channel"])), 9 ] 10 11def cosine(left: list[float], right: list[float]) -> float: 12 dot = sum(a * b for a, b in zip(left, right)) 13 left_norm = math.sqrt(sum(a * a for a in left)) 14 right_norm = math.sqrt(sum(b * b for b in right)) 15 return dot / (left_norm * right_norm) if left_norm and right_norm else 0.0 16 17sentences = [ 18 "Rollback failures require deploy IDs.", 19 "Page on-call within 15 minutes.", 20 "Commander handoff uses the active incident channel.", 21] 22 23for left, right in zip(sentences, sentences[1:]): 24 similarity = cosine(vector(left), vector(right)) 25 print(f"{similarity:.2f}", "boundary" if similarity < 0.50 else "keep together")
Output
11.00 keep together 20.00 boundary

The first two sentences share the rollback/deploy topic axis, so cosine stays 1.00. The commander sentence shares none of those words, so cosine drops to 0.00 and the lab marks a boundary. You'd still cap chunk size and re-score the labeled queries. A prettier boundary that empties the answer out of both sides is still a failed split.

Late chunking is a different escalation. Günther et al. still run a chunker to get boundary cues. They encode the longer text through the transformer first, then mean-pool the token embeddings that fall inside each span.[5]Reference 5Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models.https://arxiv.org/abs/2409.04701

The pooled ranges stay the same. What changes is the context inside each child vector: a later sentence that only says "the city" can still sit near "Berlin" in embedding space because the full document was encoded first.

You need a long-context embedder that exposes those token states, typically through mean pooling. If the document is longer than the embedder window, use the paper's long-document variant rather than silently truncating. Keep late chunking only when it improves the labeled set enough to pay for its extra compute.

Early versus late chunking on the same eight-token runbook span. Early encoding lights only within-chunk cells, so the 15-minute plus deploy-ID range can't see Rollback failure. Late encoding runs the full document first, then mean-pools that same range, so the middle vector can carry neighboring context.
The dashed box is the same 15-minute plus deploy-ID range in both panels. Early encoding isolates that block. Late encoding mean-pools it after the whole page has been contextualized. Keep late chunking only when that extra context improves measured retrieval.

Ship a chunking decision, not a guess

For this runbook, a first release can look like this:

Design choiceInitial decisionEvidence to collect
Default boundaryheading-aware sections, recursive fallbackanswerable-chunk rate and retrieval regression suite
Size budgetmeasure with the embedding tokenizer; stay under max_seq_lengthno silent truncation on policy fixtures; embed-token length ≠ word count
Tableskeep header plus rows togetherexact-value questions preserve correct row meaning
Overlapoff for complete policy sections; test for fallback textindex size, duplicate hits, and labeled-query results
Metadatasource, locator, heading, checksumcited answer can return to original record
Escalationparent expansion before semantic or late chunkingfailure examples that justify extra complexity

Your release gate can be encoded as an executable manifest check.

gate-indexable-policy-chunks.py
1from hashlib import sha256 2 3chunks = [ 4 { 5 "id": "rollback", 6 "text": "Rollback failure: page on-call within 15 minutes with deploy ID.", 7 "source": "incident-runbook-v3.pdf", 8 "locator": "page=7#rollback-failure", 9 "heading": "Rollback failure", 10 "quality": "ready", 11 }, 12 { 13 "id": "broken", 14 "text": "15 minutes with deploy ID.", 15 "source": "incident-runbook-v3.pdf", 16 "locator": "page=7#fragment", 17 "heading": "Fragment", 18 "quality": "review", 19 }, 20] 21 22for chunk in chunks: 23 chunk["checksum"] = sha256(chunk["text"].encode()).hexdigest() 24 25required_metadata = ("source", "locator", "heading", "checksum") 26indexable = [ 27 chunk for chunk in chunks 28 if chunk["quality"] == "ready" 29 and all(chunk[field] for field in required_metadata) 30] 31print(f"indexable={[chunk['id'] for chunk in indexable]}") 32print(f"blocked={len(chunks) - len(indexable)}") 33assert [chunk["id"] for chunk in indexable] == ["rollback"]
Output
1indexable=['rollback'] 2blocked=1

At this point, a policy page can be split into answerable chunks, retain ingestion locators, and block fragments at the release gate. The remaining question isn't another splitter. Does a model use that retrieved evidence, and can a public benchmark stand in for that proof?

Mastery check

A clean evidence record is only the starting point. The checks above turn it into a retrieval unit that can be evaluated.

Key concepts

  • A chunk is a searchable evidence unit, not an arbitrary slice of text.
  • Fixed windows expose boundary and overlap costs directly.
  • Structure-aware splitting is a strong baseline when headings or tables carry meaning.
  • Parent-child expansion separates precise search from sufficient answer context.
  • Semantic or late chunking should be escalations justified by measured failures.
  • A labeled retrieval set must check both correct selection and answerable evidence.

Evaluation rubric

  • Foundational: Identifies why a broken chunk can't answer the rollback-failure question
  • Foundational: Implements fixed windows and explains what overlap repeats
  • Intermediate: Preserves headings, tables, source IDs, and locators in chunk records
  • Intermediate: Compares candidate boundaries on labeled retrieval cases
  • Intermediate: Uses parent-child expansion when narrow retrieval lacks sufficient context
  • Advanced: Chooses semantic or late chunking only after measuring a baseline failure

Follow-up questions

Why is a chunk containing 15 minutes with deploy ID insufficient by itself?

Answer

It omits the condition that the deadline applies to rollback failures. Without that condition, the model can't cite the paging window as the answer to a failed-rollback question.

When should overlap be set to zero?

Answer

Start with zero when a chunk already follows a complete boundary, such as a policy section or intact table row group. Add overlap only when a measured boundary failure shows that nearby text must be repeated.

Why retain the locator from the ingestion record in every chunk?

Answer

The locator lets a retrieved answer cite original evidence, compare re-ingestion changes, and debug which boundary rule created a failed result.

What problem does parent-child retrieval solve?

Answer

It lets the search index use a small, specific match while the generator receives the larger section needed for a complete, cited answer.

What evidence justifies switching from structure-aware splitting to late chunking?

Answer

A labeled evaluation set must show failures caused by missing broader document context, and a late-chunking implementation must improve retrieval or answer quality enough to justify its latency and infrastructure cost.

Common pitfalls

  • A deadline loses its condition: A window retrieves 15 minutes without Rollback failure. Fix: split at section boundaries and assert answerability.
  • A pronoun loses its antecedent: A chunk says This process reduces wear without naming the process. Fix: keep the referred entity in the same unit, prepend section context, or expand to the parent when coreference would leave the span ambiguous.
  • A fallback emits an orphan tail: A tiny final window retrieves 14 days. without Routine deploy notes. Fix: merge undersized tails into the preceding child or block them for review.
  • A table row loses its header: A paging or archive window becomes ambiguous. Fix: keep column names with rows and test exact-value questions.
  • Overlap creates duplicates without fixing answers: Repeated chunks dominate top results. Fix: measure overlap against both complete evidence and index cost.
  • Word windows overflow the embedder: A chunk fits a word budget but exceeds max_seq_length, so the model silently drops the end of the rule. Fix: size and gate with the embedding tokenizer.
  • Chunks can't be cited: Search looks plausible but support can't defend the answer. Fix: retain source, locator, heading, and checksum metadata.
  • An advanced splitter is adopted by reputation: Complexity rises without better results. Fix: preserve a simple baseline and evaluate every escalation on the same labeled set.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A retriever returns only 15 minutes with deploy ID. for the question A rollback failed after deploy pay-742. When do I page, and what ID do I include? Which invariant fails?

Correct answer: Answerability fails because the span preserves deadline and identifier but loses the rollback-failure condition governing them.

The selected text can't prove when the 15-minute rule applies. A locator helps audit the source, but it doesn't restore a condition that the generator never receives.

2.A fixed splitter uses size 6 and overlap 2. It emits a final window notes archive within 14 days. after starting windows at offsets 0, 4, 8, and 12. What should preprocessing do?

Correct answer: Advance by 4 words and merge or quarantine that short tail, because it no longer says which notes have a 14-day window.

Step equals size minus overlap: 6 - 2 = 4. The orphan tail retains a value but drops its governing condition, so indexing it alone creates ambiguous evidence.

3.With window size 7, overlap 0, 2, and 4 indexes 17, 23, and 35 words respectively, yet every setting produces zero complete rollback rules. Which decision follows from those measurements?

Correct answer: Reject overlap as this boundary's repair and test structure-aware cuts that preserve the complete rule at lower duplication cost.

Overlap increased index volume from 17 to 35 words without producing one answerable rule. The experiment isolates a boundary failure, so a meaningful section cut is the next candidate.

4.A Markdown runbook has separate Rollback failure and Routine deploy notes sections, and each complete section fits the embedding budget. What is the strongest first-pass representation?

Correct answer: Create one chunk for each section, prepend its heading to indexed text, and retain source and locator fields for citation.

The author-supplied headings define which condition owns each deadline. Putting the heading in searchable text preserves meaning; source and locator fields preserve the route back to evidence.

5.A table row contains Rollback failure | 15 minutes | Deploy ID, but its Condition | Window | Required evidence header is omitted from the indexed text. Which correction restores its semantics?

Correct answer: Keep the header with relevant rows and carry the table heading and locator separately for retrieval context and citation.

Without column labels, 15 minutes and Deploy ID have no defensible roles. The header restores those roles, while heading and locator metadata support discovery and audit.

6.A chunk has 90 whitespace-separated words, passes a character-based splitter, and targets an embedder with max_seq_length=64. Which release check prevents silent loss of the rule's tail?

Correct answer: Count with the embedder's tokenizer and split or reject the chunk when its tokenized length exceeds the model limit.

Words, characters, and model tokens are different units. Only the embedding tokenizer can establish whether the encoded span fits; overlap applied after truncation can't recover omitted meaning.

7.A labeled case expects chunk rollback and evidence containing 15 minutes. Retrieval returns chunk rollback, but its text ends at page on-call within. How should the regression score it?

Correct answer: Fail it: source selection succeeded, but evidence sufficiency failed because the retrieved span can't establish the deadline.

A useful retrieval case checks both where the hit came from and what it can prove. Correct topic or ID isn't enough when the answer-bearing value is outside returned context.

8.A narrow child span matches 15 minutes with deploy ID, while a grounded answer also needs the parent section's rollback condition and service-name instruction. Which context path fits?

Correct answer: Search compact children, resolve the matched parent pointer, and provide the permitted parent section to generation.

Parent-child retrieval separates a precise search unit from a sufficient answer unit. A pointer or nearby fragment isn't itself evidence; generation receives the resolved parent text.

9.A structure-aware baseline still splits one paragraph after an abrupt topic change, and another local span remains ambiguous without distant document context. Which two experiments target those distinct failures?

Correct answer: Test semantic boundary detection for the topic shift and late chunking for context-dependent embeddings on labeled cases.

Semantic splitting proposes a cut where neighboring sentences diverge. Late chunking keeps the span but pools its vector after longer-context encoding. Each earns release only through the same labeled evaluation.

9 questions remaining.

Next Step
Continue to LLM Benchmarks & Limitations

You can now split a policy page into chunks that stay answerable and test those boundaries with labeled queries. Next you'll score whether a model actually uses that retrieved evidence, and when a public benchmark is the wrong proof for a release.

PreviousFile Ingestion for AI
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks.

Lewis, P., et al. · 2020 · NeurIPS 2020

https://arxiv.org/abs/2005.11401

A Systematic Analysis of Chunking Strategies for Reliable Question Answering.

Bennani, S., & Moslonka, C. · 2026 · arXiv preprint

https://arxiv.org/abs/2601.14123

RecursiveCharacterTextSplitter

LangChain · 2023

https://docs.langchain.com/oss/python/integrations/splitters/recursive_text_splitter

ParentDocumentRetriever

LangChain · 2024

https://reference.langchain.com/python/langchain-classic/retrievers/parent_document_retriever/ParentDocumentRetriever

Late Chunking: Contextual Chunk Embeddings Using Long-Context Embedding Models.

Günther, M., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2409.04701

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.