Skip to content

Embeddings

Embeddings power semantic recall, but Engram refuses to make them a hard dependency: an embeddings provider — a local model server or a remote API — is the least reliable component in the stack (network, quotas, keys, an unpulled model), and a memory server that cannot store because it cannot embed has its priorities backwards. The design rule is:

The embedding is an enhancement. When it cannot be produced, the memory workflow continues without a vector.

EmbeddingsService (packages/embeddings) returns null when the provider is disabled, unconfigured, or failing; callers treat a missing vector as “skip indexing”, never as an error. A vector-less memory is still created, listed, exported, and — once the provider is back — picked up by reindex or reembed_memory.

EMBEDDING_PROVIDER selects the implementation behind EMBEDDING_PROVIDER_TOKEN:

Provider What it does When to use
ollama (default) Calls a local Ollama server’s native /api/embed endpoint via plain HTTP (no SDK) — nomic-embed-text (768 dims) by default Local-first production recall, no API key or per-token cost
openai Calls the OpenAI embeddings API — text-embedding-3-small (1536 dims) by default, text-embedding-3-large (3072) supported Hosted recall quality, opt-in via OPENAI_API_KEY
local Deterministic hash-based vectors, no network Tests, CI, offline dev, cost-free bulk imports
disabled Always returns null Deployments that only need lexical/structured retrieval

The runtime resolves the model from the provider: EMBEDDING_MODEL defaults to nomic-embed-text under ollama and text-embedding-3-small under openai, and any model id is accepted. OLLAMA_URL (default http://localhost:11434) points at the Ollama server. Empty-string env values are read as unset, so compose-style ${VAR:-} defaults are safe.

Three properties of this lineup are intentional:

  • ollama degrades, not dies. An unreachable server, an unpulled model, or a timeout (30 s) logs a warning and returns null — a 404 logs a hint to run ollama pull <model> — and the write proceeds vector-less.
  • local is deterministic. The same text always yields the same vector, so tests and the eval harness get stable, service-free similarity behavior. It is also a cheap first pass for bulk imports: ingest with local vectors, then run a reindex with --regenerate under a real provider — the Ollama default or openai (see the migration runbook).
  • openai without a key degrades, not dies. If OPENAI_API_KEY is absent, embedding generation is silently disabled and writes proceed vector-less — same contract as disabled.

There is no cross-request embedding cache. Each embedding call generates a vector from the provider, and that vector is persisted on the memory row (embedding Float[]) — the single place a vector is stored. Reindex and backfill reuse those stored Float[] embeddings by default rather than re-embedding, so rebuilding the vector index costs no provider calls. Generated vectors live with their rows; there is no separate cache layer to size, expire, or operate.

The pipeline resolves its dimensionality in strict precedence order:

  1. VECTOR_DIMENSIONS — an optional strict pin. When set, the vector store verifies the index against it and throws an actionable error on mismatch. It no longer defaults to 1536.
  2. The known-model mapMODEL_DIMENSIONS in @engram/embeddings: nomic-embed-text 768, mxbai-embed-large 1024, all-minilm 384, bge-m3 1024, local-hash 1536, text-embedding-3-small 1536, text-embedding-3-large 3072.
  3. The actual vector — arbitrary model ids are allowed; an unknown model gets its dimensions from the first vector it produces.

When nothing pins the size up front, the store infers dimensionality from the first upserted vector and provisions itself accordingly. Switching models with a different dimensionality (e.g. nomic-embed-texttext-embedding-3-small) is therefore a reindex event: existing vectors are incompatible with the new index shape. The procedure — recreate the index and regenerate rather than reuse stored embeddings — is in Configure embeddings and Reindex embeddings.

Failure Behavior
Provider down at write time (Ollama unreachable, API down, 30 s timeout) Memory stored without vector; excluded from semantic hits until re-embedded
Provider down at query time recall returns empty semantic results (logged); no exception to the caller
Ollama model not pulled (404) Warning with an ollama pull <model> hint; write proceeds vector-less
Content edited while provider down Row flagged embeddingStale; repair with reembed_memory
Key revoked / quota exhausted Same as provider down — degrade, log, continue

This is the same philosophy as the vector store design: Postgres is the source of truth, everything derived can be regenerated, so no derived-state failure is allowed to block a write.