From d3c8ed62a833e7179777d3ff7a7ddf0b8d8d5972 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Wed, 15 Apr 2026 15:10:24 +0300 Subject: [PATCH] preserve original chunk when expansion produces less content --- haiku_rag_slim/haiku/rag/context.py | 6 +- .../haiku/rag/store/models/document_item.py | 2 + tests/test_context.py | 62 +++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/context.py b/haiku_rag_slim/haiku/rag/context.py index 77836f28..a66c8b84 100644 --- a/haiku_rag_slim/haiku/rag/context.py +++ b/haiku_rag_slim/haiku/rag/context.py @@ -230,9 +230,11 @@ async def expand_with_items( first = original_results[0] - # If noise filtering removed all content, preserve the original + # Expansion should never return less content than the original chunk. + # This can happen when item texts are fragmented (e.g., docling splits + # formatted HTML list items into many small text nodes). expanded_content = "\n\n".join(content_parts) - if not expanded_content: + if len(expanded_content) < len(first.content): expanded_content = first.content final_results.append( diff --git a/haiku_rag_slim/haiku/rag/store/models/document_item.py b/haiku_rag_slim/haiku/rag/store/models/document_item.py index 2e93883d..ee17bf0c 100644 --- a/haiku_rag_slim/haiku/rag/store/models/document_item.py +++ b/haiku_rag_slim/haiku/rag/store/models/document_item.py @@ -56,6 +56,8 @@ def extract_items( Runs iterate_items() and extracts the fields needed for context expansion: self_ref, label, pre-rendered text, and page numbers from provenance. + Items are stored as docling produces them — container items (e.g., list_item) + may have empty text with content in their children. """ items: list[DocumentItem] = [] diff --git a/tests/test_context.py b/tests/test_context.py index 6635bb7a..94e062a0 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -320,3 +320,65 @@ class TestExpandWithItems: # with skip_noise crosses into the Introduction section which has # real content — so we get expanded content, not the fallback. assert len(expanded[0].content) > 0 + + async def test_fragmented_items_preserve_chunk(self, temp_db_path): + """When items are fragmented (e.g., list_item children), the original + chunk content is preserved if expansion produces less text.""" + from haiku.rag.client import HaikuRAG + + async with HaikuRAG(temp_db_path, create=True) as rag: + # Simulate docling's list_item structure: container with empty text, + # children with tiny fragments + items = [ + DocumentItem( + document_id="doc-1", + position=0, + self_ref="#/texts/0", + label="section_header", + text="Steps", + ), + DocumentItem( + document_id="doc-1", + position=1, + self_ref="#/texts/1", + label="list_item", + text="", + ), + DocumentItem( + document_id="doc-1", + position=2, + self_ref="#/texts/2", + label="text", + text="Click", + ), + DocumentItem( + document_id="doc-1", + position=3, + self_ref="#/texts/3", + label="text", + text="+", + ), + DocumentItem( + document_id="doc-1", + position=4, + self_ref="#/texts/4", + label="text", + text="Add a New Service", + ), + ] + await rag.document_item_repository.create_items("doc-1", items) + + # The chunk had properly assembled content from the chunker + result = SearchResult( + content="1. Click + Add a New Service in the dashboard.", + score=0.9, + document_id="doc-1", + doc_item_refs=["#/texts/1", "#/texts/2", "#/texts/3", "#/texts/4"], + ) + expanded = await expand_with_items( + rag.document_item_repository, "doc-1", [result], 10, 5000 + ) + assert len(expanded) == 1 + # Expansion produces "Steps\n\nClick\n\n+\n\nAdd a New Service" = 38 chars + # which is less than the chunk's 46 chars — fallback preserves the chunk + assert expanded[0].content == result.content