CortexDB best practices: ingestion, embeddings, local vs API models
A practical guide for teams putting CortexDB into production. It covers what to send and how, which embedding model to run, which parts can run inside your network, and how to keep a deployment current. Every setting named here is in Production Deployment and the API Reference.
1. Ingestion
What CortexDB does with a write
Every event you send is stored exactly as sent, in an append-only history, and
made searchable (by meaning and by keywords) without any schema on your side.
Nothing you send is silently dropped: the original is the record, and every
summary, fact or graph is a view rebuilt from it. When enrichment is on
(CORTEX_ENRICHMENT_URL, CORTEX_ENRICHMENT_MODEL,
CORTEX_ENRICHMENT_DELAY_SECONDS), a model reads each event in the
background and extracts facts, entities and beliefs. With enrichment off,
events are still stored and searchable; nothing is extracted.
CortexDB decides what to learn from an event, never whether to keep it. Deciding what to keep is your filter (below).
Filter before you send
| Send | How |
|---|---|
| Conversations, tickets, decisions, outcomes | normally (modality: "conversation", "observation", ...) |
| Tool results, logs, reference documents | as search-only: "directives": {"extract": []}. They stay findable but never become facts about a person |
| Health checks, retries, empty or duplicate responses, huge raw dumps nobody will ask about | don't send them |
| Anything containing passwords, tokens or keys | don't send it; PII scanning flags personal data, it does not remove secrets |
Sending everything works, but every event costs an embedding call (and an extraction call when enrichment is on) and adds noise to recall.
Shape each write well
- One event per message or turn, not whole conversation snapshots. Smaller items index faster, deduplicate better and retrieve more precisely.
- Always set
idempotency_keyfrom a stable source id (ticket id, message id). A retry then replays the first write instead of storing a duplicate. - Label what the event is:
context.labels(for example["ticket", "acme", "owner:priya"]) andcontext.intent. Recall can filter on them (filters.metadata.labels,modality,intent). Keep labels to tags: 0.10.5 announces limits that a later release will enforce by default (an operator can enforce them now withCORTEX_LABEL_LIMITS=on), and a write is then refused (422) with more than 4096 labels, a label over 256 KiB, an empty label, or an ASCII control character other than tab, line feed and carriage return in one (API reference §3.5.3, "Label limits"). - Separate kinds of memory by scope, for example
org:acme/project:ticketsandorg:acme/project:runbooks, so each can be recalled, retained and erased on its own. Every segment istype:id, and the type must be one the deployment allows: on the hosted serviceorg,dept,team,app,user,agent,service,ws,projectandsource(API reference §3.4.1 lists each preset's set). - Choose extraction per event with
directives.extract: a list of the stages to run (facts,entities,beliefs,episodes,understanding), or[]for none. It is a list, not a flag:"extract": falseis refused with a 422. It is stored with the event and honored on single, bulk and import writes (on a cluster, on every replica). - Write exact facts yourself as
content.kind: "triple"(subject, predicate, object). A triple is stored exactly as sent; no model reads or rewrites it. - Set
context.observed_atto when the thing happened, not when you sent it, for anything imported after the fact.
Volume and backfills
- Batch through
POST /v1/experience/bulk(up to 1,000 items per call; larger imports use/v1/import/jsonl). - For a backfill, use
?wait=captured: each item is acknowledged as soon as it is durable and indexing continues in the background. Use?wait=indexedwhen the next step must be able to recall what you just wrote. - The built-in connectors (Slack, Teams, GitHub, GitLab, Jira, Linear, Notion, Confluence, PagerDuty, Salesforce, ServiceNow, Zendesk, Intercom, Freshdesk, HubSpot, Discord, Google Workspace, tl;dv) already follow these rules.
Documents
Upload the file with POST /v1/blobs (raw bytes, the file's own
Content-Type) and reference it as blob_ref content. What reads it is
CORTEX_DOCUMENT_PROVIDER:
| Provider | Reads | Runs |
|---|---|---|
native | plain text, Markdown, CSV, HTML, JSON, XML | inside CortexDB |
tika (recommended on-prem) | PDF, Word, PowerPoint, Excel and more | an Apache Tika server you run next to CortexDB: CORTEX_DOCUMENT_API_URL=http://tika:9998 |
unstructured | complex layouts, tables, forms | the Unstructured API, hosted or self-hosted: CORTEX_DOCUMENT_API_URL, CORTEX_DOCUMENT_API_KEY |
Scanned PDFs (pictures of pages) need OCR: use a Tika image that includes
Tesseract. Store reference documents search-only ("extract": []) unless you
want facts extracted from them.
Keeping memory tidy
- Retention: give noisy scopes a lifetime with
PUT /v1/admin/retention/policies(for example tool logs kept 30 days). Sweeps run as a dry run until you apply them. - Forget and erasure:
POST /v1/erasures, andPOST /v1/forgetwith the memories' ids inselector.memory_ids, remove memories, the facts derived from them and their blobs, and record an audit row. A forget without ids and without"cascade": "redact_events"removes derived data only (facts, beliefs, episodes, understanding): its default cascade,derived_only, leaves every memory in place, on the hosted service too.
2. Embeddings
Embeddings turn text into numbers so CortexDB can find things by meaning, not only by exact words.
Pick one before you load real data
A data directory is pinned to the embedding provider, model and width it started with. Changing any of them later is a planned rebuild (Embedding Migration Runbook): while it runs, recall uses keywords only. Choose deliberately, then keep the setting.
Keep the key set. The Docker image refuses to start without its embedding key, and a data directory refuses a different provider than the one it is pinned to. A bare binary started with no embedding endpoint or key falls back to test vectors with a warning: never load real data that way.
Options
| Provider | Settings | Good for |
|---|---|---|
| OpenAI (default) | CORTEX_EMBEDDING_API_KEY or OPENAI_API_KEY; model text-embedding-3-small, 1536 dims | best quality for the effort, cheap, nothing to run |
| Any OpenAI-compatible endpoint (Azure OpenAI, OpenRouter, LiteLLM, vLLM, TEI) | CORTEX_EMBEDDING_URL, CORTEX_EMBEDDING_MODEL, CORTEX_EMBEDDING_DIMS, CORTEX_EMBEDDING_API_KEY | keeping data in your own cloud account; test a write and a recall before loading data |
| Cohere | CORTEX_EMBEDDING_PROVIDER=cohere, COHERE_API_KEY | teams already on Cohere |
| Ollama (local) | CORTEX_EMBEDDING_PROVIDER=ollama, CORTEX_EMBEDDING_URL=http://ollama:11434, CORTEX_EMBEDDING_MODEL (for example nomic-embed-text), CORTEX_EMBEDDING_DIMS matching the model | data that must never leave your network |
CORTEX_EMBEDDING_DIMS must equal the model's output width (768 for
nomic-embed-text, 1024 for mxbai-embed-large), and a data directory built
at one width refuses to start at another.
Local or API?
- API when policy allows the text to leave your network: better quality per dollar, no hardware, no operations.
- Local (Ollama) when it may not. Plan a GPU for large volumes (CPU works for small ones), expect somewhat lower quality than the best hosted models, and run it as a service CortexDB reaches over the network.
- Either way, measure on your own data: write a few hundred real items and check that recall returns what you expect before loading everything.
3. What can run inside your network
| Part | Hosted API | Inside your network |
|---|---|---|
| Storage, indexes, search | — | always (CortexDB itself) |
| Embeddings | OpenAI, Azure OpenAI, Cohere | Ollama, or any OpenAI-compatible server |
| Enrichment (fact extraction), optional | OpenAI, Anthropic, Google, Bedrock via a proxy | Ollama or an OpenAI-compatible server (CORTEX_ENRICHMENT_URL) |
Answers (/v1/answer), optional | Anthropic (default), OpenAI, Google, Bedrock | Ollama (CORTEX_ANSWER_PROVIDER=ollama) |
| Reranking, optional | Cohere | a local ONNX cross-encoder (CORTEX_RERANKER_MODEL_PATH) or a Cohere-shaped server you host (CORTEX_RERANKER_URL) |
| Documents | Unstructured API | Apache Tika, or self-hosted Unstructured |
A fully air-gapped install runs every row on the right. Recall
(/v1/recall) needs only embeddings; enrichment, answers and reranking are
optional.
4. Reusing answers
For repeated questions, turn on the answer cache with
CORTEX_ANSWER_CACHE=1. A repeated question from the same caller, in the same
scope and with the same options, is answered from the stored response instead
of running recall and the model again. The cache never serves an answer that
may be out of date:
- new memory written in the scope invalidates the stored answer;
- any forget or erasure invalidates it;
- any permission change invalidates it;
- a question without a
question_dateis only reused on the same (UTC) day; - answers that ran with part of their evidence missing are never stored;
- entries expire after
CORTEX_ANSWER_CACHE_TTL_SECS(default five minutes, at most nine).
The x-cortex-answer-cache response header says hit, miss or bypass.
Send Cache-Control: no-cache to force a fresh answer (it then replaces the
stored one), or no-store to keep a request out of the cache. Streaming
requests are always answered fresh. The cache is in memory on each server and
is never written to disk or replicated: a restart empties it. On a cluster
each data node keeps its own (set CORTEX_ANSWER_CACHE in the node's
[engine] table), only a shard's leader answers, and after a leader change
the new leader starts with an empty cache. The cluster ingress passes
Cache-Control through to the data node.
Details: API Reference §8.4.1.
5. Staying current
-
One-click updates (a single-node Docker Compose deployment only): run the updater sidecar (
docker compose -f docker-compose.yml -f docker-compose.updater.yml up -d, withCORTEX_UPDATER_TOKENin.env). The admin console's Updates page then offers Update now. The sidecar:- checks the backup can be written while CortexDB keeps serving;
- backs up the data;
- swaps the container, keeping its settings;
- waits for the new version to report healthy and stay up;
- rolls back automatically if it does not, except after a one-way upgrade (below).
It never installs an older version than the one running, and it refuses a container that runs a cluster node (
cortexdb node): a cluster is upgraded node by node. See Production Deployment, "One-click updates". -
One-way upgrades. Some releases change the data directory so that the version before them cannot open it: 0.11.0's first start migrates a 0.10.x data directory (it keeps the old event log in
pre_stage2/), and takes longer on a large one. Upgrade 0.10.x to 0.11.0 by hand (below, and the release notes' "Upgrade"): a 0.10.x console cannot give the consent a one-way upgrade needs, so the updater from 0.11.0 on refuses it, and an older updater must not be used for it. For a later one-way release, the updater refuses the plain update, then goes on only when you confirm it (the Updates page asks) and only with its backup on; if the new version then fails, it stops it and does not start the old version on the migrated data: the way back is the backup (that release's notes). -
By hand:
- read the release notes;
- back up the data directory (
scripts/cold_backup.py); - pull the new image;
- start it with the same data volume and settings.
Settings carry over. The data carries over forward; for a one-way upgrade, going back is a restore from the backup, as the release notes describe.
-
The admin console is served by CortexDB itself at
http://<host>:3141/. It uses plain HTTP: put it behind your HTTPS proxy and keep it off the internet.
6. Launch checklist
- Embedding provider, model and dimensions chosen and tested on real data
-
CORTEX_API_KEYset to a strong secret; port not exposed publicly - Ingestion filters in place; tool output and documents sent search-only
-
idempotency_keyon every write; bulk with?wait=capturedfor backfills - Labels and scopes planned for the kinds of memory you store
- Retention policies for noisy scopes
- Document provider configured (Tika on-prem) if you ingest files
-
Backups scheduled (
scripts/cold_backup.py) and a restore tested - Update path chosen: updater sidecar or manual