Skip to main content
Three decisions, in the order you’ll hit them:
  1. Where each doc_type’s entities livestorage: file | index
  2. Which engine stores and searches themsearch.backend
  3. How search behaves — field search kind, boost, semantic_weight
The first is per doc_type and is the one people get wrong. The other two are per brain.

1. Where entities live: file vs index

Set on each doc_type, in doc_types/<name>.yaml:
Pick file when a human or agent authors the entity and you want the change reviewable: services, runbooks, products, policies. Curated knowledge belongs in git. Pick index when the data is generated, high-volume, or temporal: alerts, deployments, memories, anything a connector pulls on a schedule. Hundreds of rows churning through git helps nobody.
The trap: open-index index reconciles file-backed types from disk. If a doc_type is file and you wrote entities to it only via the DB, the next index run deletes them. Conversely, JSON files for an index type are ignored. Match how a type is written to how it’s declared.
Changing the policy later is fine, but move the data with it — switch to file and the existing DB rows will be wiped on the next index unless you export them to JSON first.

2. Which engine: SQLite vs OpenSearch

One key decides both storage and search:
There is no storage.backend. storage: only sets the SQLite file path. (Older brains that set storage.backend still load; it never did anything, and now logs a warning.)
The line is writers, not size. SQLite is single-writer: the moment a second agent (or a connector running while an agent writes) needs to write, move to OpenSearch. Entity count matters only for semantic search, where SQLite scans brute-force. Switch per environment without editing brain.yaml:
Or commit it, keeping secrets as ${ENV} refs resolved at connect time:
Running it in containers → Deployment.

Environment overrides

Empty values don’t override, so docker compose passing an unset variable through as "" leaves the file’s value alone.

Per field

  • syntactic — keyword and prefix matching. Right for names, owners, statuses, anything you’d filter on.
  • semantic — the field is embedded and matched by meaning, so “checkout is slow” finds an entity that says “latency spike at payment”. Right for prose.
  • none — stored but never searched. Right for opaque ids and blobs that would only add noise.
boost is a genuine multiplier. A match in a boost: 6 field outranks one in a boost: 1 field 6-to-1 — not approximately. The usual shape is a high boost on name, a moderate one on a summary, and 1 everywhere else.

Per brain

semantic_weight blends the two scores. The default 0.3 keeps keyword matches dominant and lets semantic similarity rescue queries that use different words than the text. Raise it if your users describe things rather than name them; set 0 to turn vectors off at query time.

Embeddings

Semantic search needs a provider. Without one the backends log a warning once and fall back to keyword-only — search still works, just less well.
Or point at any OpenAI-compatible API:
Re-embed after any change that invalidates existing vectors — enabling semantic search on a populated brain, switching models, or changing dimensions:
Changing dimensions on OpenSearch also needs the index recreated, since the knn_vector mapping is fixed at creation.

Worked examples

Local, single agent, curated knowledge — the default. SQLite, storage: file on everything, semantic on prose fields.
Shared team brain with connectors — OpenSearch (several writers), curated types in git, pulled types DB-only.
Exact-match lookup only — no embedding dependency at all:

Troubleshooting