Skip to content

fix(chunker): keep a table caption in the same window as its table - #854

Open
CyberRik wants to merge 1 commit into
docling-project:mainfrom
CyberRik:fix/chunker-caption-empty-chunk
Open

CyberRik wants to merge 1 commit into
docling-project:mainfrom
CyberRik:fix/chunker-caption-empty-chunk

Conversation

@CyberRik

@CyberRik CyberRik commented Oct 9, 2026 •

Copy link
Copy Markdown

Fixes #806.

A caption serializes to no text of its own in a chunk, because its text is emitted with its table. _split_by_doc_items still treated it as a separate item, so:

  • when it formed a window by itself, the result was a chunk with text='' (with merge_peers=False), or, once merged with the next chunk, a stray leading \n;
  • when it fit into the window before the table (cost 0 tokens), it was listed in the doc_items of the preceding paragraph's chunk instead of the table's, and that chunk got a trailing \n.

Change. Windows are now built from units: a run of items without text of their own, plus the next item with text. Items without text at the end of a chunk join the last unit. Headings are bound the same way, since their text goes to the metadata. The window-growing logic itself is unchanged; it just walks units instead of items.

Because a table now arrives together with its caption, the repeat_table_header path checks for a single table plus its own captions (_is_single_table) instead of exactly one item. That path already handles the caption text as the preamble.

Ground truth. tests/data/chunker/0d_out_chunks.json changes in exactly this way on the real test document: #/texts/72, the caption of #/tables/0, moves from the doc_items of chunk 18 (the paragraph before the table) to chunks 19 and 20 (the table). Chunk 18 also loses a trailing \n that came from joining the caption's empty text; the text of every other chunk is unchanged.

Tests. test_table_caption_stays_with_its_table, parametrized over a preceding paragraph and merge_peers, checks that no chunk is empty, no chunk starts with a delimiter before the caption, the caption is never listed without its table, and the chunk with the caption text lists the caption. All four cases fail on main and pass with this change. The chunker test files pass (74 tests); ruff and mypy are clean on the changed files.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @CyberRik, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit —

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@codecov

codecov Bot commented Oct 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling_core/transforms/chunker/hybrid_chunker.py 96.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ceberam ceberam added bug Something isn't working chunking Document chunking labels Oct 14, 2026

@chrikrah chrikrah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyberRik the fix is right at 820ee59, but the branch now conflicts with #690, which ceberam merged on 10-19. Your head drops the empty chunk from #806 on my reproduction. Main b359e1c still has it, so the fix is still needed. You build units in one pass and serialize each item once, so #808's linear time holds. Its byte-identical promise goes, on purpose.

blocking: only hybrid_chunker.py conflicts. But #690 now writes the caption into the table header itself, as | Table 1: Revenue by region | in the first table chunk on main. After the rebase, could you check that the caption text appears once and not also as its own unit?

non-blocking: when the table splits, every part lists the caption in doc_items, though only part 1 carries its text. The table is on all parts already, so I'd keep that as it is.

$ python probe.py <tree>   # table of 21 rows split at max_tokens=64, paragraph before and after, merge_peers=False
# base f61fbf5: 6 chunks. chunk 1 has doc_items [caption] and text ''. The caption text starts chunk 2.
# head 820ee59: 5 chunks, none empty. Parts 1, 2 and 3 list [caption, table]. Same with the caption after the table.
# main b359e1c: 7 chunks. chunk 1 has doc_items [caption] and text ''. chunk 2 opens '| - |\n| Table 1: Revenue by region |', chunk 5 is '| - |\n |\n'.

$ git merge-tree --write-tree origin/main 820ee594
CONFLICT (content): Merge conflict in docling_core/transforms/chunker/hybrid_chunker.py

$ python -m pytest tests/test_hybrid_chunker.py tests/test_hierarchical_chunker.py -q   # head 820ee59, CPython 3.12
39 passed
# same command, hybrid_chunker.py from base f61fbf5, tests and 0d_out_chunks.json kept
5 failed, 34 passed   # the 4 new test_table_caption_stays_with_its_table cases and test_chunk_with_repeat_table_header
$ ruff format --check && ruff check   # ruff 0.16.5
2 files already formatted
All checks passed!

# not run: mypy, tests/test_code_chunker.py (needs a git clone of external fixtures)

@ceberam, mergify wants two approvals here because 0d_out_chunks.json changes. The squash title becomes the release note. Should it say that caption membership in doc_items moved, for people who index by them?

@CyberRik
CyberRik force-pushed the fix/chunker-caption-empty-chunk branch from 820ee59 to a98c2d7 Compare October 22, 2026 08:15
@CyberRik

Copy link
Copy Markdown
Author

Thanks for the thorough review! Rebased onto main. The conflict was only in hybrid_chunker.py and was easy to sort out.

I ran your setup again after the rebase (21 rows, max_tokens=64, a paragraph on each side, merge_peers=False) and got 6 chunks, none of them empty. That's true with the caption either before or after the table. The caption text only shows up once, in the first table part, and it's never its own unit. I added that check to test_table_caption_stays_with_its_table so it stays covered.

Agreed on keeping the caption in doc_items for every part.

One thing I noticed while testing: the table parts from #690's column split still look a bit off. The first starts with | - |\n| Table 1: … |, and the last is just | - |\n |\n. It's the same on main, so it isn't from this PR. Happy to open a separate issue for it if that helps.

@chrikrah chrikrah left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CyberRik resolved: the caption appears once and the empty chunk is gone. [False-True] passes on main, so three cases fail there, not four, plus the intended repeat-header update. #851 now conflicts in tests/test_hybrid_chunker.py; once rebased, from my perspective this can be merged.

$ python -m pytest tests/test_hybrid_chunker.py tests/test_hierarchical_chunker.py -q   # head a98c2d7, CPython 3.12.6, docling_core imported from the exported tree
61 passed, 35 warnings in 10.52s
# hybrid_chunker.py from main b501ec8, tests kept
FAILED tests/test_hybrid_chunker.py::test_chunk_with_repeat_table_header
FAILED tests/test_hybrid_chunker.py::test_table_caption_stays_with_its_table[True-True]
FAILED tests/test_hybrid_chunker.py::test_table_caption_stays_with_its_table[True-False]
FAILED tests/test_hybrid_chunker.py::test_table_caption_stays_with_its_table[False-False]
4 failed, 57 passed, 35 warnings in 10.86s
$ git merge-tree --write-tree origin/main a98c2d7 | grep CONFLICT   # origin/main 5582916
CONFLICT (content): Merge conflict in tests/test_hybrid_chunker.py
$ ruff check && ruff format --check   # ruff 0.13.1, the two changed .py files
All checks passed!
2 files already formatted
# not run: mypy, tests/test_code_chunker.py

Yes, file the #690 | - | rows separately; main has them too.

@ceberam

ceberam commented Oct 28, 2026

Copy link
Copy Markdown
Member

@CyberRik could you please resolve the existing conflict?

A caption serializes to no text of its own, as its text is emitted with its table. _split_by_doc_items could still put it in a window by itself, which made an empty chunk (or a stray delimiter once merged), or in the window before the table, which listed it in the wrong chunk. Windows are now built from units that bind an item without text to the next item with text, and the repeat-header table path accepts a table together with its captions.

Signed-off-by: Ritankar Mondal <[email protected]>
@CyberRik
CyberRik force-pushed the fix/chunker-caption-empty-chunk branch from a98c2d7 to 2b0b902 Compare October 28, 2026 09:58
@CyberRik

Copy link
Copy Markdown
Author

@ceberam rebased onto main (5582916) and the conflict is resolved. It was only in tests/test_hybrid_chunker.py: #851 and this PR each added a new test in the same place, so I kept both. No changes to the chunker code itself.

$ python -m pytest tests/test_hybrid_chunker.py tests/test_hierarchical_chunker.py -q   # head 2b0b902
73 passed
# hybrid_chunker.py from main 5582916, tests kept
4 failed, 69 passed   # test_chunk_with_repeat_table_header + test_table_caption_stays_with_its_table[True-True|True-False|False-False]
$ ruff format --check && ruff check   # the two changed .py files
2 files already formatted
All checks passed!

@chrikrah thanks for the approval. I'll file the | - | rows from #690 as a separate issue.

@CyberRik

CyberRik commented Nov 9, 2026 •

Copy link
Copy Markdown
Author

@ceberam this one is approved by @chrikrah and all checks are green, the only thing holding it back is the "two reviewers for test updates" protection (currently at 1 of 2 approvals). The conflict you flagged earlier is resolved. Would you be able to give it a second review when you have a moment? Thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:ci-safe AI triage found no CI-safety concern (advisory) bug Something isn't working chunking Document chunking tables Table model, OTSL, rich tables, TableData, table serialization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HybridChunker: empty chunk / stray newline when a captioned table exceeds max_tokens

3 participants