Skip to content

fix(semantic): split sentences that exceed chunk_size#627

Open
chakshu-dhannawat wants to merge 2 commits into
feyninc:mainfrom
chakshu-dhannawat:fix/semantic-chunker-oversized-sentence
Open

fix(semantic): split sentences that exceed chunk_size#627
chakshu-dhannawat wants to merge 2 commits into
feyninc:mainfrom
chakshu-dhannawat:fix/semantic-chunker-oversized-sentence

Conversation

@chakshu-dhannawat

@chakshu-dhannawat chakshu-dhannawat commented Jul 7, 2026

Copy link
Copy Markdown

Fixes #222.

Problem

SemanticChunker only splits between sentences, never within one. When a single sentence tokenizes to more than chunk_size (common with delimiter-free content like OpenAPI/JSON specs or very long lines), it was emitted as a single oversized chunk. The issue reporter saw a ~48,811 token chunk with chunk_size=2048 on the OpenAI OpenAPI YAML.

There were actually two code paths that skipped the size limit:

  1. _split_groups packs sentences into groups but assumed every individual sentence already fit within chunk_size. A sentence larger than chunk_size just became its own group unchanged.
  2. The few-sentences early-return in chunk() (when there are <= similarity_window sentences) built a single chunk with no size check at all.

Fix

  • Added _split_oversized_sentence(), which splits a too-large sentence into token-sized pieces using tokenizer.encode / decode_batch, the same way TokenChunker already does. _split_groups calls it before packing, so no piece ever exceeds chunk_size.
  • Routed the few-sentences path through _split_groups + _create_chunks so it enforces the limit too. Output is unchanged when the content already fits.

Reproduce (before the fix)

from chonkie import SemanticChunker

chunker = SemanticChunker(embedding_model="minishlab/potion-base-8M", chunk_size=200)
text = "A normal sentence here. " + ("word " * 2000) + ". Another normal one."
chunks = chunker.chunk(text)
print(max(c.token_count for c in chunks))   # 2001 before, <= 200 after

Tests

Added two regression tests (oversized sentence in the normal path, and in the few-sentences path). Both fail on main and pass with this change. Full test_semantic_chunker.py suite is green (28 passed). ruff check and ruff format --check are clean on the touched files.

One note: token-level splitting decodes token slices, so on uncased tokenizers the split pieces come back lowercased, this matches TokenChunker's existing behavior and only affects sentences that were too large to keep whole anyway.

Summary by CodeRabbit

  • Bug Fixes
    • Improved semantic chunking to consistently enforce chunk_size, including when a single sentence is too large.
    • Fixed an edge case where chunking could return oversized chunks when there were too few sentences relative to the similarity window.
  • Tests
    • Added regression tests for oversized delimiter-free inputs, verifying both returned chunk metadata and tokenizer-derived token counts respect chunk_size.

SemanticChunker only ever split between sentences, never inside one.
So a single sentence longer than chunk_size (common with delimiter-free
blobs like OpenAPI specs or long JSON) was emitted as one oversized
chunk. On the OpenAI OpenAPI YAML this produced a ~48k token chunk with
chunk_size=2048.

_split_groups now splits any sentence larger than chunk_size into
token-sized pieces (same approach as TokenChunker) before packing, so no
chunk exceeds the limit. The few-sentences early-return path now routes
through _split_groups too, since it previously skipped size enforcement
entirely.

Added regression tests for both paths.

Closes feyninc#222
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 17c40788-fc67-494a-a8bc-185b4217953d

📥 Commits

Reviewing files that changed from the base of the PR and between f6f366c and ffa7e45.

📒 Files selected for processing (2)
  • src/chonkie/chunker/semantic.py
  • tests/chunkers/test_semantic_chunker.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/chunkers/test_semantic_chunker.py
  • src/chonkie/chunker/semantic.py

📝 Walkthrough

Walkthrough

SemanticChunker now splits oversized sentences into chunk_size-bounded token pieces via a new helper, and applies this enforcement in both group-splitting logic and the short-input path. Regression tests cover oversized single sentences and short inputs with oversized content.

Changes

Semantic Chunker Size Enforcement

Layer / File(s) Summary
Oversized sentence splitting helper
src/chonkie/chunker/semantic.py
Adds _split_oversized_sentence, which tokenizes a sentence exceeding chunk_size, slices tokens into bounded groups, decodes them, and returns Sentence objects with updated indices and token counts.
Group splitting and short-input enforcement
src/chonkie/chunker/semantic.py
Updates _split_groups to accumulate token-split pieces within chunk_size, and changes chunk()'s short-input branch to run _split_groups and _create_chunks instead of returning one unbounded chunk.
Regression tests
tests/chunkers/test_semantic_chunker.py
Adds tests confirming oversized single-sentence input and oversized content below similarity_window both produce chunks with token_count <= chunk_size.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Chunk as chunk()
  participant SplitGroups as _split_groups
  participant SplitSentence as _split_oversized_sentence
  participant CreateChunks as _create_chunks

  Chunk->>SplitGroups: _split_groups([sentences])
  SplitGroups->>SplitSentence: split sentence if token_count > chunk_size
  SplitSentence-->>SplitGroups: return Sentence pieces
  SplitGroups-->>Chunk: return grouped pieces within chunk_size
  Chunk->>CreateChunks: _create_chunks(groups)
  CreateChunks-->>Chunk: return chunks respecting chunk_size
Loading

Poem

A rabbit hopped through tokens long,
Sliced each run that grew too strong,
No chunk shall burst its size decree,
Now bounded, neat, as chunks should be,
Thump-thump! Tests pass — hooray for me! 🐇✂️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change to split oversized sentences in SemanticChunker.
Linked Issues check ✅ Passed The fix addresses the reported token-limit bug by splitting oversized sentences and the early-return path, with regression tests covering both cases.
Out of Scope Changes check ✅ Passed The changes stay focused on the chunk-size bug fix and its tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to split oversized sentences that exceed the configured chunk_size in the SemanticChunker, ensuring that chunks respect the size limit even when a single sentence is extremely long. This includes handling the early-return path for fewer sentences and adding corresponding regression tests. The review feedback correctly identifies an issue in _split_oversized_sentence where all split sentence pieces are assigned the same start_index, leading to incorrect and overlapping index ranges, and provides a code suggestion to properly offset the indices.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/chonkie/chunker/semantic.py Outdated
Comment on lines +417 to +425
return [
Sentence(
text=text,
start_index=sentence.start_index,
end_index=sentence.start_index + len(text),
token_count=len(group),
)
for text, group in zip(texts, token_groups)
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the current implementation, all split Sentence pieces are created with the same start_index (i.e., sentence.start_index). This results in overlapping and incorrect index ranges for the split sentences. Instead, we should accumulate the character lengths of the processed pieces to correctly offset the start_index and end_index for each subsequent piece.

        sentences = []
        current_start = sentence.start_index
        for text, group in zip(texts, token_groups):
            sentences.append(
                Sentence(
                    text=text,
                    start_index=current_start,
                    end_index=current_start + len(text),
                    token_count=len(group),
                )
            )
            current_start += len(text)
        return sentences

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/chunkers/test_semantic_chunker.py (1)

101-103: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the actual tokenized chunk text, not only metadata.

These regressions can still pass if Chunk.token_count is underreported while chunk.text exceeds the tokenizer limit. Count each emitted chunk’s text with the chunker tokenizer to verify the downstream-safe invariant.

Proposed test strengthening
-    assert all([chunk.token_count <= chunk_size for chunk in chunks]), (
+    assert all(chunker.tokenizer.count_tokens(chunk.text) <= chunk_size for chunk in chunks), (
         "Every chunk must respect chunk_size even when a single sentence is oversized"
     )
@@
-    assert all([chunk.token_count <= chunk_size for chunk in chunks]), (
+    assert all(chunker.tokenizer.count_tokens(chunk.text) <= chunk_size for chunk in chunks), (
         "The few-sentences path must also split oversized sentences"
     )

Also applies to: 123-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/chunkers/test_semantic_chunker.py` around lines 101 - 103, The semantic
chunker test is only checking Chunk.token_count, which can miss regressions
where chunk.text is actually too long. Update the affected assertions in
test_semantic_chunker to verify each emitted chunk’s text by re-tokenizing
chunk.text with the same chunker tokenizer and comparing that count to
chunk_size. Use the existing chunk/chunks assertions in the semantic chunker
test to enforce the downstream-safe invariant, not just the metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/chonkie/chunker/semantic.py`:
- Line 416: The semantic chunking path in semantic.py is calling
tokenizer.decode_batch on token_groups, but that method is not guaranteed by the
tokenizer protocol and can break custom tokenizers that only implement
encode/decode. Update the logic in the chunking routine around the decode step
to iterate over each token group and use tokenizer.decode for each one,
preserving protocol compatibility while keeping the rest of the
oversized-sentence handling unchanged.

---

Nitpick comments:
In `@tests/chunkers/test_semantic_chunker.py`:
- Around line 101-103: The semantic chunker test is only checking
Chunk.token_count, which can miss regressions where chunk.text is actually too
long. Update the affected assertions in test_semantic_chunker to verify each
emitted chunk’s text by re-tokenizing chunk.text with the same chunker tokenizer
and comparing that count to chunk_size. Use the existing chunk/chunks assertions
in the semantic chunker test to enforce the downstream-safe invariant, not just
the metadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bec8cc71-e556-4a33-b124-f6356629c0b5

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6baea and f6f366c.

📒 Files selected for processing (2)
  • src/chonkie/chunker/semantic.py
  • tests/chunkers/test_semantic_chunker.py

Comment thread src/chonkie/chunker/semantic.py Outdated
- Decode token groups one at a time with decode() instead of
  decode_batch(), which isn't part of the tokenizer protocol and can
  break custom tokenizers.
- Accumulate character offsets so split pieces get sequential
  start/end indices instead of all sharing the sentence's start.
- Regression tests now re-tokenize the chunk text so they can't pass on
  underreported token_count alone.
@chakshu-dhannawat

Copy link
Copy Markdown
Author

Pushed a follow-up addressing the review notes:

  • Switched from decode_batch to decoding each token group with decode(), since decode_batch isn't part of the tokenizer protocol and could break custom tokenizers on this path.
  • Split pieces now get sequential start/end indices instead of all sharing the original sentence's start.
  • The regression tests re-tokenize the chunk text now, so they can't pass on an underreported token_count alone.

Tests still green (28 passed) and ruff is clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: chunking size not consistent

1 participant