Repository navigation
feat!: align page_number metadata across splitters and fix drift in overlapping splits - #13006
Conversation
…overlapping splits
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||
| # The page the chunk's first non-page-break character is on: breaks before the chunk, plus the | ||
| # ones it opens with. Derived from the chunk's absolute offset rather than a running counter, so | ||
| # a break repeated in an overlapping tail cannot be counted twice. | ||
| new_doc.meta["page_number"] = 1 + content.count("\f", 0, current_position) + _leading_page_breaks(chunk) |
There was a problem hiding this comment.
I believe page_number now inherits an existing drift in split_idx_start when split_unit="word" and split_overlap > 0. _create_chunk_starting_with_overlap joins the overlap and the chunk with a " " that isn't in the source text, but current_position still advances over it. So split_idx_start moves one character further ahead with every chunk, and the chunks pass page breaks they haven't reached yet. It keeps growing whenever the chunk plus overlap fits within split_length (when it doesn't, the chunk gets trimmed and the offset stays close).
from haystack import Document
from haystack.components.preprocessors import RecursiveDocumentSplitter
# 8 pages of one 3-word sentence each: "p1a p1b p1c.\fp2a p2b p2c.\f..."
text = "\f".join(f"p{p}a p{p}b p{p}c." for p in range(1, 9))
# each chunk is one 3-word sentence; with the 1-word overlap it is exactly split_length words
splitter = RecursiveDocumentSplitter(split_length=4, split_overlap=1, split_unit="word", separators=["."])
for chunk in splitter.run(documents=[Document(content=text)])["documents"]:
start = chunk.meta["split_idx_start"]
first_word = chunk.content.split()[0]
print(
f"page_number={chunk.meta['page_number']} split_idx_start={start:3} actual_start={text.index(first_word):3} "
f"content={chunk.content!r}"
)On this branch the last two chunks start on pages 6 and 7 but are reported on 7 and 8:
page_number=1 split_idx_start= 0 actual_start= 0 content='p1a p1b p1c.'
page_number=1 split_idx_start= 8 actual_start= 8 content='p1c. \x0cp2a p2b p2c.'
page_number=2 split_idx_start= 22 actual_start= 21 content='p2c. \x0cp3a p3b p3c.'
page_number=3 split_idx_start= 36 actual_start= 34 content='p3c. \x0cp4a p4b p4c.'
page_number=4 split_idx_start= 50 actual_start= 47 content='p4c. \x0cp5a p5b p5c.'
page_number=5 split_idx_start= 64 actual_start= 60 content='p5c. \x0cp6a p6b p6c.'
page_number=7 split_idx_start= 78 actual_start= 73 content='p6c. \x0cp7a p7b p7c.'
page_number=8 split_idx_start= 92 actual_start= 86 content='p7c. \x0cp8a p8b p8c.'
There was a problem hiding this comment.
Good catch, fixed in 47aeb37.
Word-overlap chunks are now cut out of the source text instead of being rebuilt with " " joins, so split_idx_start and page_number match the actual start in your example.
While at it, I also fixed two other places where the chunks didn't add up to the source text: the whitespace after the last sentence got dropped, and with keep_white_spaces=False sentences within a chunk were glued together.
Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
Related Issues
page_numberinRecursiveDocumentSplitterpoints to the page the chunk ends on #12926Proposed Changes:
Problem. The splitters disagreed on what
page_numbermeans:RecursiveDocumentSplitterreported the page a chunk ends on, the other two the page it starts onDocumentSplitter,MarkdownHeaderSplitterandEmbeddingBasedDocumentSplitterreported the previous page for a chunk opening with form feeds, although all of its text is on the next one.Fix. One convention, defined in the new
_page_numbers.py:page_numberis the page of the chunk's first non-page-break character. Exception: a chunk of only page breaks is an empty page and keeps its start page —split_by="page"emits one per blank page.RecursiveDocumentSplitterno longer keeps a running page counter; the page comes from the chunk's offset, whichsplit_idx_startalready tracks. Being stateless, this also removes the drift in #12909 — a break inside an overlapping tail was counted once per chunk it appeared in, pushing pages past the document's page count.Docstrings,
docs-websitepages and anupgraderelease note updated:page_numberis user-visible and stored values need re-indexing.How did you test it?
Seven new unit tests:
test_run_page_number_is_the_page_the_chunk_starts_on— the issue's case, assertingRecursiveDocumentSplittermatchesDocumentSplitterchunk- and page-for-pagetest_run_page_number_does_not_drift_with_overlap— the fix: count page breaks in overlapping regions once in RecursiveDocumentSplitter #12909 reproductiontest_add_page_number_to_metadata_for_blank_pages—split_by="page"over"Hello\f\fWorld"and"\fHello"test_page_number_of_split_starting_with_page_break/..._multi_character_page_break— leading breaks inMarkdownHeaderSplitter, the latter withpage_break_character="<PAGE>"test_page_number_of_secondary_splits_under_a_chunk_starting_with_a_page_break— header chunk opening with a break, then split againtest_create_documents_from_splits_leading_page_breaks—EmbeddingBasedDocumentSplitter, previously uncoveredFive existing assertions changed.
Notes for the reviewer
Checklist
fix:,feat:,build:,chore:,ci:,docs:,style:,refactor:,perf:,test:and added!in case the PR includes breaking changes.