Coverage for src/qdrant_loader/core/chunking/docling/chunker.py: 96%
24 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""The chunker seam: the abstraction the rest of the codebase depends on.
3A :class:`DocumentChunker` turns a converted document into engine-neutral
4:class:`~.outcome.Chunk` objects. The codebase depends on this Protocol, not on
5docling's ``HybridChunker`` — the same swappability discipline the conversion layer
6uses in its ``engine.py``. :func:`build_chunker` is the composition root that
7selects and constructs the implementation, importing docling lazily.
9This consumes the :class:`~..conversion.ConvertedDocument` the conversion layer
10produces — the structured ``DoclingDocument``, never a re-parsed markdown string —
11which is what makes structure-aware chunking possible.
12"""
14from __future__ import annotations
16import threading
17from enum import StrEnum
18from typing import TYPE_CHECKING, Protocol, runtime_checkable
20if TYPE_CHECKING:
21 from ...conversion import ConvertedDocument
22 from .config import ChunkingConfig
23 from .outcome import Chunk
26@runtime_checkable
27class DocumentChunker(Protocol):
28 """ConvertedDocument -> list[Chunk]. The codebase depends on this, not docling."""
30 def chunk(self, document: ConvertedDocument) -> list[Chunk]: ...
33class ChunkerKind(StrEnum):
34 DOCLING = "docling"
37# One chunker (and thus one HybridChunker + tokenizer) per distinct config for the
38# whole process, mirroring entity_extraction's _EXTRACTOR_REGISTRY: ChunkingService
39# builds a fresh DoclingChunkingStrategy per document, and without this the docling
40# HybridChunker and its tokenizer (HuggingFaceTokenizer.from_pretrained / tiktoken)
41# would be rebuilt for every document. ChunkingConfig is a frozen, hashable
42# dataclass, so equal configs key the same cached instance.
43_CHUNKER_REGISTRY: dict[tuple[ChunkerKind, ChunkingConfig], DocumentChunker] = {}
44_REGISTRY_LOCK = threading.Lock()
47def build_chunker(kind: ChunkerKind, config: ChunkingConfig) -> DocumentChunker:
48 """Composition root: select and construct the chunker.
50 Memoized per ``(kind, config)`` so the same config shares one chunker instance
51 process-wide — the per-document strategy construction in ChunkingService thus
52 reuses one already-built (and pre-warmed) HybridChunker + tokenizer instead of
53 rebuilding it every document. docling is imported lazily so importing this
54 package never drags in the chunker stack, and so the import also breaks the
55 chunker <-> docling_chunker cycle.
56 """
57 key = (kind, config)
58 with _REGISTRY_LOCK:
59 chunker = _CHUNKER_REGISTRY.get(key)
60 if chunker is None:
61 chunker = _build_chunker(kind, config)
62 _CHUNKER_REGISTRY[key] = chunker
63 return chunker
66def _build_chunker(kind: ChunkerKind, config: ChunkingConfig) -> DocumentChunker:
67 match kind:
68 case ChunkerKind.DOCLING:
69 from .docling_chunker import DoclingChunker
71 return DoclingChunker(config)
72 raise ValueError(f"unknown chunker kind: {kind!r}")