Documentation
¶
Overview ¶
Package embedding turns text into vectors for semantic search. It exposes a single capability — Embed — behind a provider chain so the rest of Stella never knows or cares whether a given vector came from a remote API or a local model.
The chain is API-first: a remote provider serves the request when healthy and the chain falls back to the next provider (e.g. a local model) only on transient failures, never on caller/data errors. See Chain for the rules.
Vector-space safety. Every embedding lives in exactly one vector space — the (model, dimension) pair its provider produces. Vectors from different spaces are not comparable even at the same storage width, so Result carries the space key (Result.Model) that produced it and callers MUST filter stored vectors by it (WHERE model = result.Model). That turns a cross-space query into "no rows" instead of "wrong rows": a fallback that lands in a different space than the stored corpus simply matches nothing rather than returning garbage.
Index ¶
- Constants
- Variables
- func IsTerminal(err error) bool
- func Terminal(err error) error
- func ToStorageVector(v []float32, normalize bool) ([]float32, error)
- type APIConfig
- type BootConfig
- type BreakerConfig
- type Chain
- type Embedder
- type IndexConfig
- type Indexer
- type Kind
- type Mode
- type Privacy
- type Provider
- type Request
- type Result
- type Service
- func (s *Service) BackfillQueueConfig() (string, river.QueueConfig)
- func (s *Service) BindRiverClient(c *river.Client[pgx.Tx]) error
- func (s *Service) EmbedQuery(ctx context.Context, text string) (pgvector.Vector, string, error)
- func (s *Service) RegisterRiverWorker(workers *river.Workers)
- func (s *Service) StartBackfill() (rivertype.PeriodicJobHandle, error)
- func (s *Service) StopBackfill(handle rivertype.PeriodicJobHandle)
- type Settings
- type SettingsProvider
Constants ¶
const ( // BackfillQueue isolates embedding backfill from other River queues. One // worker per node: backfill is a single serialized drainer, so two never race. BackfillQueue = "stella_embedding_backfill" )
const StorageDim = 1536
StorageDim is the fixed width of the vector(1536) columns every embedding is stored in. The HNSW indexes require a single dimension, so all vectors — from any provider — are widened to this before persistence.
Variables ¶
var ErrDisabled = errors.New("embedding: lane disabled")
ErrDisabled is returned by resolve when the lane is turned off or unconfigured (no API key). It is a control signal, not a failure: query and backfill paths treat it as "no semantic lane", not as an error to surface or retry.
var ErrNoProvider = errors.New("embedding: no eligible provider")
ErrNoProvider means the chain had no eligible provider for the request — e.g. a sensitive request with no local provider, or an empty chain.
Functions ¶
func IsTerminal ¶
IsTerminal reports whether err (or anything it wraps) was marked Terminal.
func Terminal ¶
Terminal wraps err so the chain stops instead of falling back. Use it for caller/data faults; leave transient faults unwrapped.
func ToStorageVector ¶
ToStorageVector adapts a provider's native vector to the fixed storage width. A vector shorter than StorageDim is zero-padded (padding with zeros leaves the L2 norm unchanged, so cosine distance is preserved); a longer one is rejected, since silently truncating would corrupt the space. When normalize is true the result is L2-normalized, which lets cosine similarity be read as a dot product.
Types ¶
type APIConfig ¶
type APIConfig struct {
// Name labels the provider instance in logs (default "openai-embedding").
Name string
// Model is the embedding model id sent to the API (e.g. "text-embedding-3-small").
Model string
// Dim is the output dimension to request. When > 0 it is sent as the API
// `dimensions` parameter (supported by text-embedding-3-*), pinning output to
// the storage width so no padding is needed. Leave 0 for models that don't
// support it (the model's native dimension is then used).
Dim int
APIKey string
BaseURL string
}
APIConfig configures an OpenAI-compatible embedding API provider.
func (APIConfig) SpaceKey ¶
SpaceKey is the vector-space identity written to and filtered on the *_embedding.model column. It folds the requested dimension into the model id so that (model, dim) together name the space: changing the dimension on an existing corpus yields a NEW key, which makes old rows backfill candidates (model mismatch) and points queries at the new space, instead of silently comparing a re-dimensioned query against vectors stored at the old dimension. A 0 dim (the model's stable native width) uses the bare model id.
type BootConfig ¶
type BootConfig struct {
DB *pgxpool.Pool
Settings SettingsProvider // live config source (DB-backed)
BatchSize int // indexer batch size (0 => defaultBatchSize)
Interval time.Duration // backfill tick (0 => defaultInterval)
Logger *slog.Logger
}
BootConfig is the wiring the composition root supplies to build the lane.
type BreakerConfig ¶
type BreakerConfig struct {
// FailureThreshold is the count of consecutive transient failures that trips
// the breaker open.
FailureThreshold int
// OpenDuration is how long the breaker stays open before a single half-open
// probe is allowed through.
OpenDuration time.Duration
}
BreakerConfig tunes the per-provider circuit breaker. Zero values fall back to sensible defaults (see NewChain).
type Chain ¶
type Chain struct {
// contains filtered or unexported fields
}
Chain is an API-first provider chain: it tries providers in priority order (primary first) and falls back to the next only on transient failure. The rules, in order, for each request:
- Privacy: a PrivacySensitive request skips every KindAPI provider.
- Circuit breaker: a provider whose breaker is open is skipped without a call, so a dead API costs one timeout per open-window, not one per request.
- Terminal error: a caller/data fault (IsTerminal) stops the chain and returns immediately — falling back would only get the same rejection.
- Transient error: records a breaker failure and advances to the next provider.
If no provider is eligible (all skipped) the chain returns ErrNoProvider; if every eligible provider failed transiently it returns the last error.
type Embedder ¶
Embedder is the entry point the indexer depends on; *Chain satisfies it. Kept as a narrow interface so tests inject a deterministic fake.
type IndexConfig ¶
type IndexConfig struct {
// Model is the canonical vector space this deployment embeds into. The indexer
// scans for rows missing this space and writes only vectors produced in it, so
// a fallback to a different space never fragments the stored corpus.
Model string
// Normalize L2-normalizes each vector before storage (cosine as dot product).
Normalize bool
// BatchSize bounds how many rows of each source one BackfillOnce pass embeds
// (default 128). One API round-trip embeds a whole batch.
BatchSize int
}
IndexConfig configures backfill.
type Indexer ¶
type Indexer struct {
// contains filtered or unexported fields
}
Indexer populates the *_embedding sidecars from their source rows. One pass (BackfillOnce) embeds up to BatchSize stale rows per source; a periodic caller drives it until the backlog drains.
func NewIndexer ¶
func NewIndexer(q *sqlc.Queries, emb Embedder, cfg IndexConfig) *Indexer
NewIndexer builds an indexer. emb must produce vectors in cfg.Model's space.
func (*Indexer) BackfillOnce ¶
BackfillOnce runs one bounded pass over every source and returns how many rows it embedded. It is safe to call repeatedly; a row already current in the canonical space is skipped, so once the backlog drains a pass returns 0.
type Kind ¶
type Kind string
Kind is where a provider runs: a remote API, or a local in-process model.
type Mode ¶
type Mode string
Mode distinguishes embedding a search query from embedding stored content. Some models embed the two asymmetrically (a query prompt vs a document prompt); providers that don't care ignore it.
type Privacy ¶
type Privacy string
Privacy gates whether a request may leave the machine. A sensitive request skips every remote (KindAPI) provider in the chain and is served only by a local one; if none is configured it fails rather than silently going remote.
type Provider ¶
type Provider interface {
// Name identifies the provider instance in logs and Result.ProviderName.
Name() string
// Kind reports whether the provider is remote or local, which the chain uses
// to honor PrivacySensitive.
Kind() Kind
// Model is the vector-space key every vector from this provider belongs to.
Model() string
// Embed returns one vector per input text, in order. It must return a
// Terminal error (see IsTerminal) for caller/data faults that retrying or
// failing over cannot fix; any other error is treated as transient and lets
// the chain fall back.
Embed(ctx context.Context, req Request) (Result, error)
}
Provider is one embedding backend. Implementations are expected to be safe for concurrent use.
func NewAPIProvider ¶
NewAPIProvider builds a remote embedding provider over the OpenAI Embeddings API. It works with any OpenAI-compatible endpoint via APIConfig.BaseURL.
type Request ¶
Request is one batch embed call. All texts are embedded in the same mode and returned in input order.
type Result ¶
type Result struct {
// Vectors aligns 1:1 with Request.Texts, in order. Each has the producing
// provider's native dimension; widening to the fixed storage width is a
// separate step at the persistence boundary, not the provider's job.
Vectors [][]float32
// Model is the vector-space key: the value to write into the *_embedding
// "model" column and to filter on at query time. Two vectors are comparable
// iff their Model matches.
Model string
// ProviderName is the provider that actually served the request (for metrics
// and debugging); FallbackUsed is true when it was not the chain's primary.
ProviderName string
FallbackUsed bool
}
Result holds the vectors for a Request plus the provenance a caller needs to store and later query them safely.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the always-present bundle for the embedding lane. Unlike a boot-fixed provider, it is config-driven: it reads Settings on each use and (re)builds its chain + indexer when the configuration changes, so enabling/disabling the lane or swapping model/key/dimension in the UI takes effect at runtime. It does NOT own a River client; the composition root injects the shared one via BindRiverClient.
When the lane is disabled the query embedder reports "no space" (search stays pure-BM25) and the backfill worker no-ops, so an unconfigured deployment behaves exactly as if no embedding existed — the worker just idles.
func Boot ¶
func Boot(cfg BootConfig) *Service
Boot constructs the always-present embedding lane. The backfill periodic is not started; the composition root injects the shared River client (BindRiverClient) and starts it (StartBackfill).
func (*Service) BackfillQueueConfig ¶
func (s *Service) BackfillQueueConfig() (string, river.QueueConfig)
BackfillQueueConfig returns the queue name and per-node worker config for the composition root assembling the shared working client.
func (*Service) BindRiverClient ¶ added in v0.60.0
BindRiverClient injects the shared working River client the backfill periodic is registered against. Call after the client is built, before StartBackfill. BindRiverClient binds the shared working River client before StartBackfill. One-shot pre-start bind: rejects a nil client (missing), a second bind (duplicate), and any bind after StartBackfill (late).
func (*Service) EmbedQuery ¶
EmbedQuery embeds one search query into a storage-width vector and returns it with the vector space it belongs to, so the caller can run KNN against the matching index. When the lane is disabled it returns an empty space key and no error, which the caller treats as "no semantic lane" (pure-BM25 fallback). Normalization matches the writer's so query and document vectors are comparable.
func (*Service) RegisterRiverWorker ¶
RegisterRiverWorker registers the backfill worker into the shared workers bundle. Call before building the client (composition root).
func (*Service) StartBackfill ¶
func (s *Service) StartBackfill() (rivertype.PeriodicJobHandle, error)
StartBackfill registers the backfill as a single-leader River periodic job. River enqueues a periodic only on the elected leader and ByState uniqueness keeps at most one backfill in flight cluster-wide; RunOnStart kicks an initial drain so a fresh deployment starts populating immediately. Requires BindRiverClient first.
func (*Service) StopBackfill ¶
func (s *Service) StopBackfill(handle rivertype.PeriodicJobHandle)
StopBackfill removes the backfill periodic so no further firings are enqueued.
type Settings ¶
type Settings struct {
Enabled bool
Model string // model id sent to the API
Dim int // requested output dimension (0 = model native)
APIKey string
BaseURL string
Normalize bool
}
Settings is the runtime embedding configuration the Service reads on every query and backfill pass, so changes made in the UI take effect without a restart.