ef.segmenters¶
The Segmenter facade — ef’s segment layer (L2).
A segmenter turns a document into a stream of Segment
pieces. Like the embedder facade, its core is a tiny structural contract:
Segmenter = Callable[[str | Mapping], Iterable[Segment]]
Everything else — overlap, hierarchy, batching, the recursive splitting algorithm — is layered scaffolding over that one shape.
This module defines:
Segmenter— the@runtime_checkableprotocol (a callable). It is a protocol, not an ABC: any matching callable is a segmenter.BatchedSegmenter— an optional optimization trait (abatchmethod) for segmenters that can process many documents at once.BaseSegmenter— an implementation-convenience base (a defaultbatch()and arepr). Subclassing is optional.RecursiveCharacterSegmenter— the default segmenter: recursive character splitting at ~512 tokens with ~12.5% overlap. The honest default — within a point or two of fancier methods on realistic corpora.line_segmenter()— a builtin that splits a document into its non-empty lines (a plain function — proof that a bare callable is a segmenter).FunctionSegmenter— wraps a baretext -> piecescallable into a full segmenter; the bridge forimbed’s registered segmenters.Composition helpers —
with_overlap(),hierarchical(),materialise().
Streaming is first-class: a segmenter returns an iterable, not a list, so the heavy case (a huge corpus) never has to fit every segment in memory at once.
The dependency-injection seam as_segmenter() and
the imbed registry bridge live in ef.segmenter_adapters.
Example — split a document into ~4-token chunks with one token of overlap:
>>> seg = RecursiveCharacterSegmenter(chunk_size=4, chunk_overlap=1)
>>> [c['text'] for c in seg('alpha beta gamma delta epsilon')]
['alpha beta gamma delta', 'delta epsilon']
>>> isinstance(seg, Segmenter)
True
Module Attributes
Identifier recorded as the |
|
Separators tried, in order, by |
Functions
|
Segment a document into its non-empty lines. |
|
Wrap |
|
Compose segmenters into levels — each segments the previous level's output. |
|
Force a streaming segmenter (or segment stream) to a concrete list. |
|
Rough token count — the common |
Classes
|
Structural type for a segmenter: |
|
Optional trait: a segmenter that can process many documents at once. |
Implementation base for segmenters (optional — |
|
|
Promote a bare |
|
The default segmenter — recursive character splitting. |
- ef.segmenters.APPROX_TOKENIZER = 'char-approx'¶
Identifier recorded as the
tokenizermetadata key when token counts come fromapprox_token_count(). Recording which tokenizer counted a segment is mandatory — atokensnumber is meaningless without it.
- class ef.segmenters.BaseSegmenter[source]¶
Bases:
objectImplementation base for segmenters (optional —
Segmenteris structural).A subclass implements
__call__; in return it gets a defaultbatch()(one-document-at-a-time — override it for a genuinely batched backend) and a readablerepr. Subclassing is purely a convenience; satisfyingSegmenterstructurally is what matters.
- class ef.segmenters.BatchedSegmenter(*args, **kwargs)[source]¶
Bases:
ProtocolOptional trait: a segmenter that can process many documents at once.
Plain
Segmenterhandles one document per call. A segmenter that can amortize per-call cost across documents (a shared model load, a vectorized pass) also offersbatch.BaseSegmentersupplies a correct — if un-optimized — default, so everyBaseSegmentersubclass is already aBatchedSegmenter.
- ef.segmenters.DEFAULT_SEPARATORS: tuple[str, ...] = ('\n\n', '\n', '. ', ', ', ' ', '')¶
Separators tried, in order, by
RecursiveCharacterSegmenter. Earlier (coarser) boundaries are preferred; the empty string is the last resort, splitting between characters so an oversized run is never left unsplit.
- class ef.segmenters.FunctionSegmenter(func, *, pass_doc=False, count_tokens=<function approx_token_count>, tokenizer='char-approx')[source]¶
Bases:
BaseSegmenterPromote a bare
text -> piecescallable to a fullSegmenter.The bridge for segmenter functions that carry no segment schema — most importantly
imbed’s registered segmenters, which map text to an iterable of plain strings. Each piece the callable yields is normalized:a
strpiece becomes aSegment; its character offsets are recovered by locating it in the source text, and a token count plustokenizermetadata are attached;a
Mapping/SegmentRecordpiece is coerced withas_segment(), gaining only anindex/parent_idwhere it lacks them.
- Parameters:
func (
Callable[...,Any]) – The callable to wrap. By default it receives the document’s text; withpass_doc=Trueit receives the whole document.pass_doc (
bool) – Pass the raw document instead of its text.count_tokens (
Optional[Callable[[str],int]]) – Token counter forstrpieces (Nonedisables thetokens/tokenizerannotation).tokenizer (
str) – Name recorded as thetokenizermetadata key.
>>> fs = FunctionSegmenter(lambda text: text.split(',')) >>> [s['text'] for s in fs('a,b,c')] ['a', 'b', 'c'] >>> s = list(fs('a,b,c'))[1] >>> (s['start'], s['end'], s['index']) (2, 3, 1)
- class ef.segmenters.RecursiveCharacterSegmenter(*, chunk_size=512, chunk_overlap=64, separators=('\\n\\n', '\\n', '. ', ', ', ' ', ''), count_tokens=<function approx_token_count>, tokenizer='char-approx')[source]¶
Bases:
BaseSegmenterThe default segmenter — recursive character splitting.
Splits a document on a descending ladder of separators (paragraph → line → sentence → clause → word → character), merging the resulting pieces into chunks of about
chunk_sizetokens withchunk_overlaptokens shared between neighbours. This “good enough” default is, on realistic corpora, within a point or two of far more elaborate methods — upgrade only if you can measure the win.Sizes are measured with
count_tokens. The default counter is the dependency-freeapprox_token_count(); inject a real tokenizer (and itstokenizername) when an exact count matters. Every emitted segment records the tokenizer in its metadata and carries exactstart/endcharacter offsets into the source.- Parameters:
chunk_size (
int) – Target chunk size, incount_tokensunits.chunk_overlap (
int) – Tokens of overlap between consecutive chunks (must be smaller thanchunk_size).separators (
Sequence[str]) – The separator ladder, coarsest first; the last should be""so an unsplittable run is still broken between characters.count_tokens (
Callable[[str],int]) – Maps a string to a token count.tokenizer (
str) – Name recorded as thetokenizermetadata key.
>>> seg = RecursiveCharacterSegmenter(chunk_size=3, chunk_overlap=1) >>> [c['text'] for c in seg('one two three four')] ['one two three', 'three four']
- class ef.segmenters.Segmenter(*args, **kwargs)[source]¶
Bases:
ProtocolStructural type for a segmenter:
str | Mapping -> Iterable[Segment].A segmenter is just a callable. It accepts a document — a bare
stror aMappingwith atextkey (and optionally anid, which becomes the emitted segments’parent_id) — and yieldsSegmentpieces. It returns an iterable, not a list: streaming is the default so the heavy case scales.Because the protocol’s only member is
__call__, every callable satisfiesisinstance(x, Segmenter). Treat it as documentation of intent, not a discriminating check —as_segmenter()is the seam that turns an arbitrary callable into a well-behaved segmenter.
- ef.segmenters.approx_token_count(text)[source]¶
Rough token count — the common
~4 characters per tokenheuristic.A dependency-free default so
efcan talk in “tokens” without pulling intiktokenor a model tokenizer. Inject a real counter (and its name) into a segmenter when an exact count matters.- Return type:
>>> approx_token_count('a' * 40) 10 >>> approx_token_count('') 0
- ef.segmenters.hierarchical(segmenters, /)[source]¶
Compose segmenters into levels — each segments the previous level’s output.
Level 0 applies
segmenters[0]to the document. Level k appliessegmenters[k]to every level-(k-1) segment’s text, setting each child’sparent_idto that segment’sidand shifting its offsets to stay absolute. The result is a flat stream of every segment of every level (level 0 first), with the tree expressed purely throughparent_idpointers.Each entry of
segmentersis passed throughas_segmenter(), so strings (e.g."recursive") and bare callables are accepted alongside ready-made segmenters.- Return type:
- ef.segmenters.line_segmenter(doc, /)[source]¶
Segment a document into its non-empty lines.
Each line is stripped of surrounding whitespace; blank lines are dropped. Character offsets point at the stripped content within the source.
A plain function — and therefore already a
Segmenter. It carries the markeras_segmenter()uses to recognize a ready-made segmenter.>>> [s['text'] for s in line_segmenter(' hello \n\nworld\n')] ['hello', 'world']
- ef.segmenters.materialise(x)[source]¶
Force a streaming segmenter (or segment stream) to a concrete list.
Streaming is the default, but the light case often wants everything in hand.
materialise()is polymorphic:given a segmenter (a callable), it returns a new segmenter whose output is a
listrather than a lazy iterator;given an iterable of segments, it returns a
listof them.
>>> eager = materialise(line_segmenter) >>> out = eager('a\nb') >>> isinstance(out, list) True >>> [s['text'] for s in out] ['a', 'b'] >>> materialise(s for s in [{'text': 'x'}]) [{'text': 'x'}]- Return type:
- ef.segmenters.with_overlap(segmenter, n_chars, /, *, count_tokens=<function approx_token_count>, tokenizer='char-approx')[source]¶
Wrap
segmenterso consecutive segments overlap byn_charscharacters.Each segment is extended forward by up to
n_charscharacters (clamped to the source length), so segment K shares its tail with segment K+1’s head. A segment whose text changes has itsidre-derived and itstokensrecounted; segments lackingstart/endoffsets pass through untouched.Overlap is expressed in characters here (this is a generic post-hoc helper) — for token-measured overlap use a segmenter’s own
chunk_overlap.- Return type:
>>> base = RecursiveCharacterSegmenter(chunk_size=3, chunk_overlap=0) >>> [c['text'] for c in base('one two three four')] ['one two three', 'four'] >>> [c['text'] for c in with_overlap(base, 100)('one two three four')] ['one two three four', 'four']