Boundary rules are where quality is won or lost, and they differ by document type. The table below is a checklist you can apply during ingestion.
| Content type | Preferred boundary | Typical size target | Overlap | What breaks without it |
|---|
| Prose, articles, policies | Heading, then sentence end | A few hundred tokens | 10 to 20 percent at paragraph seams | Sentences cut mid-clause; clauses separated from their conditions |
| Technical docs, runbooks | Heading plus numbered step block | One procedure or section | Repeat the heading line | Step 3 separated from the prerequisite in step 2 |
| Source code | One function or class | Function or class, split only if huge | None inside a function | Half a function with no signature or return type |
| Tables and spreadsheets | One table with its header row | Whole table, or row groups that repeat headers | Repeat header on every part | Numbers without column labels |
| Slides and decks | One slide, with its title | Slide text plus notes | Include the deck title | Bullet fragments with no subject |
| Chat and support threads | One question plus its accepted answer | Thread turn pair | Include the product name | A reply with no question attached |
Fixed-length splitting is the simplest method and produces uniform chunks, but it can split sentences and ideas, and critical information can span chunks (Mastering LLM). Recursive splitting, which tries larger delimiters first (paragraphs, then sentences, then words) and only falls back when a segment is still too big, keeps the structure while staying inside a size budget. Chroma's evaluation found heuristic strategies such as the recursive character splitter often perform well in practice when parameterized appropriately, and that default settings of some popular splitters can perform poorly (Chroma). Parameterizing it is the whole job.
A small parsing step, respecting a size ceiling, looks like this:
def chunk_by_boundaries(text, max_tokens, overlap_tokens):
segments = split_on_headings(text) # boundaries first
chunks = []
for seg in segments:
pieces = recursive_split(seg, max_tokens) # sentences, then words
for i, piece in enumerate(pieces):
tail = pieces[i - 1][-overlap_tokens:] if i > 0 else ""
chunks.append(tail + piece)
return chunks
The important line is the first: structure decides the candidate cuts, and length only decides when a cut is forced.