Skip to main content
Version: 0.12

Vector Search & Memory (KV Store)

The KV store is a LanceDB-backed vector store for semantic memory and RAG — NPC long-term memory, lore lookups, "have I seen something like this before?" It stores entries keyed by an embedding vector and retrieves the nearest ones to a query vector by cosine similarity.

This page covers the in-process SDK KV store (Godot, Unity, Unreal, C FFI, Python, Rust). For lexical (BM25) and blended retrieval, see Hybrid & Lexical Search. For the models that produce the vectors, see Embedding Models.

The model: bring your own vector

The in-process KV store is vector-in, vector-out. You embed text yourself (via the embeddings API) and hand the store the vector; the store does the nearest-neighbor search. A typical write/read cycle:

text ──embed()──▶ vector ──kvstore_insert(key_embedding)──▶ store
query text ──embed()──▶ query vector ──kvstore_query(query_embedding)──▶ ranked results

This keeps embedding and storage decoupled: you can batch-embed, cache vectors, or use any model you like — the store only cares about the dimensionality.

:::note Auto-embedding (HTTP server) The HTTP server's KV store endpoints accept an embedding_model and embed text for you. The in-process bindings documented here do not — they take vectors directly. Match your store's embed_dim to your model's output dimension (MiniLM / bge-small / granite-small → 384, bge-base / granite-english → 768). :::

Step 1: Create a store

The config is JSON (a typed struct in Rust). Key fields:

FieldDefaultMeaning
store_id"default"Unique store name; reused by every later call.
db_path"./data"On-disk LanceDB directory. Persisted across restarts.
table_name"entries"Table within the database.
embed_dim384Must equal your embedding model's dimension.
has_priorityfalseEnable priority blending in the combined score.
similarity_weight / priority_weight0.5 / 0.5Blend weights when has_priority is on.
import atelico, json
engine = atelico.Engine()

engine.kvstore_create(json.dumps({
"store_id": "npc-memory",
"db_path": "./data/npc-memory",
"embed_dim": 384, # all-MiniLM-L6-v2 → 384
}))

Step 2: Embed your text

Get a vector for each piece of text you want to store or search. The embeddings API returns one vector per input; pull data[i].embedding.

resp = json.loads(engine.embed(json.dumps({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": ["The guard saw a thief enter the keep at midnight."],
})))
vector = resp["data"][0]["embedding"] # list[float], length 384

Step 3: Insert entries

Each entry carries an id, the key_text (kept for display/reranking), its key_embedding (the vector from Step 2), optional facets (exact-match metadata for filtering), and an optional priority.

engine.kvstore_insert("npc-memory", json.dumps([{
"id": "mem-001",
"key_text": "The guard saw a thief enter the keep at midnight.",
"key_embedding": vector,
"facets": {"npc_id": "guard-01", "location": "keep"},
"priority": 0.8,
}]))

Step 4: Query by vector

Embed the query text (Step 2), then search. vector_search_limit is the ANN candidate pool (set it higher than limit to give the reranker room); facet_filters restrict to exact metadata matches; use_prefilter applies those filters before the ANN search (faster) vs after (exact).

results = json.loads(engine.kvstore_query("npc-memory", json.dumps({
"query_embedding": query_vector,
"query_text": "who broke into the castle?", # for the reranker / logs
"facet_filters": {"npc_id": "guard-01"},
"vector_search_limit": 20,
"limit": 5,
"use_prefilter": True,
})))
for r in results:
print(r["key_text"], r["similarity"], r["combined_score"])

Each result carries id, key_text, similarity (cosine), priority (if enabled), and combined_score (the blended ranking used for ordering).

Filtering, counting, and lifecycle

  • Facet filters are exact-match string equality on the facets you stored. With use_prefilter: true they prune candidates before the ANN search (fast, good when a facet is very selective); false filters after (exact, better when facets match most rows).
  • Count: kvstore_count(store_id, filter) returns the number of entries, optionally with a SQL-like WHERE filter (empty string = all).
  • Persistence: everything lives under db_path. Restart the engine, call kvstore_create with the same store_id/db_path, and your entries (and any FTS index) are still there.
  • Destroy: kvstore_destroy(store_id) removes the store from memory (the on-disk table is left intact unless you delete db_path).

Where to go next

  • Embedding Models — pick a model, the embed_dim it implies, and quantized (GGUF) options for on-device use.
  • Hybrid & Lexical Search — add a BM25 full-text index and blend keyword + vector scores for name-and-intent retrieval.

Where to look in the code

  • atelico-search/src/kv_store.rsKvStoreConfig, KvEntry, KvQuery, KvResult, and the query reranker.
  • atelico-sdk/src/kvstore.rs — the typed Rust SDK surface every binding wraps.
  • atelico-ffi/src/lib.rsatelico_kvstore_* C entry points.