Coverage for src/qdrant_loader/core/chunking/docling/tokenizer.py: 66%
32 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"""Builds the docling tokenizer that bounds chunk size.
3:class:`TokenizerFactory` is the single place that turns our engine-neutral
4:class:`~.config.TokenizerConfig` into a docling ``BaseTokenizer`` — the chunking
5analogue of the conversion option builder. It is a small pure class holding the
6injected config and imports docling lazily, so importing this package costs nothing
7until a chunk is actually sized.
9Aligning the chunk budget to the *embedding* model's tokenizer is the whole point of
10this layer; a mismatched tokenizer is the character-budget mistake it exists to fix.
11"""
13from __future__ import annotations
15from typing import TYPE_CHECKING
17from qdrant_loader.utils.logging import LoggingConfig
19from .config import TokenizerConfig, TokenizerKind
20from .exceptions import TokenizerUnavailableError
22if TYPE_CHECKING:
23 from docling_core.transforms.chunker.tokenizer.base import BaseTokenizer
25logger = LoggingConfig.get_logger(__name__)
28class TokenizerFactory:
29 """Config -> docling ``BaseTokenizer``. The only place tokenizers are built."""
31 def __init__(self, config: TokenizerConfig) -> None:
32 self._config = config
34 def build(self) -> BaseTokenizer:
35 """Construct the tokenizer for the configured kind.
37 Raises :class:`TokenizerUnavailableError` (loud, not a silent char-count
38 fallback) when the backing library or model cannot be loaded. When the config
39 is an approximate counting proxy (``embedding.tokenizer="none"``), warns once
40 here so the choice is observable rather than silent.
41 """
42 if self._config.approximate:
43 logger.warning(
44 "Chunk token counts are approximate: the embedding model exposes no "
45 "tokenizer, so this proxy is used only for counting. Set a chunking "
46 "tokenizer matching your embedding model for exact token budgeting.",
47 proxy_tokenizer=self._config.model,
48 )
49 match self._config.kind:
50 case TokenizerKind.OPENAI:
51 return self._build_openai()
52 case TokenizerKind.HUGGINGFACE:
53 return self._build_huggingface()
54 raise TokenizerUnavailableError(
55 str(self._config.kind), "unknown tokenizer kind"
56 )
58 def _build_openai(self) -> BaseTokenizer:
59 try:
60 import tiktoken
61 from docling_core.transforms.chunker.tokenizer.openai import (
62 OpenAITokenizer,
63 )
65 encoding = tiktoken.get_encoding(self._config.model)
66 except Exception as error: # missing dep / unknown encoding / no cached BPE
67 raise TokenizerUnavailableError("openai", str(error)) from error
68 return OpenAITokenizer(tokenizer=encoding, max_tokens=self._config.max_tokens)
70 def _build_huggingface(self) -> BaseTokenizer:
71 try:
72 from docling_core.transforms.chunker.tokenizer.huggingface import (
73 HuggingFaceTokenizer,
74 )
76 return HuggingFaceTokenizer.from_pretrained(
77 model_name=self._config.model,
78 max_tokens=self._config.max_tokens,
79 )
80 except Exception as error: # missing dep / model not present / offline hub
81 raise TokenizerUnavailableError("huggingface", str(error)) from error