ef.embedders

The Embedder facade — ef’s embed layer (L3).

An embedder is, fundamentally, a batch callable:

Iterable[str] -> ndarray of shape (n, dim)

Everything else — batching, async/batch-APIs, normalization, task hints, caching, retries — is layered scaffolding over that one contract.

This module defines the small, structural core of the facade:

  • Embedder — the @runtime_checkable protocol (a callable plus four introspectable metadata attributes). It is a protocol, not an ABC: any object with the right shape is an embedder.

  • BatchHandle / ready_handle() — a uniform poll/result shape that collapses synchronous embedding and provider Batch APIs into one thing.

  • BaseEmbedder — an implementation convenience base (supplies a default synchronous embed_batch() and a repr). Subclassing it is optional; satisfying Embedder structurally is what matters.

  • FunctionEmbedder — wraps a bare texts -> vectors callable into a full Embedder (the bridge for e.g. imbed’s embedder functions).

  • HashingEmbedder — a dependency-free embedder (the feature-hashing trick, numpy only); ef’s zero-install default when no embedder is given.

  • cache_key() — a deterministic key pinning everything that changes a vector, for CachedEmbedder.

  • embed_length_sorted() — length-sorted batching with order reconstruction; a 5–10× throughput win that ef owns so users never have to think about padding or sort order.

Composition wrappers live in ef.embedder_wrappers; concrete provider / local adapters and the as_embedder() dependency -injection seam live in ef.embedder_adapters.

Example — wrap a plain function and embed two strings:

>>> import numpy as np
>>> def toy(texts):  # a bare callable: list[str] -> array(n, 3)
...     return np.array([[len(t), t.count(' '), 1.0] for t in texts])
>>> embedder = FunctionEmbedder(toy, model_id='toy@3')
>>> vectors = embedder(['hello', 'a b c'])
>>> vectors.shape
(2, 3)
>>> embedder.dim  # inferred on first call
3
>>> isinstance(embedder, Embedder)
True

Module Attributes

InputType

Canonical embedding-task vocabulary.

BatchStatus

The lifecycle states of a BatchHandle.

Functions

ready_handle(array)

Wrap an already-computed array in a finished BatchHandle.

cache_key(embedder, text[, input_type])

Deterministic cache key for one (embedder, text, input_type) triple.

embed_length_sorted(texts, encode, *[, ...])

Encode texts in length-sorted batches; return vectors in caller order.

Classes

BatchHandle(poll, result[, cancel])

A poll/result handle over an in-flight (or already-finished) embed job.

Embedder(*args, **kwargs)

Structural type for an embedder: a batch callable plus metadata.

BaseEmbedder()

Implementation base for embedders (optional — Embedder is structural).

FunctionEmbedder(func, *[, model_id, dim, ...])

Promote a bare texts -> vectors callable to a full Embedder.

HashingEmbedder(*[, dim, ngram_range, ...])

A dependency-free embedder — the feature-hashing trick, numpy only.

Exceptions

EmbedderError

Raised for embedder-specific failures (shape mismatch, batch job failure).

class ef.embedders.BaseEmbedder[source]

Bases: object

Implementation base for embedders (optional — Embedder is structural).

Subclasses set model_id / dim / normalized / honored_input_types and implement __call__; they then satisfy Embedder structurally. In return they get:

  • a default synchronous embed_batch() (ready_handle(self(...))) — override it only for a genuinely async provider Batch API;

  • a readable repr.

embed_batch(texts, *, input_type=None, **backend)[source]

Default: embed synchronously and return a finished handle.

Return type:

BatchHandle

class ef.embedders.BatchHandle(poll, result, cancel=<function BatchHandle.<lambda>>)[source]

Bases: object

A poll/result handle over an in-flight (or already-finished) embed job.

Batch-and-poll is just a special case of async. A synchronous backend returns a ready handle (via ready_handle()); a provider Batch API returns a genuinely pending one. Either way the caller sees one shape.

poll

() -> BatchStatus — non-blocking status check.

result

() -> np.ndarray — blocks until done, returns (n, dim).

cancel

() -> None — best-effort cancellation.

ef.embedders.BatchStatus

The lifecycle states of a BatchHandle.

alias of Literal[‘pending’, ‘done’, ‘failed’]

class ef.embedders.Embedder(*args, **kwargs)[source]

Bases: Protocol

Structural type for an embedder: a batch callable plus metadata.

An embedder maps Iterable[str] to an ndarray of shape (n, dim). The four metadata attributes are the single source of truth *on the object itself* — no global registry, no config singleton.

model_id

Identity that bakes in everything affecting the vector — "openai:text-embedding-3-large@1024" (provider:model@dim). Works directly as a cache namespace.

dim

Output dimensionality. May be None until first call for lazily-probed embedders (e.g. a bare callable of unknown width).

normalized

True iff ||v|| == 1 by construction.

honored_input_types

The InputType values this embedder acts on (empty if it ignores the task hint).

embed_batch(texts, *, input_type=None, **backend)[source]

Submit an embed job, returning a BatchHandle.

Return type:

BatchHandle

exception ef.embedders.EmbedderError[source]

Bases: Exception

Raised for embedder-specific failures (shape mismatch, batch job failure).

class ef.embedders.FunctionEmbedder(func, *, model_id=None, dim=None, normalized=False, honored_input_types=(), pass_backend=True)[source]

Bases: BaseEmbedder

Promote a bare texts -> vectors callable to a full Embedder.

This is the bridge for embedder functions that carry no metadata — most importantly imbed’s registered embedders. The wrapped callable is asked only for func(texts) (it knows nothing of input_type); the wrapper advertises honored_input_types=() accordingly.

Parameters:
  • func (Callable[..., Any]) – A callable mapping a list of strings to an array-like of shape (n, dim) (a numpy array or a nested sequence — both accepted).

  • model_id (str | None) – Vector-identity string. Defaults to "function:<name>".

  • dim (int | None) – Output width. If None it is inferred (and frozen) on the first call.

  • normalized (bool) – Whether func already returns unit vectors.

  • honored_input_types (Iterable[Literal['query', 'document', 'classification', 'clustering']]) – Task hints func honors (usually none).

  • pass_backend (bool) – If True (default), forward any **backend kwargs to func; set False for callables with a strict (texts,) signature.

>>> import numpy as np
>>> e = FunctionEmbedder(lambda ts: np.ones((len(ts), 4)), model_id='ones@4')
>>> e(['a', 'b', 'c']).shape
(3, 4)
class ef.embedders.HashingEmbedder(*, dim=512, ngram_range=(1, 2), sublinear_tf=True)[source]

Bases: BaseEmbedder

A dependency-free embedder — the feature-hashing trick, numpy only.

HashingEmbedder is ef’s zero-install default: the embedder ingest() resolves to when none is given, so the headline ingest([...]).search(query) works on a bare pip install ef — no torch, no model download, no network.

It is a lexical embedder, not a neural one. Each text is tokenized into lowercased word n-grams; every token is hashed into one of dim buckets with a sign (the hashing trick — no vocabulary, no fitting, fixed memory); the signed, sublinear-tf-weighted bucket counts are L2-normalized. The cosine similarity of two vectors then ranks by shared vocabulary — enough for demos, doctests and offline tests, and a sane fallback in production. For genuine semantic quality install a real embedder (ef[sentence-transformers], ef[openai]) and pass it explicitly.

Every parameter that changes a vector is baked into model_id, so a HashingEmbedder’s artifacts stay correctly content-addressed in the ArtifactGraph.

Parameters:
  • dim (int) – Output width. Wider → fewer hash collisions, larger vectors.

  • ngram_range (tuple[int, int]) – Inclusive (min, max) word-n-gram sizes. The default (1, 2) mixes single words with adjacent pairs — a little phrase sensitivity at no real cost.

  • sublinear_tf (bool) – Weight a token by 1 + log(count) rather than its raw count, damping a word repeated many times in one text.

>>> e = HashingEmbedder()
>>> e.model_id
'hashing:v1@512'
>>> import numpy as np
>>> v = e(['the quick brown fox'])
>>> v.shape
(1, 512)
>>> bool(np.isclose(np.linalg.norm(v[0]), 1.0))  # rows are L2-normalized
True
>>> a, b = e(['hello world']), e(['hello world'])
>>> round(float(a[0] @ b[0]), 5)  # identical text → cosine similarity 1.0
1.0
>>> isinstance(e, Embedder)
True
ef.embedders.InputType

Canonical embedding-task vocabulary. Every provider names the same distinction differently (input_type / task_type / task / prompt_name); ef defines one canonical set and each adapter translates it at its boundary. This is the facade’s single most valuable normalization. Backends that ignore the hint advertise an empty honored_input_types.

alias of Literal[‘query’, ‘document’, ‘classification’, ‘clustering’]

ef.embedders.cache_key(embedder, text, input_type=None)[source]

Deterministic cache key for one (embedder, text, input_type) triple.

The key pins everything that changes the resulting vector. model_id is the embedder-identity SSOT — it already bakes in provider, model and dim — so dim is not keyed separately (keying it would also break FunctionEmbedder’s lazy dim inference). normalized is keyed because a NormalizingEmbedder shares its inner’s model_id yet yields different vectors.

Returns "<model_id>/<sha256>" — a namespaced key safe for any MutableMapping store.

Return type:

str

>>> class _E:
...     model_id, normalized = 'm@8', False
>>> k1 = cache_key(_E(), 'hello', 'document')
>>> k2 = cache_key(_E(), 'hello', 'query')
>>> k1 == k2  # input_type participates
False
>>> k1.startswith('m@8/')
True
ef.embedders.embed_length_sorted(texts, encode, *, batch_size=128)[source]

Encode texts in length-sorted batches; return vectors in caller order.

Transformer self-attention is O(n²) in sequence length and a batch is padded to its longest member, so grouping similar-length texts together avoids wasted compute — a 5–10× speedup on skewed corpora. ef owns this so callers never sort, pad, or un-sort themselves.

Parameters:
  • texts (Sequence[str]) – The strings to embed.

  • encode (Callable[[Sequence[str]], Any]) – Maps a list of strings to an array-like of shape (k, dim).

  • batch_size (int) – Texts per call to encode.

Return type:

ndarray

Returns:

A float32 array of shape (len(texts), dim) in the original order of texts.

>>> import numpy as np
>>> def enc(batch):  # toy encoder: vector = [length]
...     return np.array([[len(t)] for t in batch], dtype=np.float32)
>>> out = embed_length_sorted(['aaa', 'a', 'aaaaa', 'aa'], enc, batch_size=2)
>>> out.ravel().tolist()  # order preserved despite internal sorting
[3.0, 1.0, 5.0, 2.0]
ef.embedders.ready_handle(array)[source]

Wrap an already-computed array in a finished BatchHandle.

Return type:

BatchHandle

>>> import numpy as np
>>> h = ready_handle(np.zeros((2, 3)))
>>> h.poll()
'done'
>>> h.result().shape
(2, 3)