ef.segments¶
The canonical Segment data model — ef’s interchange type for L2.
A segment is a piece of text carved out of a source document by a
Segmenter. This module defines the one shape every
segmenter, adapter and store in ef agrees on, so segments round-trip
losslessly across the whole pipeline.
Two surfaces over the same data:
Segment— aTypedDict: the lightweight, streaming-friendly interchange type. A segment is just adict; the type is passive and every helper here is a free function over it.SegmentRecord— a frozen dataclass: the convenience surface for code that prefers attribute access and immutability.
Only text is genuinely required; id is required by convention and is
content-derived by default (segment_id()) so that re-segmenting the
same document yields the same ids — the basis of idempotent ingestion.
The two field names that are promoted to top level (rather than buried in
metadata) are parent_id and index — hierarchy is expressed by a
flat list with parent_id pointers, never a nested structure.
Example — build a segment, derive its id, view it as a record:
>>> seg = make_segment('Hello world.', index=0, tokens=3)
>>> seg['text']
'Hello world.'
>>> seg['id'] == segment_id('Hello world.')
True
>>> rec = segment_record(seg)
>>> rec.text, rec.index
('Hello world.', 0)
>>> rec.to_segment() == seg
True
Module Attributes
Metadata keys that, by convention, every |
Functions
|
Content-derived id for a segment — |
|
Build a |
|
Coerce |
Coerce |
Classes
A piece of text carved from a source document — the interchange type. |
|
|
Frozen-dataclass view over a |
- ef.segments.PROMOTED_METADATA_KEYS = ('source', 'source_type', 'tokenizer', 'token_count', 'embedding_model', 'page', 'license', 'ingestion_run_id')¶
Metadata keys that, by convention, every
efadapter must round-trip faithfully. They live insideSegment["metadata"](they are not promoted to top-level fields) but carry meaning the pipeline relies on. Recordingtokenizerin particular is mandatory — omitting it is the single most common silent bug in chunking pipelines (a segment’stokenscount is meaningless without knowing which tokenizer produced it).
- class ef.segments.Segment[source]¶
Bases:
TypedDictA piece of text carved from a source document — the interchange type.
A
TypedDict: aSegmentis a plaindict, which keeps it cheap to create and stream.total=Falsebecause most keys are optional; by conventiontextis always present andidis always set (derived fromtextif not supplied).- Keys:
text: The segment’s text. Required. id: Stable identifier — content-derived by default (
segment_id()). parent_id: Id of the source document or parent segment this came from. start: Character offset of the segment’s start in the source text. end: Character offset of the segment’s end (exclusive) in the source. index: Ordinal position of the segment in its segmenter’s output. tokens: Token count oftext(meaningful only withmetadatakeytokenizerrecording which tokenizer counted it).- metadata: Free-form mapping; framework-specific keys live here. See
PROMOTED_METADATA_KEYSfor the conventional keys.
- class ef.segments.SegmentRecord(text, id='', parent_id=None, start=None, end=None, index=None, tokens=None, metadata=<factory>)[source]¶
Bases:
objectFrozen-dataclass view over a
Segment— the convenience surface.Use this when attribute access and immutability read better than dict access; use the
SegmentTypedDictdirectly on the hot path. The two convert losslessly viato_segment()andsegment_record().idis derived fromtext(andmetadata) if left empty.>>> rec = SegmentRecord('hello', index=2) >>> rec.id == segment_id('hello') True >>> rec.to_segment()['index'] 2
- ef.segments.as_segment(x, **defaults)[source]¶
Coerce
xinto aSegment— accepts astr,Mappingor record.This is the segment-side normalizer that lets a
Segmenteraccept loosely-typed output (a bare string, a partialdict) and still emit a well-formedSegment.**defaultssupplies fallback values: a default fills a field only whenxdoes not already carry it.Accepts:
a
SegmentRecord— converted viaSegmentRecord.to_segment();a
str— becomes thetextof a fresh segment;a
Mappingwith atextkey — its keys are carried through.
The
idis always present on the result (derived if absent).- Raises:
TypeError – if
xis none of the accepted forms.ValueError – if
xis aMappingwithout atextkey.
- Return type:
>>> as_segment('plain text', index=3)['index'] 3 >>> as_segment({'text': 'kept', 'index': 0}, index=9)['index'] # x wins 0- Return type:
- ef.segments.make_segment(text, *, id=None, parent_id=None, start=None, end=None, index=None, tokens=None, metadata=None)[source]¶
Build a
Segment, deriving a content-basedidif none given.Optional fields are omitted from the result when
None— a segmentdictcarries only the keys it actually has.- Return type:
>>> seg = make_segment('chunk one', index=0, start=0, end=9) >>> sorted(seg) ['end', 'id', 'index', 'start', 'text'] >>> seg['id'] == segment_id('chunk one') True
- ef.segments.segment_id(text, metadata=None)[source]¶
Content-derived id for a segment —
sha256of its normalized content.The id is
sha256(normalize(text) + canonical_json(metadata))— the normalization and hashing primitives live inef.hashing, the single source of truthefcontent-addresses through. Because the id depends only on content, re-segmenting an unchanged document produces identical ids — which is what makes ingestion idempotent (a re-run is a no-op, not a duplicate). Passmetadataonly when two segments could share identicaltextyet must stay distinct (e.g. same boilerplate from different sources).- Return type:
>>> segment_id('hello') == segment_id('hello') True >>> segment_id('hello') == segment_id('world') False >>> segment_id('x', {'source': 'a'}) == segment_id('x', {'source': 'b'}) False
- ef.segments.segment_record(x)[source]¶
Coerce
x(str/Mapping/SegmentRecord) into a record.The dataclass counterpart of
as_segment()— the convenience-surface constructor.- Return type:
>>> segment_record('hello').text 'hello' >>> segment_record({'text': 'hi', 'tokens': 1}).tokens 1