Skip to content

Commit 1216c61

Browse files
Osamaali313sjrl
andauthored
fix: populate _split_overlap metadata for word/token units in RecursiveDocumentSplitter (#11825)
Co-authored-by: Sebastian Husch Lee <10526848+sjrl@users.noreply.github.com>
1 parent d75b2e0 commit 1216c61

3 files changed

Lines changed: 68 additions & 8 deletions

File tree

haystack/components/preprocessors/recursive_splitter.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -403,17 +403,14 @@ def _fall_back_to_fixed_chunking(self, text: str, split_units: Literal["word", "
403403

404404
def _add_overlap_info(self, curr_pos: int, new_doc: Document, new_docs: list[Document]) -> None:
405405
prev_doc = new_docs[-1]
406-
overlap_length = self._chunk_length(prev_doc.content) - (curr_pos - prev_doc.meta["split_idx_start"]) # type: ignore
406+
# curr_pos and split_idx_start are character offsets, so measure the
407+
# overlap and range in characters too (not via _chunk_length, which returns a word/token count).
408+
prev_doc_length = len(prev_doc.content) # type: ignore
409+
overlap_length = prev_doc_length - (curr_pos - prev_doc.meta["split_idx_start"])
407410
if overlap_length > 0:
408411
prev_doc.meta["_split_overlap"].append({"doc_id": new_doc.id, "range": (0, overlap_length)})
409412
new_doc.meta["_split_overlap"].append(
410-
{
411-
"doc_id": prev_doc.id,
412-
"range": (
413-
self._chunk_length(prev_doc.content) - overlap_length, # type: ignore
414-
self._chunk_length(prev_doc.content), # type: ignore
415-
),
416-
}
413+
{"doc_id": prev_doc.id, "range": (prev_doc_length - overlap_length, prev_doc_length)}
417414
)
418415

419416
def _run_one(self, doc: Document) -> list[Document]:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed ``RecursiveDocumentSplitter`` not populating the ``_split_overlap``
5+
metadata when ``split_unit`` is ``"word"`` or ``"token"`` and ``split_overlap``
6+
is greater than 0. The overlap length was computed with a word/token count
7+
while the offsets it is compared against are character positions, so it
8+
became negative and the overlap metadata was silently dropped. The overlap
9+
is now measured in characters, matching the ranges it records.

test/components/preprocessors/test_recursive_splitter.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,60 @@ def test_run_split_by_dot_and_overlap_1_word_unit_split_idx_start():
750750
)
751751

752752

753+
def test_word_unit_split_populates_split_overlap_metadata():
754+
"""
755+
_split_overlap ranges must be character offsets into the referenced chunk when
756+
split_unit="word" and split_overlap > 0
757+
"""
758+
splitter = RecursiveDocumentSplitter(split_length=4, split_overlap=1, separators=["."], split_unit="word")
759+
text = "This is sentence one. This is sentence two. This is sentence three. This is sentence four."
760+
chunks = splitter.run([Document(content=text)])["documents"]
761+
assert len(chunks) == 5
762+
763+
assert chunks[0].content == "This is sentence one."
764+
assert chunks[0].meta["_split_overlap"] == [{"doc_id": chunks[1].id, "range": (0, 4)}] # "one."
765+
766+
assert chunks[1].content == "one. This is sentence"
767+
assert chunks[1].meta["_split_overlap"] == [
768+
{"doc_id": chunks[0].id, "range": (17, 21)}, # "one."
769+
{"doc_id": chunks[2].id, "range": (0, 8)}, # "sentence"
770+
]
771+
772+
assert chunks[2].content == "sentence two. This is"
773+
assert chunks[2].meta["_split_overlap"] == [
774+
{"doc_id": chunks[1].id, "range": (13, 21)}, # "sentence"
775+
{"doc_id": chunks[3].id, "range": (0, 2)}, # "is"
776+
]
777+
778+
assert chunks[3].content == "is sentence three. This"
779+
assert chunks[3].meta["_split_overlap"] == [
780+
{"doc_id": chunks[2].id, "range": (19, 21)}, # "is"
781+
{"doc_id": chunks[4].id, "range": (0, 4)}, # "This"
782+
]
783+
784+
assert chunks[4].content == "This is sentence four."
785+
assert chunks[4].meta["_split_overlap"] == [{"doc_id": chunks[3].id, "range": (19, 23)}] # "This"
786+
787+
788+
@pytest.mark.integration
789+
def test_token_unit_split_populates_split_overlap_metadata():
790+
"""
791+
_split_overlap ranges must be character offsets into the referenced chunk when
792+
split_unit="token" and split_overlap > 0
793+
"""
794+
splitter = RecursiveDocumentSplitter(split_length=4, split_overlap=1, separators=["."], split_unit="token")
795+
text = "This is sentence one. This is sentence two. This is sentence three. This is sentence four."
796+
chunks = splitter.run([Document(content=text)])["documents"]
797+
assert len(chunks) == 8
798+
799+
assert chunks[0].meta["_split_overlap"] == [{"doc_id": chunks[1].id, "range": (0, 4)}]
800+
assert chunks[1].meta["_split_overlap"] == [
801+
{"doc_id": chunks[0].id, "range": (16, 20)},
802+
{"doc_id": chunks[2].id, "range": (0, 1)},
803+
]
804+
assert chunks[7].meta["_split_overlap"] == [{"doc_id": chunks[6].id, "range": (9, 18)}]
805+
806+
753807
def test_run_trigger_dealing_with_remaining_word_larger_than_split_length():
754808
splitter = RecursiveDocumentSplitter(split_length=3, split_overlap=2, separators=["."], split_unit="word")
755809
text = """A simple sentence1. A bright sentence2. A clever sentence3"""

0 commit comments

Comments
 (0)