Embeddings
Every embedding-related knob — provider, model, dimensions, batch size, retries, and the constraints that link them to the HNSW index.
CortexDB calls an embedding service on every event ingest (one vector per chunk) and on every recall query (one for the query, plus N for HyDE-expanded queries). Embeddings are the single largest line item in the inference bill for most workloads, and vector recall does not work without a real provider configured.
The core constraint
The embedding service's output dimension must match the engine's vector index dimension. They are configured separately. A mismatch between the on-disk index width and the configured model is startup-fatal ("vector index dimension mismatch: on-disk index at /data/vector is 1536-dim but the configured embedding model is 3072-dim … refusing to serve broken semantic recall"). Upgrade from earlier builds, which served such a store with empty recall results.
| Setting | Where | Default |
|---|---|---|
| Embedding output dim | CORTEX_EMBEDDING_DIMS env var | 1536 |
| Index storage dim | cortex.toml → [engine] vector_dimensions | follows CORTEX_EMBEDDING_DIMS when unset |
Set the width through CORTEX_EMBEDDING_DIMS
With vector_dimensions unset (no cortex.toml, or an [engine] section without it), the index is
created at CORTEX_EMBEDDING_DIMS (1536 on the image). If you do set
vector_dimensions, it must equal the embedding dims. vector_dimensions
accepts only {256, 384, 512, 768, 1024, 1536, 3072} — anything else fails schema validation at
startup.
# cortex.toml
[engine]
vector_dimensions = 1536export CORTEX_EMBEDDING_MODEL=text-embedding-3-small
export CORTEX_EMBEDDING_DIMS=1536Choosing where embeddings run
The two common self-hosted setups are equally supported and behave identically from the API's point of view; they differ in where the model runs and therefore in what the machine needs.
- OpenAI API,
text-embedding-3-small(1536 dims). The image default: setCORTEX_EMBEDDING_API_KEYand nothing else. Nothing runs locally, the same key can serve the answer and enrichment lanes, and it costs fractions of a cent per thousand tokens. Choose this on modest hardware (a laptop, no GPU, limited RAM or disk) or whenever you want the fewest moving parts. - Local Ollama,
nomic-embed-text(768 dims). Free per call and the text to embed never leaves the box, at the price of running an embedding model yourself. Choose this on a high-end machine (spare CPU and RAM, ideally a GPU) or when vectors must stay on-premises. Expect ingest and recall latency to track the host's CPU on machines without a GPU.
Decide before the first write: the provider, model and dimensions are pinned to the data directory (below), and changing them afterwards is a deliberate offline rebuild.
Provider selection
export CORTEX_EMBEDDING_PROVIDER=<empty> | mock | cohere | ollama| Value | Behavior | Required env |
|---|---|---|
| (empty / unset) | HTTP service against CORTEX_EMBEDDING_URL. Default. | CORTEX_EMBEDDING_API_KEY, OPENAI_API_KEY or LLM_API_KEY |
mock | Deterministic mock embeddings at CORTEX_EMBEDDING_DIMS (1536 on the image). Development only. | — |
cohere | Cohere's embed API. | COHERE_API_KEY |
ollama (also: any URL containing :11434) | Local Ollama daemon. | CORTEX_EMBEDDING_URL=http://localhost:11434 + a non-empty CORTEX_EMBEDDING_API_KEY (see below) |
Ollama still needs a non-empty embedding key
Although Ollama ignores auth, a non-empty CORTEX_EMBEDDING_API_KEY is still required — an empty
value fails the readiness check. Set a dummy value (e.g. ollama). Setting
CORTEX_EMBEDDING_URL=http://localhost:11434 without an explicit provider auto-selects Ollama mode.
The provider pin
On first boot the server writes an embedding_provider.pin file (recording provider:model:dims) into
the data directory. On every later start, the configured provider is checked against the pin — a
mismatch is startup-fatal (FATAL: embedding provider mismatch: data dir /data is pinned to 'openai-http:text-embedding-3-small:1536' but the configured provider resolves to …), and a refused
boot leaves the data directory byte-identical. A missing API key can no longer silently downgrade a
real corpus to mock vectors. The pin is not a dead end: switching provider or model on an existing
store is the supported procedure in the next section.
Changing the embedding model on an existing store
Vectors from different models are not comparable, so a model change means re-embedding the corpus. That is a supported offline procedure on the same data directory. It takes two deliberate flags, and the destructive one is spent after one boot:
# 1. stop the server, then start it ONCE with the new model plus both flags
docker run -d --name cortexdb -v cortexdb-data:/data \
-e CORTEX_EMBEDDING_MODEL=text-embedding-3-large -e CORTEX_EMBEDDING_DIMS=3072 \
-e CORTEX_EMBEDDING_API_KEY=$OPENAI_API_KEY \
-e CORTEX_EMBEDDING_ALLOW_REPIN=1 -e CORTEX_EMBEDDING_REBUILD_VECTORS=1 \
cortexdb/cortexdb:v0.12.8
# boot log: "DESTRUCTIVE VECTOR REBUILD … DROPPING chunk (/data/vector, 1536-dim), entity (…)
# Recreating them EMPTY at 3072-dim"
# 2. refill the chunk vectors from the WAL with the new model (idempotent, resumable)
curl -X POST localhost:3141/v1/admin/index-audit -H "Content-Type: application/json" \
-d '{"scope":"org:acme","repair":true,"vectors":true}'
# repeat per scope until missing_vectors_total is 0; vector recall returns nothing until then (BM25 only)
# 3. remove the flags from your unit file / compose; confirm with an audit
curl -X POST localhost:3141/v1/admin/index-audit -H "Content-Type: application/json" -d '{"scope":"org:acme"}'What the server guarantees:
- Never automatic. A dimension mismatch without both flags still refuses to boot.
ALLOW_REPINalone is not enough for a dims change (vector index dimension mismatch … refusing to serve). - One-shot. The rebuild writes
/data/embedding_rebuild.consumed; while it exists the flag "authorizes NOTHING" on later boots (logged as a warning), so a flag left in a unit file is harmless. - Refused for mock. A lost credential looks like a switch to mock embeddings; that is refused with the data untouched.
- Honest about completeness.
/data/vector_rebuild_pending.jsonmarks the store INCOMPLETE and the audit response (vector_rebuild_pending,vector_families) lists each family with its own refill instruction:chunkis refilled by the audit repair;entity,derivedandsummaryvectors are written by enrichment and the layer builds, which must be re-run at the new width (the audit never writes those and does not report them missing). Delete the pending file once the families are back, or to accept the store as it is, andembedding_provider.repin.jsonafter re-embedding. ?repair=truewithout"vectors": truerepairs rows only and leaves the vectors empty.
The old alternative, export → fresh data directory on the new provider → import, still works and needs no offline window on the old store. An online re-embed job (dual-embed during the job, atomic swap, no BM25-only window) is not available.
When the API key is missing
An explicitly configured embedding endpoint without a credential is a boot
refusal, not a fallback: FATAL: an explicit CORTEX_EMBEDDING_URL requires CORTEX_EMBEDDING_API_KEY, OPENAI_API_KEY, or LLM_API_KEY and the process exits. The image bakes
an OpenAI embedding URL, so this includes the plain docker run cortexdb/cortexdb with no key. Mock
vectors are meaningless search, and the data directory is pinned to whatever wrote it, so a store filled
with mock vectors could never be pointed at a real provider afterwards; refusing is the honest failure.
CORTEX_EMBEDDING_PROVIDER=mock is the explicit opt-in (384-d deterministic vectors, development only);
a mock store reports degraded: true on GET /v1/admin/ready.
A data directory pinned to mock must be rebuilt on a real provider (see the migration section above).
The full env-var surface
| Env var | Default | What it controls |
|---|---|---|
CORTEX_EMBEDDING_URL | https://api.openai.com/v1 | Base URL of the embedding HTTP API. |
CORTEX_EMBEDDING_MODEL | text-embedding-3-small | Model name passed to the provider. |
CORTEX_EMBEDDING_DIMS | 1536 | Output dimension. Must match engine.vector_dimensions. |
CORTEX_EMBEDDING_PROVIDER | (empty) | mock, cohere, ollama, or empty for OpenAI-compatible HTTP. |
CORTEX_EMBEDDING_API_KEY | (none) | Provider-specific embedding key. Required non-empty even for Ollama. |
CORTEX_EMBEDDING_ALLOW_REPIN | (unset) | Set =1 for one restart to change the pinned provider/model. A dims change also needs REBUILD_VECTORS. |
CORTEX_EMBEDDING_REBUILD_VECTORS | (unset) | With ALLOW_REPIN, drop and recreate the mismatched vector indexes at the new width, once; see above. |
CORTEX_EMBEDDING_MAX_BATCH_ITEMS | 256 in the Docker image (2048 in the binary) | Max items per provider call. The OpenAI-compatible client clamps it to what fits one response; see Batch and retry tuning. |
CORTEX_EMBEDDING_RETRY_ATTEMPTS | 1 | Retries on transient error before failing the request. |
CORTEX_EMBEDDING_RETRY_BASE_DELAY_MS | 250 | Initial backoff between retries (exponential). |
CORTEX_EMBEDDING_HTTP_TIMEOUT_SECS | 30 | Per-request timeout for the embedding HTTP call. |
OPENAI_API_KEY | (none) | Primary key for OpenAI / HTTP mode. |
LLM_API_KEY | (none) | Generic fallback if OPENAI_API_KEY not set. |
COHERE_API_KEY | (none) | Required if CORTEX_EMBEDDING_PROVIDER=cohere. |
Choosing a model
| Model | Provider | Dims | Cost / 1M tok | When to pick it |
|---|---|---|---|---|
text-embedding-3-small | OpenAI | 1536 | $0.020 | Default. Used in the published 93.8% number. |
text-embedding-3-large | OpenAI | 3072 | $0.130 | ~+0.4pp on LongMemEval-S, ~3× cost. |
embed-multilingual-v3.0 | Cohere | 1024 | $0.100 | Strong on non-English content. |
nomic-embed-text | Ollama (local) | 768 | $0 | Local, free, ~5pp worse than OpenAI small. |
mxbai-embed-large | Ollama (local) | 1024 | $0 | Best local option; ~3pp worse than OpenAI small. |
bge-large-en-v1.5 | Ollama (local) | 1024 | $0 | Strong English-only local. |
The local rows assume a machine that can run the model comfortably: spare CPU and RAM, ideally a GPU. On modest hardware the OpenAI default is the better trade (see Choosing where embeddings run); the accuracy gap of the local models is small, the latency gap on a weak host is not.
Batch and retry tuning
The service packs pending requests into provider calls of up to CORTEX_EMBEDDING_MAX_BATCH_ITEMS
items. Larger batches improve throughput but raise the latency of the first request in a batch.
| Workload | MAX_BATCH_ITEMS | RETRY_ATTEMPTS | Why |
|---|---|---|---|
| Realtime / voice | 256 | 1 | Fail fast; small batches don't gather enough to wait for. |
| Default / mixed | 256 | 1 | The Docker image's default (the binary's compiled default is 2048). |
| Batch / ingest | 256 | 3 | Tolerate 429s with backoff; a batch above one response's limit (340 at 1536 dimensions) is clamped anyway (below). |
| Cohere | 96 | 2 | Cohere's per-call cap is lower than OpenAI's. |
The response-size clamp. Each provider response has to fit the OpenAI-compatible
client's byte limit, so the client caps each call at floor(12 MiB / (24 × dimensions + 128)) texts:
340 at 1536 dimensions, 170 at 3072, 2006 at 256. A larger CORTEX_EMBEDDING_MAX_BATCH_ITEMS is
clamped to that on every call, and the server logs one warning at startup:
WARN cortex_embedding: CORTEX_EMBEDDING_MAX_BATCH_ITEMS=2048 clamped to 340: one response for 2048 x 1536-dim vectors would exceed the embedding client's response byte limit; each provider call carries at most 340 texts configured=2048 effective=340 model="text-embedding-3-small" dims=1536At 3072 dimensions the image's own 256 is above the limit, so the warning (clamped to 170) appears
with no setting of yours; it is harmless. A long
document makes more, smaller calls.
Local-first with Ollama
ollama pull nomic-embed-text
export CORTEX_EMBEDDING_URL=http://localhost:11434
export CORTEX_EMBEDDING_MODEL=nomic-embed-text
export CORTEX_EMBEDDING_DIMS=768
export CORTEX_EMBEDDING_API_KEY=ollama # non-empty required# cortex.toml — match the dim
[engine]
vector_dimensions = 768Expect ~30 ms / embedding on a modern CPU. See Self-hosting defaults for the full content-only setup.
Caching
HTTP embedding services are wrapped in an in-process LRU keyed by (model, text_hash). The cache
survives the process lifetime; it's lost on restart, and its size is not env-configurable (~10K
entries compiled default). Highly effective for re-embedding the same text (eval reruns), near-useless
for cold ingest.
Diagnostics
On startup the binary logs which service it picked:
info Using HTTP embedding service model=text-embedding-3-small dims=1536
info Using Ollama local embedding service model=nomic-embed-text dims=768 url=http://localhost:11434
info Using MOCK embeddings (explicitly requested via CORTEX_EMBEDDING_PROVIDER=mock) dims=1536Grep for "embedding service" after changing config, and check GET /v1/admin/ready — it reports the
pinned provider and degraded: true when on mock. Recall failures are usually a missing API key or a
dim mismatch.
Next steps
- LLM & Answer Generation — the extraction and answer LLMs
- Recall Tuning — how embeddings feed the recall pipeline
- Storage & Cluster — the HNSW index that consumes these vectors
Profiles & Presets
Seven copy-paste configurations — Benchmark / Max-Recall / Voice / Batch / Cost / Enterprise / Quickstart — for the most common CortexDB deployment shapes.
LLM & Answer Generation
All 18 LLM-related env vars — entity extraction, async enrichment, answer generation, verifier — plus how the fallback chain works.