Files
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

841 lines
33 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
from collections import defaultdict
from unittest.mock import ANY
import pytest
from haystack import Document
from haystack.components.preprocessors.markdown_header_splitter import MarkdownHeaderSplitter
# Fixtures
@pytest.fixture
def sample_text():
return (
"# Header 1\n"
"Content under header 1.\n"
"## Header 1.1\n"
"### Subheader 1.1.1\n"
"Content under sub-header 1.1.1\n"
"## Header 1.2\n"
"### Subheader 1.2.1\n"
"Content under header 1.2.1.\n"
"### Subheader 1.2.2\n"
"Content under header 1.2.2.\n"
"### Subheader 1.2.3\n"
"Content under header 1.2.3."
)
@pytest.fixture
def sample_text_with_page_breaks():
return (
"# Header 1\n"
"Content under header 1.\n\f\n"
"## Header 1.1\n"
"### Subheader 1.1.1\n"
"Content under sub-header 1.1.1\n\f\n"
"## Header 1.2\n"
"### Subheader 1.2.1\n"
"Content under header 1.2.1.\n\f\n"
"### Subheader 1.2.2\n"
"Content under header 1.2.2.\n\f\n"
"### Subheader 1.2.3\n"
"Content under header 1.2.3."
)
# Basic splitting and structure
def test_basic_split(sample_text):
splitter = MarkdownHeaderSplitter()
docs = [Document(content=sample_text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Check that content is present and correct
# Test first split
header1_doc = split_docs[0]
assert header1_doc.meta["split_id"] == 0
assert header1_doc.meta["page_number"] == 1
assert header1_doc.content == "# Header 1\nContent under header 1.\n"
# Test second split
subheader111_doc = split_docs[1]
assert subheader111_doc.meta["split_id"] == 1
assert subheader111_doc.meta["page_number"] == 1
assert subheader111_doc.content == "## Header 1.1\n### Subheader 1.1.1\nContent under sub-header 1.1.1\n"
# Test third split
subheader121_doc = split_docs[2]
assert subheader121_doc.meta["split_id"] == 2
assert subheader121_doc.meta["page_number"] == 1
assert subheader121_doc.content == "## Header 1.2\n### Subheader 1.2.1\nContent under header 1.2.1.\n"
# Test fourth split
subheader122_doc = split_docs[3]
assert subheader122_doc.meta["split_id"] == 3
assert subheader122_doc.meta["page_number"] == 1
assert subheader122_doc.content == "### Subheader 1.2.2\nContent under header 1.2.2.\n"
# Test fifth split
subheader123_doc = split_docs[4]
assert subheader123_doc.meta["split_id"] == 4
assert subheader123_doc.meta["page_number"] == 1
assert subheader123_doc.content == "### Subheader 1.2.3\nContent under header 1.2.3."
# Reconstruct original text
reconstructed_doc = "".join([doc.content for doc in split_docs])
assert reconstructed_doc == sample_text
def test_keep_headers_preserves_parent_headers_for_first_child():
text = (
"# Header 1\n"
"Intro text\n\n"
"## Header 1.1\n"
"Text 1\n\n"
"## Header 1.2\n"
"Text 2\n\n"
"### Header 1.2.1\n"
"Text 3\n\n"
"### Header 1.2.2\n"
"Text 4\n"
)
splitter = MarkdownHeaderSplitter(keep_headers=True)
split_docs = splitter.run(documents=[Document(content=text)])["documents"]
assert [(doc.meta["header"], doc.meta["parent_headers"]) for doc in split_docs] == [
("Header 1", []),
("Header 1.1", ["Header 1"]),
("Header 1.2", ["Header 1"]),
("Header 1.2.1", ["Header 1", "Header 1.2"]),
("Header 1.2.2", ["Header 1", "Header 1.2"]),
]
# reconstruct original text
reconstructed_text = "".join(doc.content for doc in split_docs)
assert reconstructed_text == text
def test_split_without_headers(sample_text):
splitter = MarkdownHeaderSplitter(keep_headers=False)
docs = [Document(content=sample_text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Should split into all headers with content
headers = [doc.meta["header"] for doc in split_docs]
assert "Header 1" in headers
assert "Subheader 1.1.1" in headers
assert "Subheader 1.2.1" in headers
assert "Subheader 1.2.2" in headers
assert "Subheader 1.2.3" in headers
# Check that content is present and correct
# Test first split
header1_doc = split_docs[0]
assert header1_doc.meta["header"] == "Header 1"
assert header1_doc.meta["split_id"] == 0
assert header1_doc.meta["page_number"] == 1
assert header1_doc.meta["parent_headers"] == []
assert header1_doc.content == "\nContent under header 1.\n"
# Test second split
subheader111_doc = split_docs[1]
assert subheader111_doc.meta["header"] == "Subheader 1.1.1"
assert subheader111_doc.meta["split_id"] == 1
assert subheader111_doc.meta["page_number"] == 1
assert subheader111_doc.meta["parent_headers"] == ["Header 1", "Header 1.1"]
assert subheader111_doc.content == "\nContent under sub-header 1.1.1\n"
# Test third split
subheader121_doc = split_docs[2]
assert subheader121_doc.meta["header"] == "Subheader 1.2.1"
assert subheader121_doc.meta["split_id"] == 2
assert subheader121_doc.meta["page_number"] == 1
assert subheader121_doc.meta["parent_headers"] == ["Header 1", "Header 1.2"]
assert subheader121_doc.content == "\nContent under header 1.2.1.\n"
# Test fourth split
subheader122_doc = split_docs[3]
assert subheader122_doc.meta["header"] == "Subheader 1.2.2"
assert subheader122_doc.meta["split_id"] == 3
assert subheader122_doc.meta["page_number"] == 1
assert subheader122_doc.meta["parent_headers"] == ["Header 1", "Header 1.2"]
assert subheader122_doc.content == "\nContent under header 1.2.2.\n"
# Test fifth split
subheader123_doc = split_docs[4]
assert subheader123_doc.meta["header"] == "Subheader 1.2.3"
assert subheader123_doc.meta["split_id"] == 4
assert subheader123_doc.meta["page_number"] == 1
assert subheader123_doc.meta["parent_headers"] == ["Header 1", "Header 1.2"]
assert subheader123_doc.content == "\nContent under header 1.2.3."
def test_split_no_headers():
splitter = MarkdownHeaderSplitter()
docs = [Document(content="No headers here."), Document(content="Just some text without headers.")]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Should return one doc per input, and no header key in meta
assert len(split_docs) == 2
for doc in split_docs:
assert "header" not in doc.meta
# Sanity Checks
assert split_docs[0].content == docs[0].content
assert split_docs[1].content == docs[1].content
def test_split_multiple_documents(sample_text):
splitter = MarkdownHeaderSplitter(keep_headers=False)
docs = [
Document(content=sample_text),
Document(content="# Another Header\nSome content."),
Document(content="# H1\nA"),
Document(content="# H2\nB"),
]
result = splitter.run(documents=docs)
split_docs = result["documents"]
assert len(split_docs) == 8
# First 5 splits are from sample_text
assert split_docs[5].meta["header"] == "Another Header"
assert split_docs[6].meta["header"] == "H1"
assert split_docs[7].meta["header"] == "H2"
# Verify that split_ids are per-parent-document
splits_by_source = defaultdict(list)
for doc in split_docs:
splits_by_source[doc.meta["source_id"]].append(doc.meta["split_id"])
# Each parent document should have split_ids starting from 0
for split_ids in splits_by_source.values():
assert split_ids == list(range(len(split_ids)))
def test_split_only_headers():
text = "# H1\n# H2\n# H3"
splitter = MarkdownHeaderSplitter()
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Return doc without content unchunked
assert len(split_docs) == 1
assert split_docs[0].content == text
# Metadata preservation
def test_preserve_document_metadata():
"""Test that document metadata is preserved through splitting."""
splitter = MarkdownHeaderSplitter(keep_headers=False) # keep_headers=True case is covered by this test too
docs = [Document(content="# Header\nContent", meta={"source": "test", "importance": "high", "custom_field": 123})]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Original metadata should be preserved
assert split_docs[0].meta["source"] == "test"
assert split_docs[0].meta["importance"] == "high"
assert split_docs[0].meta["custom_field"] == 123
# New metadata should be added
assert "header" in split_docs[0].meta
assert split_docs[0].meta["header"] == "Header"
assert "split_id" in split_docs[0].meta
assert split_docs[0].meta["split_id"] == 0
assert split_docs[0].content == "\nContent"
def test_secondary_split_keeps_content_before_embedded_header():
"""With keep_headers=False, prose before an embedded lower-level header must
not be dropped during the secondary split."""
splitter = MarkdownHeaderSplitter(
keep_headers=False, header_split_levels=[1], secondary_split="word", split_length=100
)
docs = splitter.run(documents=[Document(content="# Main\nintro paragraph text\n## Sub\nmore text\n")])["documents"]
combined = "".join(doc.content or "" for doc in docs)
assert "intro paragraph text" in combined
def test_secondary_split_keeps_content_before_code_fence_comment():
"""With keep_headers=False, prose before a '#' comment inside a fenced code block must
not be dropped during the secondary split (the comment is not a real header)."""
splitter = MarkdownHeaderSplitter(
keep_headers=False, header_split_levels=[1], secondary_split="word", split_length=100
)
content = "# Main\nsome intro text\n```python\n# a comment in code\n```\nmore text\n"
docs = splitter.run(documents=[Document(content=content)])["documents"]
combined = "".join(doc.content or "" for doc in docs)
assert "some intro text" in combined
# Error and edge case handling
def test_non_text_document():
"""Test that the component correctly handles non-text documents."""
splitter = MarkdownHeaderSplitter()
docs = [Document(content=None)]
# Should raise ValueError about text documents
with pytest.raises(ValueError, match="only works with text documents"):
splitter.run(documents=docs)
def test_empty_document_list():
"""Test handling of an empty document list."""
splitter = MarkdownHeaderSplitter()
result = splitter.run(documents=[])
assert result["documents"] == []
class TestHeaderSplitLevels:
def test_default_splits_on_all_levels(self, sample_text):
"""Default behaviour: all six header levels create split boundaries.
Note: empty headers (no content of their own) are folded into the next chunk via pending_headers rather
than appearing as standalone entries in meta.
"""
splitter = MarkdownHeaderSplitter() # header_split_levels defaults to [1,2,3,4,5,6]
docs = splitter.run(documents=[Document(content=sample_text)])["documents"]
# sample_text has 5 chunks with content; "Header 1.1" and "Header 1.2" are empty headers prepended to their
# first child and do not appear as their own split boundary
assert len(docs) == 5
assert docs[0].content == "# Header 1\nContent under header 1.\n"
assert docs[1].content == "## Header 1.1\n### Subheader 1.1.1\nContent under sub-header 1.1.1\n"
assert docs[2].content == "## Header 1.2\n### Subheader 1.2.1\nContent under header 1.2.1.\n"
assert docs[3].content == "### Subheader 1.2.2\nContent under header 1.2.2.\n"
assert docs[4].content == "### Subheader 1.2.3\nContent under header 1.2.3."
headers = [doc.meta["header"] for doc in docs]
assert headers == ["Header 1", "Subheader 1.1.1", "Subheader 1.2.1", "Subheader 1.2.2", "Subheader 1.2.3"]
def test_h1_and_h2_only(self, sample_text):
"""Only h1/h2 headers create splits; h3+ content is absorbed into the preceding chunk."""
splitter = MarkdownHeaderSplitter(header_split_levels=[1, 2])
docs = splitter.run(documents=[Document(content=sample_text)])["documents"]
assert len(docs) == 3
assert docs[0].content == "# Header 1\nContent under header 1.\n"
assert docs[1].content == "## Header 1.1\n### Subheader 1.1.1\nContent under sub-header 1.1.1\n"
assert docs[2].content == (
"## Header 1.2\n"
"### Subheader 1.2.1\nContent under header 1.2.1.\n"
"### Subheader 1.2.2\nContent under header 1.2.2.\n"
"### Subheader 1.2.3\nContent under header 1.2.3."
)
headers = [doc.meta["header"] for doc in docs]
assert headers == ["Header 1", "Header 1.1", "Header 1.2"]
# h3 headers must not appear as split boundaries
assert "Subheader 1.1.1" not in headers
assert "Subheader 1.2.1" not in headers
def test_single_level(self, sample_text):
"""Splitting on only h1 yields one chunk that is the full document."""
splitter = MarkdownHeaderSplitter(header_split_levels=[1])
docs = splitter.run(documents=[Document(content=sample_text)])["documents"]
assert len(docs) == 1
assert docs[0].meta["header"] == "Header 1"
# entire document is in one chunk — h1 is first, so content equals the full source text
assert docs[0].content == sample_text
def test_deep_levels_only(self):
"""Splitting on h3 only; h1/h2 headers above the first h3 are not captured in any chunk."""
text = (
"# Top Level\n"
"Ignored top content.\n"
"## Mid Level\n"
"Ignored mid content.\n"
"### Deep Section A\n"
"Content A.\n"
"### Deep Section B\n"
"Content B.\n"
)
splitter = MarkdownHeaderSplitter(header_split_levels=[3])
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
assert docs[0].content == "### Deep Section A\nContent A.\n"
assert docs[1].content == "### Deep Section B\nContent B.\n"
headers = [doc.meta["header"] for doc in docs]
assert "Top Level" not in headers
assert "Mid Level" not in headers
# text before the first h3 match is not absorbed — it is dropped entirely
assert "Ignored top content." not in docs[0].content
assert "Ignored mid content." not in docs[0].content
def test_non_contiguous_levels(self):
"""Non-contiguous level selection (e.g. [1, 3]) splits on h1 and h3 but not h2."""
text = "# H1 Title\n## H2 Ignored\nH2 content.\n### H3 Section\nH3 content.\n"
splitter = MarkdownHeaderSplitter(header_split_levels=[1, 3])
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
# h2 content sits between the h1 and h3 match boundaries, so it is absorbed into the h1 chunk
assert docs[0].content == "# H1 Title\n## H2 Ignored\nH2 content.\n"
assert docs[0].meta["header"] == "H1 Title"
assert docs[1].content == "### H3 Section\nH3 content.\n"
assert docs[1].meta["header"] == "H3 Section"
assert "H2 Ignored" not in [doc.meta["header"] for doc in docs]
def test_validation_empty_list(self):
with pytest.raises(ValueError, match="non-empty list"):
MarkdownHeaderSplitter(header_split_levels=[])
def test_validation_level_zero(self):
with pytest.raises(ValueError, match="invalid values"):
MarkdownHeaderSplitter(header_split_levels=[0, 1, 2])
def test_validation_level_seven(self):
with pytest.raises(ValueError, match="invalid values"):
MarkdownHeaderSplitter(header_split_levels=[1, 7])
def test_validation_non_integer(self):
with pytest.raises(ValueError, match="invalid values"):
MarkdownHeaderSplitter(header_split_levels=[1, "2"]) # type: ignore[list-item]
def test_validation_duplicate_levels(self):
with pytest.raises(ValueError, match="duplicate"):
MarkdownHeaderSplitter(header_split_levels=[1, 2, 2])
class TestCodeBlockExclusion:
"""Tests that hash lines inside fenced code blocks are not treated as headers."""
def test_backtick_fence(self):
"""Hash lines inside triple-backtick fences are ignored."""
text = (
"# Real Header\n"
"Some content.\n"
"```python\n"
"# this is a Python comment, not a header\n"
"## also not a header\n"
"x = 1\n"
"```\n"
"More content.\n"
"## Real Subheader\n"
"Subheader content.\n"
)
splitter = MarkdownHeaderSplitter()
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
assert docs[0].content == (
"# Real Header\n"
"Some content.\n"
"```python\n"
"# this is a Python comment, not a header\n"
"## also not a header\n"
"x = 1\n"
"```\n"
"More content.\n"
)
assert docs[0].meta["header"] == "Real Header"
assert docs[1].content == "## Real Subheader\nSubheader content.\n"
assert docs[1].meta["header"] == "Real Subheader"
assert "this is a Python comment, not a header" not in [doc.meta["header"] for doc in docs]
assert "also not a header" not in [doc.meta["header"] for doc in docs]
def test_tilde_fence(self):
"""Hash lines inside triple-tilde fences are ignored."""
text = "# Real Header\n~~~bash\n# shell comment\necho hello\n~~~\n## Real Subheader\nContent.\n"
splitter = MarkdownHeaderSplitter()
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
assert docs[0].content == "# Real Header\n~~~bash\n# shell comment\necho hello\n~~~\n"
assert docs[0].meta["header"] == "Real Header"
assert docs[1].content == "## Real Subheader\nContent.\n"
assert docs[1].meta["header"] == "Real Subheader"
assert "shell comment" not in [doc.meta["header"] for doc in docs]
def test_multiple_code_blocks(self):
"""Multiple fenced code blocks in one document are all excluded."""
text = (
"# Section One\n"
"Intro text.\n"
"```\n"
"# fake header A\n"
"```\n"
"Middle text.\n"
"```python\n"
"# fake header B\n"
"```\n"
"## Section Two\n"
"More content.\n"
)
splitter = MarkdownHeaderSplitter()
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
assert docs[0].content == (
"# Section One\nIntro text.\n```\n# fake header A\n```\nMiddle text.\n```python\n# fake header B\n```\n"
)
assert docs[0].meta["header"] == "Section One"
assert docs[1].content == "## Section Two\nMore content.\n"
assert docs[1].meta["header"] == "Section Two"
assert "fake header A" not in [doc.meta["header"] for doc in docs]
assert "fake header B" not in [doc.meta["header"] for doc in docs]
def test_longer_fence_delimiters(self):
"""Fences with more than three backticks/tildes are also recognised."""
text = "# Real Header\n````python\n# not a header\n````\n## Real Subheader\nContent.\n"
splitter = MarkdownHeaderSplitter()
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 2
assert docs[0].content == "# Real Header\n````python\n# not a header\n````\n"
assert docs[0].meta["header"] == "Real Header"
assert docs[1].content == "## Real Subheader\nContent.\n"
assert docs[1].meta["header"] == "Real Subheader"
assert "not a header" not in [doc.meta["header"] for doc in docs]
def test_code_block_with_no_real_headers(self):
"""If the only hash lines are inside code blocks, the document is returned unchunked."""
text = "Plain text before code.\n```\n# entirely fake\n```\nPlain text after code.\n"
splitter = MarkdownHeaderSplitter()
docs = splitter.run(documents=[Document(content=text)])["documents"]
assert len(docs) == 1
assert docs[0].content == text
assert "header" not in docs[0].meta
def test_invalid_secondary_split_at_init():
"""Test that an invalid secondary split type raises an error at initialization time."""
with pytest.raises(ValueError, match="split_by must be one of"):
MarkdownHeaderSplitter(secondary_split="invalid_split_type")
def test_invalid_split_parameters_at_init():
"""Test invalid split parameter validation at initialization time."""
# Test split_length validation
with pytest.raises(ValueError, match="split_length must be greater than 0"):
MarkdownHeaderSplitter(secondary_split="word", split_length=0)
# Test split_overlap validation
with pytest.raises(ValueError, match="split_overlap must be greater than or equal to 0"):
MarkdownHeaderSplitter(secondary_split="word", split_overlap=-1)
def test_empty_content_handling():
"""Test handling of documents with empty content."""
splitter_skip = MarkdownHeaderSplitter() # skip empty documents by default
docs = [Document(content="")]
result = splitter_skip.run(documents=docs)
assert len(result["documents"]) == 0
splitter_no_skip = MarkdownHeaderSplitter(skip_empty_documents=False)
docs = [Document(content="")]
result = splitter_no_skip.run(documents=docs)
assert len(result["documents"]) == 1
def test_split_id_sequentiality_primary_and_secondary(sample_text):
# Test primary splitting with single document
splitter = MarkdownHeaderSplitter()
docs = [Document(content=sample_text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Test number of documents
assert len(split_docs) == 5
# Check that split_ids are sequential from 0 for this single parent document
split_ids = [doc.meta["split_id"] for doc in split_docs]
assert split_ids == list(range(len(split_ids)))
# Test secondary splitting with single document
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=3)
docs = [Document(content=sample_text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Test number of documents
assert len(split_docs) == 12
# Check that split_ids are sequential from 0 for this single parent document
split_ids = [doc.meta["split_id"] for doc in split_docs]
assert split_ids == list(range(len(split_ids)))
# Test with multiple input documents; each should have its own split_id sequence
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=3) # Use fresh instance
docs = [Document(content=sample_text), Document(content="# Another Header\nSome more content here.")]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Test number of documents
assert len(split_docs) == 14
# Verify split_ids are per-parent-document
splits_by_source = defaultdict(list)
for doc in split_docs:
splits_by_source[doc.meta["source_id"]].append(doc.meta["split_id"])
# Each parent document should have split_ids starting from 0
for split_ids in splits_by_source.values():
assert split_ids == list(range(len(split_ids)))
def test_secondary_split_with_overlap():
text = (
"# Introduction\n"
"This is the introduction section with some words for testing overlap splitting. "
"It should be split into chunks with overlap.\n"
"## Details\n"
"Here are more details about the topic. "
"Splitting should work across multiple headers and content blocks.\n"
"### Subsection\n"
"This subsection contains additional information and should also be split with overlap."
)
# keep_headers=False
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=8, split_overlap=3, keep_headers=False)
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
assert len(split_docs) == 9
# Verify exact content and metadata of each split
# intro (4 docs)
assert split_docs[0].content == "\nThis is the introduction section with some words "
assert split_docs[0].meta["header"] == "Introduction"
assert split_docs[0].meta["split_id"] == 0
assert split_docs[1].content == "with some words for testing overlap splitting. It "
assert split_docs[1].meta["header"] == "Introduction"
assert split_docs[1].meta["split_id"] == 1
assert split_docs[2].content == "overlap splitting. It should be split into chunks "
assert split_docs[2].meta["header"] == "Introduction"
assert split_docs[2].meta["split_id"] == 2
assert split_docs[3].content == "split into chunks with overlap.\n"
assert split_docs[3].meta["header"] == "Introduction"
assert split_docs[3].meta["split_id"] == 3
# details (3 docs)
assert split_docs[4].content == "\nHere are more details about the topic. Splitting "
assert split_docs[4].meta["header"] == "Details"
assert split_docs[4].meta["split_id"] == 4
assert split_docs[5].content == "the topic. Splitting should work across multiple headers "
assert split_docs[5].meta["header"] == "Details"
assert split_docs[5].meta["split_id"] == 5
assert split_docs[6].content == "across multiple headers and content blocks.\n"
assert split_docs[6].meta["header"] == "Details"
assert split_docs[6].meta["split_id"] == 6
# subsection (2 docs)
assert split_docs[7].content == "\nThis subsection contains additional information and should also "
assert split_docs[7].meta["header"] == "Subsection"
assert split_docs[7].meta["split_id"] == 7
assert split_docs[8].content == "and should also be split with overlap."
assert split_docs[8].meta["header"] == "Subsection"
assert split_docs[8].meta["split_id"] == 8
# verify 3-word overlap behavior (split_overlap=3)
# consecutive pairs within a header should share the 3 words at their boundary
# intro
assert split_docs[0].content.split()[-3:] == split_docs[1].content.split()[:3]
assert split_docs[1].content.split()[-3:] == split_docs[2].content.split()[:3]
assert split_docs[2].content.split()[-3:] == split_docs[3].content.split()[:3]
# details
assert split_docs[4].content.split()[-3:] == split_docs[5].content.split()[:3]
assert split_docs[5].content.split()[-3:] == split_docs[6].content.split()[:3]
# subsection
assert split_docs[7].content.split()[-3:] == split_docs[8].content.split()[:3]
# re-run with keep_headers=True, change split_length and split_overlap
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=4, split_overlap=2)
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
assert len(split_docs) == 24
assert split_docs[0].content.startswith("# Introduction")
assert all("header" in doc.meta for doc in split_docs)
def test_secondary_split_with_threshold():
text = "# Header\n" + " ".join([f"word{i}" for i in range(1, 11)])
# keep_headers=True
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=3, split_threshold=2, keep_headers=True)
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Explicitly test each split
assert len(split_docs) == 4
assert split_docs[0].content == "# Header\nword1 word2 "
assert split_docs[0].meta["split_id"] == 0
assert split_docs[1].content == "word3 word4 word5 "
assert split_docs[1].meta["split_id"] == 1
assert split_docs[2].content == "word6 word7 word8 "
assert split_docs[2].meta["split_id"] == 2
assert split_docs[3].content == "word9 word10"
assert split_docs[3].meta["split_id"] == 3
# keep_headers=False
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=3, split_threshold=2, keep_headers=False)
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
# Explicitly test each split
assert len(split_docs) == 3
assert split_docs[0].content == "\nword1 word2 word3 "
assert split_docs[0].meta["split_id"] == 0
assert split_docs[1].content == "word4 word5 word6 "
assert split_docs[1].meta["split_id"] == 1
assert split_docs[2].content == "word7 word8 word9 word10" # 4 words (due to threshold, not possible to split 3-1)
assert split_docs[2].meta["split_id"] == 2
def test_page_break_handling_in_secondary_split():
text = "# Header\nFirst page\f Second page\f Third page"
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=1)
docs = [Document(content=text)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
expected_page_numbers = [1, 1, 1, 2, 2, 3, 3]
actual_page_numbers = [doc.meta.get("page_number") for doc in split_docs]
assert actual_page_numbers == expected_page_numbers
def test_page_break_handling_with_multiple_headers(sample_text_with_page_breaks):
splitter = MarkdownHeaderSplitter(secondary_split="word", split_length=3)
docs = [Document(content=sample_text_with_page_breaks)]
result = splitter.run(documents=docs)
split_docs = result["documents"]
assert len(split_docs) == 12
assert split_docs[0].content == "# Header 1\nContent "
assert split_docs[0].meta == {
"source_id": ANY,
"split_id": 0,
"page_number": 1,
"split_idx_start": 0,
"header": "Header 1",
"parent_headers": [],
}
assert split_docs[1].content == "under header 1.\n\f\n"
assert split_docs[1].meta == {
"source_id": ANY,
"split_id": 1,
"page_number": 1,
"split_idx_start": 19,
"header": "Header 1",
"parent_headers": [],
}
assert split_docs[2].content == "## Header 1.1\n### "
assert split_docs[2].meta == {
"source_id": ANY,
"split_id": 2,
"page_number": 2,
"split_idx_start": 0,
"header": "Subheader 1.1.1",
"parent_headers": ["Header 1", "Header 1.1"],
}
assert split_docs[3].content == "Subheader 1.1.1\nContent under "
assert split_docs[3].meta == {
"source_id": ANY,
"split_id": 3,
"page_number": 2,
"split_idx_start": 18,
"header": "Subheader 1.1.1",
"parent_headers": ["Header 1", "Header 1.1"],
}
assert split_docs[4].content == "sub-header 1.1.1\n\f\n"
assert split_docs[4].meta == {
"source_id": ANY,
"split_id": 4,
"page_number": 2,
"split_idx_start": 48,
"header": "Subheader 1.1.1",
"parent_headers": ["Header 1", "Header 1.1"],
}
assert split_docs[5].content == "## Header 1.2\n### "
assert split_docs[5].meta == {
"source_id": ANY,
"split_id": 5,
"page_number": 3,
"split_idx_start": 0,
"header": "Subheader 1.2.1",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[6].content == "Subheader 1.2.1\nContent under "
assert split_docs[6].meta == {
"source_id": ANY,
"split_id": 6,
"page_number": 3,
"split_idx_start": 18,
"header": "Subheader 1.2.1",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[7].content == "header 1.2.1.\n\f\n"
assert split_docs[7].meta == {
"source_id": ANY,
"split_id": 7,
"page_number": 3,
"split_idx_start": 48,
"header": "Subheader 1.2.1",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[8].content == "### Subheader 1.2.2\nContent "
assert split_docs[8].meta == {
"source_id": ANY,
"split_id": 8,
"page_number": 4,
"split_idx_start": 0,
"header": "Subheader 1.2.2",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[9].content == "under header 1.2.2.\n\f\n"
assert split_docs[9].meta == {
"source_id": ANY,
"split_id": 9,
"page_number": 4,
"split_idx_start": 28,
"header": "Subheader 1.2.2",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[10].content == "### Subheader 1.2.3\nContent "
assert split_docs[10].meta == {
"source_id": ANY,
"split_id": 10,
"page_number": 5,
"split_idx_start": 0,
"header": "Subheader 1.2.3",
"parent_headers": ["Header 1", "Header 1.2"],
}
assert split_docs[11].content == "under header 1.2.3."
assert split_docs[11].meta == {
"source_id": ANY,
"split_id": 11,
"page_number": 5,
"split_idx_start": 28,
"header": "Subheader 1.2.3",
"parent_headers": ["Header 1", "Header 1.2"],
}
# reconstruct original
reconstructed_text = "".join(doc.content for doc in split_docs)
assert reconstructed_text == sample_text_with_page_breaks