Repository navigation
Conversation
`combine_text_under_n_chars` is documented as capped at `new_after_n_chars`: a combine-threshold above the soft-max recombines pre-chunks the soft-max has already declared full, so the caller gets chunks well past the size they asked for. The cap was implemented until the `title.py` refactor in 9ac4445 (Unstructured-IO#2657), which replaced return soft_max if combine_text_under_n_chars > soft_max else combine_text_under_n_chars with a plain read of the argument, while leaving both the property's own docstring ("Is reduced to `new_after_n_chars` when it exceeds that value") and the public option documentation in `base.py` ("Note this value is 'capped' at the `new_after_n_chars` value") describing the old behavior. chunk_by_title( elements, max_characters=500, new_after_n_chars=100, combine_text_under_n_chars=400, ) returned one 367-character chunk where three 121-character chunks were asked for. Restore the cap. Validation now reads the argument as the caller gave it rather than the capped property, so a negative value and a value greater than `max_characters` are both still rejected with the same messages; the cap would otherwise have hidden the second case.
An options-level test over the conflicting, equal, lower, and unspecified cases, plus an end-to-end test asserting three already-full sections are not recombined into one oversized chunk.
0.27.8 was released while this was open, so the entry opens a new 0.27.9-dev0 section instead of the closed one.
abo3losh1
force-pushed
the
fix/combine-text-under-n-chars-cap
branch
from
September 22, 2026 15:33
8b79038 to
d79456b
Compare
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Resolve the token/character unit mismatch, reconcile the version target, and add the requested validation regression case.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Restores the documented cap on combine_text_under_n_chars and adds regression coverage for title chunking.
Changes:
- Caps the combine threshold at the soft maximum.
- Preserves raw-argument validation.
- Adds tests and updates changelog/version metadata.
| File | Summary |
|---|---|
unstructured/chunking/title.py |
Implements capping and validation. Critical concern (2 votes): token-mode unit mismatch. Moderate concern (1 vote): add coverage for capped values exceeding max_characters. |
unstructured/__version__.py |
Version is 0.27.9-dev0, conflicting with the described 0.27.8-dev1 target (nit, 3 votes). |
test_unstructured/chunking/test_title.py |
Adds cap and integration regression tests. |
CHANGELOG.md |
Documents the fix and uses the updated development version. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| # -- NOTE(abo3losh1): `new_after_n_chars` takes precedence on conflict. A threshold above | ||
| # -- the soft-max would combine pre-chunks the soft-max has already declared full. | ||
| return min(combine_text_under_n_chars, self.soft_max) |
| @@ -1 +1 @@ | |||
| __version__ = "0.27.8" # pragma: no cover | |||
| __version__ = "0.27.9-dev0" # pragma: no cover | |||
`combine_text_under_n_chars` is a character count, measured against
`len(text)` in `PreChunk.can_combine()`, but `soft_max` is a token count
when `max_tokens` is configured. Capping one at the other mixed units:
_ByTitleChunkingOptions.new(
max_tokens=100, new_after_n_tokens=10,
tokenizer="cl100k_base", combine_text_under_n_chars=50,
).combine_text_under_n_chars
returned 10, turning a 50-character threshold into a 10-character one and
changing token-mode behavior for no reason. The documented cap is against
`new_after_n_chars`, which has no counterpart in token mode, so the cap now
applies only when counting characters.
Fails before the fix: a 50-character combine-threshold came back as 10 under `new_after_n_tokens=10`.
…r-n-chars-cap # Conflicts: # CHANGELOG.md # unstructured/__version__.py
Author
|
Closing this since it hasn't been reviewed. Happy to reopen if it's still useful. |
cragwolfe
pushed a commit
that referenced
this pull request
Oct 9, 2026
…4501) No open issue covers this. None. Found while reading the three sibling validators in `_validate()` against the parameters they guard. `ChunkingOptions._validate()` at `unstructured/chunking/base.py` rejects a negative `new_after_n_chars` and a negative `new_after_n_tokens`. It never checks `overlap`. A negative overlap then corrupts output two ways, with no error and no warning. `base.py:2731` computes the remainder as `s[maxlen - self._opts.overlap :]`. With `overlap = -5` that remainder starts five characters past the end of the fragment, so five characters of the document disappear at every split boundary. `base.py:755` computes `self._text[-overlap:]`. With `overlap = -5` that is a forward slice, so the tail carried into the next chunk is most of the current chunk. Before, on a 60-character input of unique characters at `max_characters=20`: ``` $ python repro.py overlap= 0: ['ABCDEFGHIJKLMNOPQRST', 'UVWXYZabcdefghijklmn', 'opqrstuvwxyz01234567'] dropped from output entirely: '' overlap= -5: ['ABCDEFGHIJKLMNOPQRST', 'Zabcdefghijklmnopqrs', 'yz01234567'] dropped from output entirely: 'UVWXYtuvwx' ``` The `overlap=0` line is the control: same input, same commit, nothing lost. With `overlap_all=True`, three 10-character elements at `max_characters=12`: ``` overlap= 0 -> ['AAAAAAAAAA', 'BBBBBBBBBB', 'CCCCCCCCCC'] overlap= -4 -> ['AAAAAAAAAA', 'AAAAAA\n\nBBBB', 'BB', 'AA\n\nBBBBBBBB', 'CCCCCCCCCC'] ``` Text duplicated and out of order. Reachable from all three public entry points: `chunk_elements(...)`, `chunk_by_title(...)`, and `partition(filename=..., chunking_strategy="basic", overlap=-5)`. All three drop the same characters. After, the four added lines mirror the existing `new_after_n_chars` check and raise `ValueError`. ``` $ python -m pytest test_unstructured/chunking -q 484 passed, 24 skipped in 15.04s ``` Baseline on `1bedf7b` is `482 passed, 24 skipped`, so the delta is the two new parametrised cases. Reverting `unstructured/chunking/base.py` and keeping the test gives `2 failed, 482 passed, 24 skipped`, both of them `Failed: DID NOT RAISE ValueError`. `ruff check` is clean on both touched files. I did not run `make test` or `make check` in full, and no PDF, image or OCR test ran here: the install was `-e ".[csv,docx,md,pptx,xlsx]"` without the inference extras. `make check-version` passes, with `CHANGELOG.md` and `__version__.py` both at `0.27.10`. Duplicate check: all 130 open pull requests enumerated, 10 touching `unstructured/chunking/`. I grepped each diff for added lines matching `_validate|overlap.*must be|overlap_arg`. The only hit is #4487, which caps `combine_text_under_n_chars` in `title.py::_validate`: different file, different function, different parameter. #4392 is the nearest neighbour and changes `_split_by_tokens` rather than validation, so the two do not collide. @cragwolfe you merged the last chunking change here. Would you rather this raised, or clamped a negative overlap to zero with a warning? <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/Unstructured-IO/unstructured/pull/4501?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
cragwolfe
pushed a commit
that referenced
this pull request
Oct 22, 2026
…s set (#4528) `PreChunk.can_combine()` sizes its text with `len()`, so the `combine_text_under_n_chars` threshold and the hard-max check count characters even when `max_tokens` has selected token counting. Every sibling computation in the class — `will_fit`, `_text_length` — goes through `ChunkingOptions.measure()`, which branches on the counting mode; `can_combine` is the one that doesn't. In token mode a section of a few tokens is many more characters than the token budget, so every section looks too large to combine and becomes its own chunk. With `max_tokens=60`, `chunk_elements` fills the window while `chunk_by_title` produces twelve ten-token chunks — about 17% utilization — and the obvious workaround, `combine_text_under_n_chars=240`, is rejected because it exceeds the hard max. The fix is the two `measure()` substitutions. Character-mode behavior is unchanged (`measure()` is `len()` there): a 3,000-chunk-list corpus across both chunkers and five option sets hashes identically before and after, and the existing character-mode `can_combine` cases pass untouched. Tests: a deterministic offline token counter fixture (no tiktoken download), a token-mode `can_combine` parametrization mirroring the character-mode sibling, a `chunk_by_title` case asserting the token budget is filled and the chunk texts equal `chunk_elements()` on the same input, and a discriminating character-mode case that would flip if the measure were swapped. The first two fail on `main`. `test_unstructured/chunking/test_base.py` + `test_title.py`: 391 passed, 16 skipped; ruff clean. CHANGELOG entry under a new `0.27.18` section with the matching `__version__` bump, following the one-patch-per-PR pattern of the recent entries. Related but different: #4487 changes what the `combine_text_under_n_chars` threshold *is* (caps it at `new_after_n_chars`); this changes which *unit* it's compared in. No textual overlap. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/Unstructured-IO/unstructured/pull/4528?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
combine_text_under_n_charsis documented in two places as being capped atnew_after_n_chars:ChunkingOptionsinbase.py: "Note this value is 'capped' at thenew_after_n_charsvalue since a value higher than that would not change this parameter's effect."_ByTitleChunkingOptions.combine_text_under_n_charsitself: "Is reduced tonew_after_n_charswhen it exceeds that value."It isn't. The cap was implemented until the
title.pyrefactor in 9ac4445 (#2657), which replacedwith a plain read of the argument, leaving both docstrings describing behaviour that had gone.
The effect is that a combine-threshold above the soft-max recombines pre-chunks the soft-max has already declared full, and the caller gets chunks several times the size they asked for:
Each section is already over
new_after_n_chars=100, so none of them should have been eligible for combining.Changes
_ByTitleChunkingOptions.combine_text_under_n_charscaps the value atsoft_max, restoring what the docstrings describe. No documentation changes were needed; the code now matches what is already written._validate()reads the raw argument instead of the (now capped) property. Without this the cap would swallow thecombine_text_under_n_chars > max_characterscase and thatValueErrorwould stop being raised. Both existing validation errors keep their exact messages, andDescribe_ByTitleChunkingOptionsstill passes unchanged.The default path is unaffected: with
new_after_n_charsunspecified,soft_max == hard_max == max_characters, so the cap is a no-op.combine_text_under_n_chars=0and values below the soft-max are likewise unchanged.Testing
495 passed, 18 skipped. Three of those fail onmain:The new tests are:
it_caps_combine_text_under_n_chars_at_new_after_n_chars: parametrized over the conflicting, equal, lower, unspecified-combine and unspecified-both cases.test_it_does_not_combine_pre_chunks_past_new_after_n_chars: the snippet above, end to end.ruff checkandruff format --checkpass on the touched files.CHANGELOG.mdand__version__.pybumped to0.27.10-dev0. (0.27.9 was released while this was open, so the entry opens a new dev section rather than the closed one.)To see it by hand, run the snippet above on
mainand on this branch.Update, token-counting mode
The cap is a character-mode rule.
combine_text_under_n_charsis a character count, measured againstlen(text)inPreChunk.can_combine(), whilesoft_maxis a token count whenmax_tokensis configured, so capping one at the other mixed units:new_after_n_charshas no counterpart in token mode, so the cap now applies only when counting characters, andit_does_not_cap_combine_text_under_n_chars_when_counting_tokenspins that. 495 passed, 18 skipped on current main.