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.
Four providers
Section titled “Four providers”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:
ollamadegrades, not dies. An unreachable server, an unpulled model, or a timeout (30 s) logs a warning and returnsnull— a 404 logs a hint to runollama pull <model>— and the write proceeds vector-less.localis 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--regenerateunder a real provider — the Ollama default oropenai(see the migration runbook).openaiwithout a key degrades, not dies. IfOPENAI_API_KEYis absent, embedding generation is silently disabled and writes proceed vector-less — same contract asdisabled.
No cross-request cache
Section titled “No cross-request cache”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.
Dimensions are resolved, not assumed
Section titled “Dimensions are resolved, not assumed”The pipeline resolves its dimensionality in strict precedence order:
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.- The known-model map —
MODEL_DIMENSIONSin@engram/embeddings:nomic-embed-text768,mxbai-embed-large1024,all-minilm384,bge-m31024,local-hash1536,text-embedding-3-small1536,text-embedding-3-large3072. - 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-text →
text-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 modes, made boring
Section titled “Failure modes, made boring”| 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.