Coverage for src/lilbee/retrieval/clustering_embedding/types.py: 100%
19 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Type definitions for the chunk-level mutual-kNN clusterer."""
3from __future__ import annotations
5from dataclasses import dataclass, field
7# Minimum token length for TF-IDF labeling. Shorter tokens are mostly
8# articles, prepositions, and single letters: noise that inflates term
9# counts without adding topic signal. Three characters keeps useful
10# acronyms (api, xml, sql).
11_MIN_TF_TOKEN_LEN = 3
14def _tokenize_for_tf(text: str) -> list[str]:
15 """Lowercase alphanumeric tokens for TF-IDF scoring.
17 Deliberately has NO stopword list: common words like "the" or "and"
18 get an IDF near zero (they appear in almost every chunk) so TF-IDF
19 filters them automatically. A hand-curated English stoplist would
20 add maintenance burden and break on non-English corpora for no
21 additional quality.
23 Lives beside :class:`ClusterChunk` because the record derives its
24 tokens from it; importing it from the helpers module would be circular.
25 """
26 result: list[str] = []
27 for raw in text.lower().split():
28 word = "".join(ch for ch in raw if ch.isalnum())
29 if len(word) >= _MIN_TF_TOKEN_LEN:
30 result.append(word)
31 return result
34@dataclass(slots=True)
35class ClusterChunk:
36 """Lightweight view of one chunk row used by the clusterer.
38 Named for the clusterer rather than ``ChunkRecord`` so it cannot be
39 confused with :class:`lilbee.data.types.ChunkRecord`, an
40 unrelated TypedDict with a different field set used across ingest.
42 ``tokens`` is a derived cache of ``_tokenize_for_tf(text)``. It is
43 computed here rather than at each construction site: TF-IDF labeling
44 silently degrades to fallback labels when a record carries no tokens,
45 so the invariant is structural instead of caller discipline. ``slots``
46 keeps the per-record footprint down, since one record is materialized
47 per corpus chunk.
48 """
50 source: str
51 chunk_index: int
52 text: str
53 tokens: list[str] = field(default_factory=list)
55 def __post_init__(self) -> None:
56 if not self.tokens:
57 self.tokens = _tokenize_for_tf(self.text)