embedding

package
v0.60.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 19, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

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

View Source
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"
)
View Source
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

View Source
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.

View Source
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

func IsTerminal(err error) bool

IsTerminal reports whether err (or anything it wraps) was marked Terminal.

func Terminal

func Terminal(err error) error

Terminal wraps err so the chain stops instead of falling back. Use it for caller/data faults; leave transient faults unwrapped.

func ToStorageVector

func ToStorageVector(v []float32, normalize bool) ([]float32, error)

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

func (c APIConfig) SpaceKey() string

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.

func NewChain

func NewChain(providers []Provider, cfg BreakerConfig, clock func() time.Time) *Chain

NewChain builds a chain over providers in priority order (primary first). A nil clock uses time.Now; tests inject a fake one.

func (*Chain) Embed

func (c *Chain) Embed(ctx context.Context, req Request) (Result, error)

Embed runs the request through the chain and returns the first success.

type Embedder

type Embedder interface {
	Embed(ctx context.Context, req Request) (Result, error)
}

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

func (ix *Indexer) BackfillOnce(ctx context.Context) (int, error)

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.

const (
	KindAPI   Kind = "api"
	KindLocal Kind = "local"
)

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.

const (
	ModeQuery    Mode = "query"
	ModeDocument Mode = "document"
)

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.

const (
	PrivacyNormal    Privacy = "normal"
	PrivacySensitive Privacy = "sensitive"
)

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

func NewAPIProvider(cfg APIConfig) Provider

NewAPIProvider builds a remote embedding provider over the OpenAI Embeddings API. It works with any OpenAI-compatible endpoint via APIConfig.BaseURL.

type Request

type Request struct {
	Texts   []string
	Mode    Mode
	Privacy Privacy
}

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

func (s *Service) BindRiverClient(c *river.Client[pgx.Tx]) error

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

func (s *Service) EmbedQuery(ctx context.Context, text string) (pgvector.Vector, string, error)

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

func (s *Service) RegisterRiverWorker(workers *river.Workers)

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.

type SettingsProvider

type SettingsProvider interface {
	EmbeddingSettings(ctx context.Context) (Settings, error)
}

SettingsProvider supplies the current embedding configuration. The composition root backs it with the DB config store; the Service reads it live.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL