Core Principle: Enrichment, Not Replacement
The embedding annotates the observation - it doesnβt replace.data.
In memory1, .data IS the embedding and you need parent_id + project_to() to get back to the source image. We avoid this entirely.
Observation Types
EmbeddedObservation is a subclass - passes anywhere Observation is accepted (LSP).
Users who donβt care about types just use Observation. Users who want precision annotate with EmbeddedObservation.
derive() on Observation promotes to EmbeddedObservation if embedding= is passed.
derive() on EmbeddedObservation returns EmbeddedObservation, preserving the embedding unless explicitly replaced.
Embed Transformer
Embed is Transformer[T, T] - same data type in and out. It populates .embedding on each observation:
Stream[Image] stays Stream[Image] after embedding - T is about .data, not the observation subclass.
Search
.search(query_vec, k) lives on Stream itself. Returns a new Stream filtered to top-k by cosine similarity:
Backend Handles Storage Strategy
The Backend composite decides how to route storage based on what it sees:append(image, ts=now, embedding=vec)β backend routes: blob via BlobStore, vector via VectorStore, metadata via ObservationStoreappend(image, ts=now)β blob + metadata only (no embedding)ListObservationStore: stores metadata in-memory, brute-force cosine via MemoryVectorStoreSqliteObservationStore: metadata in SQLite, vec0 side table for fast ANN search via SqliteVectorStore- Future backends (Postgres/pgvector, Qdrant, etc.) do their thing
.search() calls through.
Projection / Lineage
Usually not needed. Since.data IS the original data, search results give you the image directly.
When a downstream transform replaces .data (e.g., Image β Detection), use temporal join to get back to the source:
