Skip to content

fix: keep inter-sentence whitespace when a sentence ends with a closing quote - #12431

Open
winklemad wants to merge 1 commit into
deepset-ai:mainfrom
winklemad:fix/sentence-splitter-whitespace-after-closing-quote
Open

fix: keep inter-sentence whitespace when a sentence ends with a closing quote#12431
winklemad wants to merge 1 commit into
deepset-ai:mainfrom
winklemad:fix/sentence-splitter-whitespace-after-closing-quote

Conversation

@winklemad

Copy link
Copy Markdown
Contributor

Related Issues

  • No existing issue — I found this while checking that sentence chunks still map back onto the source text. Happy to open one if you'd rather track it separately.

Proposed Changes:

SentenceSplitter(keep_white_spaces=True) silently drops the whitespace between two sentences when the first one ends with a closing quote:

from haystack.components.preprocessors.sentence_tokenizer import SentenceSplitter

splitter = SentenceSplitter(keep_white_spaces=True)
text = 'He said "Hello there." Then he left.'
spans = splitter.split_sentences(text)

[(s["start"], s["end"]) for s in spans]     # [(0, 22), (23, 36)]  <- char 22 is in no span
"".join(s["sentence"] for s in spans)       # 'He said "Hello there."Then he left.'

Root cause. CustomPunktLanguageVars.period_context_re extends SentEndChars with closing brackets only (self._re_sent_end_chars + r"[\)\]}]*"). For ." the whitespace sits behind the closing quote, so the \s* in _period_context_fmt never reaches it; nltk's boundary realignment then ends the sentence at the quote but moves the next break past the whitespace, and those characters end up in none of the returned spans. He said (hi.) Then left. round-trips correctly today, which is exactly the bracket case that is already covered — closing quotes are simply missing from that character class.

Impact. The characters are lost from the chunk text and every following split_idx_start shifts, so chunks no longer index back into the source. This reaches DocumentSplitter (split_by="sentence", and split_by="word" with respect_sentence_boundary=True), RecursiveDocumentSplitter, MarkdownHeaderSplitter and EmbeddingBasedDocumentSplitter — any prose with a quoted sentence. It also breaks the assumption stated in embedding_based_document_splitter.py that "chunks are contiguous substrings of the original text ... so the character offset is simply accumulated".

Fix. Match closing quotes as well as closing brackets after a sentence ending. Because a span now carries the trailing whitespace, _needs_join compares the split rules against the end of the sentence itself (text[start:end].rstrip()); without that line the existing "a cited question is not a sentence boundary" rule would silently stop firing, so there is a test guarding it.

Chunk boundaries change for text containing quoted sentences, so re-indexing an existing corpus produces different chunks — this is called out in the release note.

I deliberately did not touch recursive_splitter.py: the overlap problem in #12281 / #12284 has a different root cause and is already being worked on.

How did you test it?

Unit tests, added first and confirmed failing on unmodified code (7 failed, 2 passed), then passing with the fix (9 passed):

  • test_split_sentences_keeps_white_spaces_after_a_closing_quote — parametrized over ", ', “ ”, ‘ ’, « » and !", asserting no character is lost and that the spans still tile the text. It includes the bracket case He said (two.) Three. as a control that already passed before the fix.
  • test_split_sentences_keeps_a_cited_question_joined — guards the existing split rule for cited questions.
  • test_run_split_by_sentence_quoted_text_keeps_offsets_aligned — checks at DocumentSplitter level that every split_idx_start still indexes its own chunk in the source text.

Also ran hatch run test:unit test/components/preprocessors/ (367 passed, 358 before the new tests), the full hatch run test:unit, hatch run test:types (clean) and hatch run fmt. Since the pattern is version-sensitive, I re-ran the preprocessor tests against the declared floor nltk==3.9.1 as well — 73 passed there too.

Notes for the reviewer

  • The closer set is ) ] } " ' ’ ” ». If apostrophe-heavy languages are a concern I'm happy to drop ' from it — the remaining characters still fix the common ." and cases, just say the word and I'll push the change.
  • I kept the set to actual closing characters. nltk's own re_boundary_realignment also lists the opening ‘ “ «; including them made no difference on any case I tried, so I left them out.
  • The one-line change in _needs_join is required by the fix, not a drive-by: it keeps the existing quote/numeration rules working now that a span can end with whitespace.

Checklist

…ng quote

With keep_white_spaces=True the custom Punkt pattern only allowed closing
brackets after a sentence ending, so for `He said "Hi." Bye.` the whitespace
sits behind the closing quote and the pattern cannot reach it. The space ends
up in none of the returned spans: it is dropped from the chunk text and shifts
the split_idx_start offsets of every following chunk.

Match closing quotes as well, and compare the split rules against the end of
the sentence itself, since a span now carries the trailing whitespace.
@winklemad
winklemad requested a review from a team as a code owner August 21, 2026 21:27
@winklemad
winklemad requested review from bogdankostic and removed request for a team August 21, 2026 21:27
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@winklemad is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/components/preprocessors
  sentence_tokenizer.py
Project Total  

This report was generated by python-coverage-comment-action

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant