Skip to content

Commit 6d4b437

Browse files
authored
fix: set source_id on RecursiveDocumentSplitter chunks (#12155)
Signed-off-by: onatozmenn <onatozmen44@gmail.com>
1 parent 6f75d43 commit 6d4b437

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

haystack/components/preprocessors/recursive_splitter.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class RecursiveDocumentSplitter:
3838
from haystack import Document
3939
from haystack.components.preprocessors import RecursiveDocumentSplitter
4040
41-
chunker = RecursiveDocumentSplitter(split_length=260, split_overlap=0, separators=["\\n\\n", "\\n", ".", " "])
41+
chunker = RecursiveDocumentSplitter(split_length=15, split_overlap=0, separators=["\\n\\n", "\\n", ".", " "])
4242
text = ('''Artificial intelligence (AI) - Introduction
4343
4444
AI, in its broadest sense, is intelligence exhibited by machines, particularly computer systems.
@@ -47,10 +47,11 @@ class RecursiveDocumentSplitter:
4747
doc_chunks = chunker.run([doc])
4848
print(doc_chunks["documents"])
4949
# [
50-
# Document(id=..., content: 'Artificial intelligence (AI) - Introduction\\n\\n', meta: {'original_id': '...', 'split_id': 0, 'split_idx_start': 0, '_split_overlap': []})
51-
# Document(id=..., content: 'AI, in its broadest sense, is intelligence exhibited by machines, particularly computer systems.\\n', meta: {'original_id': '...', 'split_id': 1, 'split_idx_start': 45, '_split_overlap': []})
52-
# Document(id=..., content: 'AI technology is widely used throughout industry, government, and science.', meta: {'original_id': '...', 'split_id': 2, 'split_idx_start': 142, '_split_overlap': []})
53-
# Document(id=..., content: ' Some high-profile applications include advanced web search engines; recommendation systems; interac...', meta: {'original_id': '...', 'split_id': 3, 'split_idx_start': 216, '_split_overlap': []})
50+
# Document(id=..., content: 'Artificial intelligence (AI) - Introduction\\n\\n', meta: {'source_id': '...', 'parent_id': '...', 'split_id': 0, 'split_idx_start': 0, '_split_overlap': None, 'page_number': 1})
51+
# Document(id=..., content: 'AI, in its broadest sense, is intelligence exhibited by machines, particularly computer systems.\\n', meta: {'source_id': '...', 'parent_id': '...', 'split_id': 1, 'split_idx_start': 45, '_split_overlap': None, 'page_number': 1})
52+
# Document(id=..., content: 'AI technology is widely used throughout industry, government, and science.', meta: {'source_id': '...', 'parent_id': '...', 'split_id': 2, 'split_idx_start': 142, '_split_overlap': None, 'page_number': 1})
53+
# Document(id=..., content: ' Some high-profile applications include advanced web search engines; recommendation systems; interac...', meta: {'source_id': '...', 'parent_id': '...', 'split_id': 3, 'split_idx_start': 216, '_split_overlap': None, 'page_number': 1})
54+
# Document(id=..., content: 'vehicles; generative and creative tools; and superhuman play and analysis in strategy games.', meta: {'source_id': '...', 'parent_id': '...', 'split_id': 4, 'split_idx_start': 350, '_split_overlap': None, 'page_number': 1})
5455
# ]
5556
```
5657
""" # noqa: E501
@@ -425,6 +426,7 @@ def _run_one(self, doc: Document) -> list[Document]:
425426

426427
for split_nr, chunk in enumerate(chunks):
427428
meta = deepcopy(doc.meta)
429+
meta["source_id"] = doc.id
428430
meta["parent_id"] = doc.id
429431
meta["split_id"] = split_nr
430432
meta["split_idx_start"] = current_position
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed ``RecursiveDocumentSplitter`` not setting the ``source_id`` meta field on the chunks
5+
it produces. It wrote only ``parent_id``, while every other splitter in the library
6+
(``DocumentSplitter``, ``CSVDocumentSplitter``, ``EmbeddingBasedDocumentSplitter``,
7+
``HierarchicalDocumentSplitter``, ``MarkdownHeaderSplitter`` and ``PythonCodeSplitter``)
8+
writes ``source_id``. Components that follow that convention therefore rejected its output:
9+
``SentenceWindowRetriever`` reads ``source_id`` by default and raises when it is absent, so
10+
it failed with "The retrieved documents must have 'source_id' in their metadata." on a
11+
pipeline that worked with any other splitter. Chunks now carry ``source_id`` as well as
12+
``parent_id``, which keeps its previous value for callers already reading it.

test/components/preprocessors/test_recursive_splitter.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from haystack import Document, Pipeline
1212
from haystack.components.preprocessors.recursive_splitter import RecursiveDocumentSplitter
1313
from haystack.components.preprocessors.sentence_tokenizer import SentenceSplitter
14+
from haystack.components.retrievers.sentence_window_retriever import SentenceWindowRetriever
15+
from haystack.document_stores.in_memory import InMemoryDocumentStore
1416

1517

1618
def test_get_custom_sentence_tokenizer_success():
@@ -1049,12 +1051,30 @@ def test_recursive_splitter_generates_unique_ids_and_correct_meta():
10491051
# IDs must be unique
10501052
assert len({c.id for c in chunks}) == len(chunks)
10511053

1052-
# parent_id and split_id checks
1054+
# source_id, parent_id and split_id checks
10531055
for idx, chunk in enumerate(chunks):
1056+
assert chunk.meta["source_id"] == source_doc.id
10541057
assert chunk.meta["parent_id"] == source_doc.id
10551058
assert chunk.meta["split_id"] == idx
10561059

10571060

1061+
def test_recursive_splitter_output_works_with_sentence_window_retriever():
1062+
"""SentenceWindowRetriever looks up `source_id` by default and raises when it is
1063+
absent"""
1064+
source_doc = Document(content="Haystack is awesome. " * 10)
1065+
chunks = RecursiveDocumentSplitter(split_length=3).run([source_doc])["documents"]
1066+
assert len(chunks) > 2
1067+
1068+
store = InMemoryDocumentStore()
1069+
store.write_documents(chunks)
1070+
1071+
result = SentenceWindowRetriever(document_store=store, window_size=1).run(retrieved_documents=[chunks[1]])
1072+
1073+
# The middle chunk plus one neighbour on each side.
1074+
assert len(result["context_documents"]) == 3
1075+
assert result["context_windows"]
1076+
1077+
10581078
def test_warm_up_is_idempotent_sentence(monkeypatch):
10591079
splitter = RecursiveDocumentSplitter(separators=["sentence", " "])
10601080

0 commit comments

Comments
 (0)