fix: keep inter-sentence whitespace when a sentence ends with a closing quote - #12431
fix: keep inter-sentence whitespace when a sentence ends with a closing quote#12431winklemad wants to merge 2 commits into
Conversation
…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 is attempting to deploy a commit to the deepset Team on Vercel. A member of the Team first needs to authorize it. |
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||
bogdankostic
left a comment
There was a problem hiding this comment.
Thanks for your PR @winklemad! I found a case where the changes alter the behavior that is intended, I left an in-line comment about this.
Also, I'm interested, did you run into the splitting problem that your PR should fix yourself while processing your own documents?
|
|
||
| # with keep_white_spaces=True a span also covers the whitespace up to the next sentence, the rules below | ||
| # look at where the sentence itself ends | ||
| end = start + len(text[start:end].rstrip()) |
There was a problem hiding this comment.
This moves the boundary such that the rule documented three lines down — # sentence.", sentence -> no split — stops firing:
s = SentenceSplitter(language="en", keep_white_spaces=True)
[x["sentence"] for x in s.split_sentences('He said "Hi.", then left.')]
# main: ['He said "Hi.", then left.']
# this PR: ['He said "Hi."', ', then left.']Same for ."+;/:/— and the 'Hi.', form. Via DocumentSplitter you get a chunk starting with a comma.
Why. The quote span is (8, 13). On main the boundary lands at 12, inside the quote, so quote_start < end < quote_end joins. The widened closer class moves it to 13 — exactly quote_end — where only the ? rule applies. rstrip() can't help: there's no whitespace to strip, the next char is ,.
Related Issues
Proposed Changes:
SentenceSplitter(keep_white_spaces=True)silently drops the whitespace between two sentences when the first one ends with a closing quote:Root cause.
CustomPunktLanguageVars.period_context_reextendsSentEndCharswith closing brackets only (self._re_sent_end_chars + r"[\)\]}]*"). For."the whitespace sits behind the closing quote, so the\s*in_period_context_fmtnever 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_startshifts, so chunks no longer index back into the source. This reachesDocumentSplitter(split_by="sentence", andsplit_by="word"withrespect_sentence_boundary=True),RecursiveDocumentSplitter,MarkdownHeaderSplitterandEmbeddingBasedDocumentSplitter— any prose with a quoted sentence. It also breaks the assumption stated inembedding_based_document_splitter.pythat "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_joincompares 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 caseHe 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 atDocumentSplitterlevel that everysplit_idx_startstill 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 fullhatch run test:unit,hatch run test:types(clean) andhatch run fmt. Since the pattern is version-sensitive, I re-ran the preprocessor tests against the declared floornltk==3.9.1as well — 73 passed there too.Notes for the reviewer
) ] } " ' ’ ” ». 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.re_boundary_realignmentalso lists the opening‘ “ «; including them made no difference on any case I tried, so I left them out._needs_joinis 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
fix:.