Merge pull request #458 from mcdonc/thread-chunker-chunk

Thread chunker.chunk() off the asyncio event loop
This commit is contained in:
Yiorgis Gozadinos 2026-06-23 08:50:13 +03:00 committed by GitHub
commit fbb63f7e99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 63 additions and 18 deletions

View file

@ -15,7 +15,7 @@ class DocumentChunker(ABC):
""" """
@abstractmethod @abstractmethod
async def chunk(self, document: "DoclingDocument") -> list["Chunk"]: async def chunk(self, document: "DoclingDocument | None") -> list["Chunk"]:
"""Split a document into chunks with metadata. """Split a document into chunks with metadata.
Args: Args:

View file

@ -1,3 +1,4 @@
import asyncio
from functools import cache from functools import cache
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
@ -105,24 +106,12 @@ class DoclingLocalChunker(DocumentChunker):
"Must be 'hybrid' or 'hierarchical'." "Must be 'hybrid' or 'hierarchical'."
) )
async def chunk(self, document: "DoclingDocument") -> list[Chunk]: def _chunk_sync(self, document: "DoclingDocument") -> list[Chunk]:
"""Split the document into chunks with metadata. """Synchronous chunking helper (CPU-bound, no I/O).
Extracts structured metadata from each DocChunk including: Runs the underlying HybridChunker/HierarchicalChunker and extracts
- doc_item_refs: JSON pointer references to DocItems (e.g., "#/texts/5") structured metadata from each DocChunk.
- headings: Section heading hierarchy
- labels: Semantic labels for each doc_item (e.g., "paragraph", "table")
- page_numbers: Page numbers where content appears
Args:
document: The DoclingDocument to be split into chunks.
Returns:
List of Chunk containing content and structured metadata.
""" """
if document is None:
return []
raw_chunks = list(self.chunker.chunk(document)) raw_chunks = list(self.chunker.chunk(document))
result: list[Chunk] = [] result: list[Chunk] = []
@ -172,3 +161,23 @@ class DoclingLocalChunker(DocumentChunker):
) )
return result return result
async def chunk(self, document: "DoclingDocument | None") -> list[Chunk]:
"""Split the document into chunks with metadata.
Extracts structured metadata from each DocChunk including:
- doc_item_refs: JSON pointer references to DocItems (e.g., "#/texts/5")
- headings: Section heading hierarchy
- labels: Semantic labels for each doc_item (e.g., "paragraph", "table")
- page_numbers: Page numbers where content appears
Args:
document: The DoclingDocument to be split into chunks.
Returns:
List of Chunk containing content and structured metadata.
"""
if document is None:
return []
return await asyncio.to_thread(self._chunk_sync, document)

View file

@ -129,7 +129,7 @@ class DoclingServeChunker(DocumentChunker):
return result.get("chunks", []) return result.get("chunks", [])
async def chunk(self, document: "DoclingDocument") -> list[Chunk]: async def chunk(self, document: "DoclingDocument | None") -> list[Chunk]:
"""Split the document into chunks with metadata via docling-serve. """Split the document into chunks with metadata via docling-serve.
Extracts structured metadata from the API response including: Extracts structured metadata from the API response including:

View file

@ -55,6 +55,42 @@ async def test_local_chunker(qa_corpus: list[dict[str, str]]):
assert abs(total_tokens - original_tokens) <= original_tokens * 0.1 assert abs(total_tokens - original_tokens) <= original_tokens * 0.1
@pytest.mark.asyncio
async def test_local_chunker_none_document():
"""Test DoclingLocalChunker returns empty list for None document."""
chunker = DoclingLocalChunker()
assert await chunker.chunk(None) == []
@pytest.mark.asyncio
async def test_local_chunker_runs_off_event_loop_thread():
"""Chunking is CPU-bound; verify it runs in a worker thread."""
import threading
from unittest.mock import patch
chunker = DoclingLocalChunker()
event_loop_thread = threading.current_thread()
called_from: list[threading.Thread] = []
original = chunker._chunk_sync
def recording_chunk_sync(self, document):
called_from.append(threading.current_thread())
return original(document)
converter = get_converter(Config)
doc = await converter.convert_text("# Hello\n\nWorld", name="test.md")
with patch.object(DoclingLocalChunker, "_chunk_sync", recording_chunk_sync):
await chunker.chunk(doc)
assert called_from, "_chunk_sync was never called"
assert called_from[0] is not event_loop_thread, (
"_chunk_sync ran on the event-loop thread; "
"it must be dispatched via asyncio.to_thread"
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_local_chunker_custom_config(): async def test_local_chunker_custom_config():
"""Test DoclingLocalChunker with custom configuration.""" """Test DoclingLocalChunker with custom configuration."""