Chunking
DocSlicer groups document blocks into chunks — text segments sized for an embedding model's context window. This page covers how chunking works: the structure it follows, how it decides where to split, and the parameters that control sizing. For the exact parameter signature and defaults, see parse_document.
result = docslicer.parse_document(
"report.pdf",
max_chunk_size=3200, # hard ceiling
optimal_chunk_size=1500, # target
min_chunk_size=700, # soft floor
merge_small_chunks=True,
)A structure-aware chunker
DocSlicer is a structure-aware chunker, not a fixed-size sliding window. It groups whole blocks — headings, paragraphs, tables — following the document's heading hierarchy and section boundaries. A chunk never begins mid-paragraph or splits a table away from its heading.
Two consequences follow:
- Chunks vary in size. A chunk's length is dictated by where the content's natural boundaries fall, not by a fixed character count.
optimal_chunk_sizeis a target the chunker aims for, not a width it cuts to — so a document of identical settings still produces chunks of many different sizes. - There is no
overlapparameter. Fixed-size chunkers cut at arbitrary offsets, routinely splitting a sentence or idea across two chunks. Overlap — repeating the last bit of one chunk at the start of the next — is their damage control, so the straddling text still survives intact in at least one chunk. DocSlicer splits at structural boundaries instead, so ideas aren't cut mid-thought and there's nothing for overlap to patch over.
The size parameters below shape this process; they don't override the structure.
Size parameters
All three are measured in characters, not tokens.
| Parameter | Default | Role |
|---|---|---|
max_chunk_size | 3200 | Hard ceiling. No chunk ever exceeds this. The chunker splits before crossing it, even mid-section. |
optimal_chunk_size | 1500 | Target. When choosing where to split, the chunker aims to land near this size at a clean semantic boundary (paragraph or heading). |
min_chunk_size | 700 | Soft floor. Chunks below this are candidates for merging into a neighbour. "Soft" because a short section at a boundary may stay small. |
The chunker walks the document in order, accumulating blocks toward optimal_chunk_size, preferring to break at a heading or paragraph edge, and never letting a chunk pass max_chunk_size. Once chunks exist, small ones are reconciled — see merging below.
Why these defaults
The defaults target general-purpose RAG over dense business, legal, and financial documents — where the unit of meaning is a clause or a footnote, not a sentence. The three sizes balance two competing goals: semantic completeness (a chunk should hold a whole idea) and retrieval precision (a chunk shouldn't be so large that the relevant part is buried).
optimal 1500 chars (~375 tokens) — the semantic unit
A self-contained business thought — a contract clause, a revenue footnote, a breakdown of an asset class — typically runs one to three paragraphs, roughly 250–350 words. At ~1,500 characters a chunk captures that core concept and its immediate qualifiers (the "under what conditions"), which is what makes the embedding specific enough to retrieve accurately. Go much smaller and the vector only sees a fragment of the clause, losing the context that made it answerable.
This Apple 10-Q revenue note is ~1,500 characters — one complete idea, table included:
## Note 2 – Revenue
The following table shows disaggregated net sales, as well as the portion of total
net sales that was previously deferred, for the three months ended December 27, 2025
and December 28, 2024 (in millions):
| | Three Months Ended | Three Months Ended |
| | December 27, 2025 | December 28, 2024 |
|----------------------------------|--------------------|--------------------|
| iPhone® | $85,269 | $69,138 |
| Mac® | 8,386 | 8,987 |
| iPad® | 8,595 | 8,088 |
| Wearables, Home and Accessories | 11,493 | 11,747 |
| Services | 30,013 | 26,340 |
| Total net sales | $143,756 | $124,300 |
| Portion previously deferred | $4,050 | $3,690 |
The Company's proportion of net sales by disaggregated revenue source was generally
consistent for each reportable segment in Note 10, "Segment Information," except in
Greater China, where iPhone revenue represented a moderately higher proportion.
As of December 27, 2025, the Company had total deferred revenue of $14.3 billion. It
expects 66% to be realized in less than a year, 23% within one-to-two years, 9% within
two-to-three years and 2% in greater than three years.max 3200 chars (~800 tokens) — the safety valve
Dense documents contain indivisible blocks: a sprawling credit-agreement definition, a single-sentence indemnification clause, a wide financial table. Forcing a hard cut at optimal would slice these mid-thought and destroy their meaning. The ceiling gives the splitter permission to overshoot the target — but only when a block genuinely can't be broken without losing its integrity.
This single legal subsection is ~3,100 characters. It's one clause and cannot be split without breaking it, so it stays whole right up against the ceiling:
## 6.9 Employee Matters
(c) New Plans. To the extent that a Company Plan or Comparable Plan is made available to
any Continuing Employee at or after the Effective Time, the Surviving Corporation and its
Subsidiaries will (and Parent will cause the Surviving Corporation and its Subsidiaries
to) cause to be granted to such Continuing Employee credit for all service with the
Company and its Subsidiaries and their respective predecessors prior to the Effective Time
for purposes of eligibility to participate, vesting and entitlement to benefits where
length of service is relevant (including for purposes of vacation accrual and severance
pay entitlement, but excluding for purposes of benefit accruals under any defined benefit
pension plan or post-employment welfare plan), except that such service need not be
credited to the extent that it would result in duplication of benefits. In addition, and
without limiting the generality of the foregoing, (i) each Continuing Employee will be
immediately eligible to participate, without any waiting period, in any and all employee
benefit plans sponsored by the Surviving Corporation and its Subsidiaries (other than the
Company Plans) (such plans, the "New Plans") to the extent that coverage pursuant to any
such New Plan replaces coverage pursuant to a comparable Company Plan in which such
Continuing Employee participates immediately before the Effective Time (such plans, the
"Old Plans"); (ii) for purposes of each New Plan providing medical, dental, pharmaceutical,
vision or disability benefits to any Continuing Employee, the Surviving Corporation will
cause all waiting periods, pre-existing condition exclusions, evidence of insurability
requirements and actively-at-work or similar requirements of such New Plan to be waived
for such Continuing Employee and his or her covered dependents, to the extent waived under
the corresponding Old Plan, and the Surviving Corporation will cause any eligible expenses
incurred by such Continuing Employee and his or her covered dependents during the portion
of the plan year of the Old Plan ending on the date that such Continuing Employee's
participation in the corresponding New Plan begins to be given full credit pursuant to
such New Plan for purposes of satisfying all deductible, coinsurance and maximum
out-of-pocket requirements applicable to such Continuing Employee and his or her covered
dependents for the applicable plan year as if such amounts had been paid in accordance
with such New Plan, to the extent credited under the corresponding Old Plan; and (iii)
credit the accounts of such Continuing Employees pursuant to any New Plan that is a
flexible spending plan with any unused balance in the account of such Continuing Employee.
Any vacation or paid time off accrued but unused by a Continuing Employee as of immediately
prior to the Effective Time will be credited to such Continuing Employee following the
Effective Time in accordance with the Company's vacation or paid time off policies in
effect immediately prior to the Effective Time.min 700 chars — the noise filter
Documents are full of short fragments: boilerplate, headers, page labels, one-line transitions like "Subject to the terms below:". These carry little unique meaning, yet because they're brief their embedding vectors can spuriously align with a query and pull junk into the top results. The floor merges sub-700-character fragments into a neighbour, so every chunk handed to the vector store has enough substance to be worth retrieving.
How splits are chosen
Within each heading section, DocSlicer decides how to group that section's blocks by minimising how far each chunk strays from optimal_chunk_size. It looks at the whole section at once and picks the partition that keeps every chunk closest to the target — a dynamic-programming search for the globally best split, not a left-to-right "fill until full" pass. This is why chunk sizes cluster around optimal_chunk_size rather than hugging the max ceiling.
That optimal search is expensive: its cost grows roughly cubically O(n³) with the number of blocks under a heading, so a heading with a few hundred blocks beneath it could take tens of seconds. Above about a hundred blocks under a single heading, DocSlicer falls back to a greedy pass — accumulate blocks until reaching optimal_chunk_size, then start a new chunk. At that scale the optimal split converges to the same "pack to target" behaviour anyway, so retrieval quality is unaffected.
In practice: a densely-headed document (many short sections) gets the optimal treatment throughout, while a document with sparse headings and long heading-less runs is chunked greedily within those large runs. Both aim at the same sizes — only the search strategy differs.
How merging works
Because DocSlicer is structure-aware, it initially produces one chunk per heading or paragraph boundary. For documents with many short sections this can yield a lot of small chunks. When merge_small_chunks=True (the default), sibling sections under the same parent heading are merged together until they reach min_chunk_size — folded into an adjacent chunk only where doing so won't exceed max_chunk_size and won't cross into a different parent section.
Take this revenue breakdown from an Apple 10-K. Five short subsections all sit under ## Products and Services Performance, each just a sentence or two:
## Products and Services Performance
The following table shows net sales by category for 2025, 2024 and 2023
(dollars in millions):
| | 2025 | Change | 2024 | Change | 2023 |
|----------------------------------|-----------|--------|-----------|--------|-----------|
| iPhone | $209,586 | 4% | $201,183 | —% | $200,583 |
| Mac | 33,708 | 12% | 29,984 | 2% | 29,357 |
| iPad | 28,023 | 5% | 26,694 | (6)% | 28,300 |
| Wearables, Home and Accessories | 35,686 | (4)% | 37,005 | (7)% | 39,845 |
| Services | 109,158 | 14% | 96,169 | 13% | 85,200 |
| Total net sales | $416,161 | 6% | $391,035 | 2% | $383,285 |
Services net sales include amortization of the deferred value of services bundled
in the sales price of certain products.
### iPhone
iPhone net sales increased during 2025 compared to 2024 due to higher net sales of
Pro models.
### Mac
Mac net sales increased during 2025 compared to 2024 primarily due to higher net
sales of laptops and desktops.
### iPad
iPad net sales increased during 2025 compared to 2024 primarily due to higher net
sales of iPad Air, iPad mini and iPad, partially offset by lower net sales of iPad Pro.
### Wearables, Home and Accessories
Wearables, Home and Accessories net sales decreased during 2025 compared to 2024
primarily due to lower net sales of Accessories and Wearables.
### Services
Services net sales increased during 2025 compared to 2024 primarily due to higher
net sales from advertising, the App Store and cloud services.On their own, the five ### subsections would each become a tiny chunk of ~120–160 characters — well below min_chunk_size. Because they're siblings under the same parent, merging folds them into one coherent chunk of about 1,700 characters that still carries the correct path for each paragraph, instead of five fragments that each retrieve poorly on their own.
This is also why you occasionally see chunks smaller than min_chunk_size: a short chunk sitting alone at the end of a section has no eligible sibling to merge with.
Set merge_small_chunks=False to keep every chunk exactly as the splitter produced it — one chunk per section, regardless of size.
When to tune
| Goal | Adjustment |
|---|---|
| More precise retrieval / short Q&A | Lower optimal (e.g. 800) and max (e.g. 2000). Smaller chunks, tighter matches. |
| More context per chunk / summarisation | Raise optimal and max. Fewer, broader chunks. |
| Match a specific embedding model's window | Set max_chunk_size to the model's token limit × ~4 chars/token, with margin. |
| Keep tiny fragments separate | merge_small_chunks=False. |
| Skip chunking entirely | chunking=False — returns only result.blocks, and is faster. |
# Precise retrieval preset
result = docslicer.parse_document(
"contract.pdf",
max_chunk_size=2000,
optimal_chunk_size=800,
min_chunk_size=400,
)The resulting sizes land on each Chunk as char_count and token_count. For the difference between chunks and the raw blocks they're built from, see Blocks.