Chunking¶
The strategy registry and its five members: recursive_character (the v1
default) plus the v2 additions token_based, markdown_aware,
docling_hybrid, and semantic. Mind that CHUNK_SIZE's unit is
per-strategy (characters vs tokens — see Configuration).
varagity.chunking.base
¶
Chunking-strategy protocol and registry (the spec §5.1 sketch, typed).
Each strategy module defines one implementation decorated with
@register("name"); callers resolve the configured strategy with
get_chunker(settings.CHUNKING_STRATEGY).
ChunkingStrategy
¶
Bases: Protocol
Interface every chunking strategy implements.
Source code in varagity/chunking/base.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's text into chunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text. |
required |
source_meta
|
dict[str, Any]
|
Provenance seeded into every chunk's metadata
( |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks, each with metadata seeded plus its |
Source code in varagity/chunking/base.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | |
get_chunker(name)
¶
Look up a registered chunking strategy by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registry key (e.g. |
required |
Returns:
| Type | Description |
|---|---|
ChunkingStrategy
|
The registered strategy instance. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If no strategy is registered under |
Source code in varagity/chunking/base.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
register(name)
¶
Class decorator registering a chunking strategy instance under name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registry key (the |
required |
Returns:
| Type | Description |
|---|---|
Callable[[T], T]
|
The decorator, which instantiates and registers the class unchanged. |
Source code in varagity/chunking/base.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
warn_near_token_ceiling(chunks, *, strategy)
¶
Warn for chunks whose content nears e5's 512-token ceiling (research Q6).
The ingest-time warning :class:~varagity.models.embeddings.EmbeddingsClient
fires per contextualized passage, carried forward to chunking time:
a chunk already ≥ the threshold before its situating
blurb is prepended is guaranteed to truncate at embedding time, and the
strategy's sizing knobs — not the blurb length — are what to fix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
chunks
|
Sequence[Document]
|
The chunks produced for one document. |
required |
strategy
|
str
|
Registry name of the producing strategy (names the knob in the log line). |
required |
Source code in varagity/chunking/base.py
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
varagity.chunking.recursive_character
¶
Default v1 chunking strategy: recursive character splitting (spec §9.3).
Wraps langchain's RecursiveCharacterTextSplitter with the configured
CHUNK_SIZE / CHUNK_OVERLAP. Both parameters count characters, not
tokens (the splitter's length_function defaults to len); a
token-based strategy would be a new registry file, not an edit here.
RecursiveCharacterStrategy
¶
Splits on a separator hierarchy (paragraph → line → word → char).
Source code in varagity/chunking/recursive_character.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's text into overlapping character chunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text. |
required |
source_meta
|
dict[str, Any]
|
Provenance copied into every chunk's metadata (the splitter deep-copies per chunk, so later per-chunk fields don't bleed across siblings). |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks in document order, each carrying |
list[Document]
|
its |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/chunking/recursive_character.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
varagity.chunking.token_based
¶
Token-denominated recursive splitting (spec_v2 §7, v2 plan decision #12).
Wraps langchain's RecursiveCharacterTextSplitter.from_tiktoken_encoder:
the same separator hierarchy as recursive_character (paragraph → line →
word → char), but CHUNK_SIZE / CHUNK_OVERLAP count tokens, not
characters — aligning chunk budgets to the e5 embedder's 512-token ceiling
instead of the character approximation the v1 strategy documents.
Token counting uses tiktoken's cl100k_base — the codebase-wide
documented approximation of e5's actual XLM-RoBERTa vocabulary (see
varagity.tokens). An exact counter (the e5 HF tokenizer as a custom
length_function) plugs into the __init__ seam without touching the
registry; it was evaluated against the approximation in the chunker sweep
rather than shipped (an extra transformers hot path + first-run
tokenizer download for a small counting delta — ADR-008).
Note the langchain merge nuance: the splitter sizes chunks by summing piece lengths, and re-tokenizing joined text can differ by a token or two at the seams, so real-tokenizer chunks may exceed the budget by a whisker. The ≥480 warning (not a hard 512 assert) is the guard that matters.
TokenBasedStrategy
¶
Recursive separator splitting with token-denominated sizes.
Source code in varagity/chunking/token_based.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
__init__(*, length_function=None)
¶
Create the strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length_function
|
Callable[[str], int] | None
|
Alternative token counter (e.g. an exact e5 HF
tokenizer |
None
|
Source code in varagity/chunking/token_based.py
41 42 43 44 45 46 47 48 49 50 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's text into overlapping token-budgeted chunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text. |
required |
source_meta
|
dict[str, Any]
|
Provenance copied into every chunk's metadata (the splitter deep-copies per chunk). |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks in document order, each carrying |
list[Document]
|
its |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/chunking/token_based.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | |
varagity.chunking.markdown_aware
¶
Heading-aware markdown splitting (spec_v2 §7).
Every rich-format parser (PDF, office, web) exports structure-aware
markdown through the shared Docling core — this strategy finally exploits
that structure: split at ATX headings (# … ######), keep each
section's text together, and carry the section's heading breadcrumb into
chunk metadata as heading_path (e.g. "Harbor Operations > Dredging >
Channel depth"), where the JSONB metadata column absorbs it with no schema
migration and the evidence panel can surface it as breadcrumb context.
This is the from-scratch header splitter the v2 plan benchmarks against
Docling's HybridChunker (shipped alongside as docling_hybrid; the
chunker sweep decides the default — ADR-008). Behavior notes:
- Headings inside fenced code blocks (three-plus backticks or tildes) are content, not structure.
- A section's own heading line stays in its chunk text (retrieval-visible);
ancestors live only in
heading_path. - Sections are not merged across headings — coherence over packing;
HybridChunkermakes the opposite trade, which is exactly what the sweep measures. - A section longer than
CHUNK_SIZE(characters, the v1 unit — this strategy sizes likerecursive_character) is re-split recursively; every sub-chunk inherits the section'sheading_path. - Text before the first heading forms a section with no
heading_path.
MarkdownAwareStrategy
¶
Splits on ATX headings, carrying the heading breadcrumb as metadata.
Source code in varagity/chunking/markdown_aware.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's markdown into heading-coherent chunks.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text (markdown from the rich-format parsers; plain text degrades to one section, then the recursive re-split). |
required |
source_meta
|
dict[str, Any]
|
Provenance copied into every chunk's metadata. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks in document order, each carrying |
list[Document]
|
its |
list[Document]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/chunking/markdown_aware.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
varagity.chunking.docling_hybrid
¶
Docling HybridChunker as a registered strategy (spec_v2 §7).
The alternative markdown_aware implementation the v2 plan benchmarks:
Docling's tokenization- and structure-aware chunker, shipped as its own
registry entry so the chunker eval sweep compares the two head-to-head and
the default is benchmark-decided (ADR-008), not hand-picked.
The strategy re-parses the parser's markdown into a DoclingDocument
(in-memory DocumentStream, markdown backend — declarative, no layout
models) and runs HybridChunker over the document object model: one chunk
per document item, oversized items token-split, undersized peers under the
same headings merged (merge_peers — the packing trade markdown_aware
declines). Heading provenance lands in heading_path exactly like
markdown_aware. Sizing: CHUNK_SIZE is the tokenizer's max_tokens
budget (tokens, tiktoken cl100k_base — the codebase-wide documented
approximation); CHUNK_OVERLAP has no HybridChunker equivalent and is
ignored.
Chunk text is the item text (chunk.text), not the chunker's
heading-prefixed contextualize() rendering: Contextual Retrieval already
prepends an LLM situating blurb at ingest (spec §9.4), and stacking two
context prefixes would double-count headings and eat the 512-token budget.
Docling imports are deferred to call time (the parser-module convention): importing this module on CLI start must not pay for Docling's machinery.
DoclingHybridStrategy
¶
Docling HybridChunker over the re-parsed document structure.
Source code in varagity/chunking/docling_hybrid.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
__init__()
¶
Initialize the converter/chunker caches (nothing heavy here).
Source code in varagity/chunking/docling_hybrid.py
52 53 54 55 56 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's markdown with Docling's HybridChunker.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text (markdown from the rich-format parsers; plain text parses as markdown paragraphs). |
required |
source_meta
|
dict[str, Any]
|
Provenance copied into every chunk's metadata. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks in document order, each carrying |
list[Document]
|
its |
list[Document]
|
the |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/chunking/docling_hybrid.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
varagity.chunking.semantic
¶
Embedding-similarity boundary detection (spec_v2 §7, plan decision #12).
Groups adjacent sentences and splits where the topic shifts: each
segment is embedded (with one neighbor of context on each side, smoothing
single-sentence noise) through the existing infinity embeddings client in
passage mode, consecutive-segment cosine distances are computed, and a
boundary is drawn wherever the distance is a statistical outlier — above
the 95th percentile of the document's distances (LangChain's default
percentile breakpoint; the gradient variant is avoided per its open
KeyError bug — plan decision #12).
Two guarantees bound the output:
- Exact reconstruction below the ceiling — segments are sliced from the original text with their separators attached, so a chunk is a verbatim substring and un-resplit chunks concatenate back to the document.
- The 512-token ceiling — similarity groups are meaning-sized, not
budget-sized, so any group exceeding
CHUNK_SIZE(tokens, liketoken_based) is re-split with the token-denominated recursive splitter (CHUNK_OVERLAPtokens of overlap applies only there).
This strategy embeds at ingest time — one passage request per segment
window, batched by EMBEDDING_BATCH_SIZE — an added ingest cost the
chunker sweep measures before any default changes (ADR-008). The client
resolves lazily via get_model("embedding") per split, so the registry's
import-time instantiation performs no I/O; tests inject a deterministic
fake instead.
SemanticStrategy
¶
Splits where consecutive-segment embedding similarity drops.
Source code in varagity/chunking/semantic.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
__init__(*, embeddings=None)
¶
Create the strategy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
embeddings
|
EmbeddingsClient | None
|
Embeddings client for boundary detection; resolved
via the model registry ( |
None
|
Source code in varagity/chunking/semantic.py
130 131 132 133 134 135 136 137 138 | |
split(text, *, source_meta, verbose=None)
¶
Split a document's text at embedding-similarity boundaries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The full document text. |
required |
source_meta
|
dict[str, Any]
|
Provenance copied into every chunk's metadata. |
required |
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
list[Document]
|
The chunks in document order, each carrying |
list[Document]
|
its |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
APIError
|
If segment embedding still fails after the client's retries. |
Source code in varagity/chunking/semantic.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |