fix(chunking): measure inter-chunk overlap in tokens under token chunking - #4422
Open
eeshsaxena wants to merge 1 commit into
Open
fix(chunking): measure inter-chunk overlap in tokens under token chunking#4422eeshsaxena wants to merge 1 commit into
eeshsaxena wants to merge 1 commit into
Conversation
…king `overlap` is documented as a size in the chunking window's units, and under max_tokens chunking every other size is measured in tokens via ChunkingOptions.measure. The text-splitter follows that at split boundaries, but PreChunk.overlap_tail took a raw character slice (self._text[-overlap:]) regardless of mode, so the same `overlap` value meant tokens at a split boundary and characters at a chunk boundary. With max_tokens=100, overlap=50 the chunk boundary carried ~13 tokens instead of the requested ~50, silently degrading the cross-boundary context that overlap exists to preserve, and varying with the text. Pull the existing binary-search tail lookup out of _TextSplitter._get_token_overlap_tail into a module-level _token_overlap_tail(text, target_tokens, measure) and call it from both places. _get_token_overlap_tail now delegates to it, so the splitter's behaviour is byte-for-byte unchanged. Character-mode chunking still takes the character slice. Adds two cases to DescribePreChunk in test_base.py: one asserting the tail measures the requested token count (and is a real suffix), one for text shorter than the overlap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="unstructured/chunking/base.py">
<violation number="1" location="unstructured/chunking/base.py:740">
P3: With this change, `overlap`/`inter_chunk_overlap` are token counts when `max_tokens` is configured but character counts otherwise. The `overlap`, `inter_chunk_overlap`, and the `ChunkingOptions` kwarg docstrings still describe them as character counts only, so the public API docs are now inconsistent with the token-mode behavior this PR establishes. Consider updating those docstrings to say the value is characters in character-based mode and tokens in token-based mode, so the same option isn't documented two conflicting ways.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
| # -- tokens at a split boundary but characters at a chunk boundary, silently | ||
| # -- shrinking inter-chunk overlap to a fraction of what was requested. | ||
| if self._opts.use_token_counting: | ||
| return _token_overlap_tail(self._text, overlap, self._opts.measure).strip() |
Contributor
There was a problem hiding this comment.
P3: With this change, overlap/inter_chunk_overlap are token counts when max_tokens is configured but character counts otherwise. The overlap, inter_chunk_overlap, and the ChunkingOptions kwarg docstrings still describe them as character counts only, so the public API docs are now inconsistent with the token-mode behavior this PR establishes. Consider updating those docstrings to say the value is characters in character-based mode and tokens in token-based mode, so the same option isn't documented two conflicting ways.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured/chunking/base.py, line 740:
<comment>With this change, `overlap`/`inter_chunk_overlap` are token counts when `max_tokens` is configured but character counts otherwise. The `overlap`, `inter_chunk_overlap`, and the `ChunkingOptions` kwarg docstrings still describe them as character counts only, so the public API docs are now inconsistent with the token-mode behavior this PR establishes. Consider updating those docstrings to say the value is characters in character-based mode and tokens in token-based mode, so the same option isn't documented two conflicting ways.</comment>
<file context>
@@ -729,7 +729,16 @@ def overlap_tail(self) -> str:
+ # -- tokens at a split boundary but characters at a chunk boundary, silently
+ # -- shrinking inter-chunk overlap to a fraction of what was requested.
+ if self._opts.use_token_counting:
+ return _token_overlap_tail(self._text, overlap, self._opts.measure).strip()
+ return self._text[-overlap:].strip()
</file context>
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.
overlapis documented as a size in the chunking window's units, and undermax_tokenschunking every other size (max_tokens,new_after_n_tokens,hard_max,soft_max) is measured in tokens viaChunkingOptions.measure. The text splitter follows that: when it splits an oversized element it calls_get_token_overlap_tail(fragment, overlap)and hands the next fragment approximatelyoverlaptokens.PreChunk.overlap_tail, which produces the overlap between two whole chunks, did not:That is a character slice regardless of mode, so the same
overlapvalue means two different things depending on which boundary it lands on.With
max_tokens=100, tokenizer="cl100k_base", overlap=50, overlap_all=Trueover a long paragraph:So inter-chunk overlap is silently about a quarter of what was asked for, and it varies with the text, since characters-per-token is not constant. Overlap exists to keep context across a boundary for retrieval, so quietly shrinking it degrades results without any error.
The fix pulls the existing binary-search tail lookup out of
_TextSplitterinto a module-level_token_overlap_tail(text, target_tokens, measure)and calls it from both places._TextSplitter._get_token_overlap_tailnow delegates to it, so the splitter's behaviour is byte-for-byte the same. Character-based chunking still takes the character slice and is untouched.After the change the chunk boundary carries approximately 50 tokens, matching the split boundary.
Added two cases to
DescribePreChunkintest_unstructured/chunking/test_base.py: one asserting the tail measures the requested token count (and is a real suffix of the text), one covering text shorter than the overlap. Verified withpytest test_unstructured/chunking/test_base.py: the token case fails against the current code, and all 226 pass with the fix. The existing character-modeoverlap_tailcases pass unchanged either way.I kept the approximate semantics rather than making the tail exact, since
_get_token_overlap_tailalready rounds to a word boundary and the docstring says the overlap "will not exceed this number ... but may be less as required to respect splitting-character boundaries".