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:
| Field | Default | Meaning |
|---|---|---|
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_dim | 384 | Must equal your embedding model's dimension. |
has_priority | false | Enable priority blending in the combined score. |
similarity_weight / priority_weight | 0.5 / 0.5 | Blend weights when has_priority is on. |
- Python
- Godot (GDScript)
- Unity (C#)
- Unreal (C++)
- C FFI
- Rust SDK
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
}))
@onready var kv: AtelicoKvStoreNode = $AtelicoKvStore
func _ready() -> void:
kv.kvstore_create(JSON.stringify({
"store_id": "npc-memory",
"db_path": "./data/npc-memory",
"embed_dim": 384,
}))
string cfg = JsonSerializer.Serialize(new {
store_id = "npc-memory",
db_path = "./data/npc-memory",
embed_dim = 384,
});
ulong storeId = engine.KvStore.Create(cfg);
const FString Cfg = TEXT(R"({
"store_id": "npc-memory",
"db_path": "./data/npc-memory",
"embed_dim": 384
})");
Atelico->CreateKvStore(Cfg);
uint64_t store_id = 0;
atelico_kvstore_create(engine,
"{\"store_id\":\"npc-memory\",\"db_path\":\"./data/npc-memory\",\"embed_dim\":384}",
&store_id);
use atelico_sdk::KvStoreConfig;
engine.kvstore().create_sync(
"npc-memory",
"./data/npc-memory",
KvStoreConfig { embed_dim: 384, ..Default::default() },
)?;
# Ok::<_, atelico_sdk::SdkError>(())
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.
- Python
- Godot (GDScript)
- Unity (C#)
- Unreal (C++)
- C FFI
- Rust SDK
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
var resp := JSON.parse_string(kv_engine.embed(JSON.stringify({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": "The guard saw a thief enter the keep at midnight.",
})))
var vector: Array = resp.data[0].embedding
// Two texts to a similarity score directly:
float sim = engine.Embeddings.Similarity(
"in-memory::sentence-transformers/all-MiniLM-L6-v2",
"the knight draws his sword", "a warrior unsheathes a blade");
// …or get the raw vectors for the KV store:
string r = engine.Embeddings.Embed(JsonSerializer.Serialize(new {
model = "in-memory::sentence-transformers/all-MiniLM-L6-v2",
input = new[] { "The guard saw a thief enter the keep at midnight." },
}));
// parse r["data"][0]["embedding"] into float[]
const FString Resp = Atelico->Embed(TEXT(R"({
"model": "in-memory::sentence-transformers/all-MiniLM-L6-v2",
"input": ["The guard saw a thief enter the keep at midnight."]
})"));
// parse Resp -> data[0].embedding into a TArray<float>
const char *resp = NULL;
atelico_embed(engine,
"{\"model\":\"in-memory::sentence-transformers/all-MiniLM-L6-v2\","
"\"input\":[\"The guard saw a thief enter the keep at midnight.\"]}",
&resp);
// parse resp -> data[0].embedding with your JSON lib
let vectors = engine.embeddings().embed_sync(
"in-memory::sentence-transformers/all-MiniLM-L6-v2",
&["The guard saw a thief enter the keep at midnight."],
)?;
let vector = &vectors[0]; // Vec<f32>, length 384
# Ok::<_, atelico_sdk::SdkError>(())
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.
- Python
- Godot (GDScript)
- Unity (C#)
- Unreal (C++)
- C FFI
- Rust SDK
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,
}]))
# NOTE: AtelicoKvStoreNode is query-only — it exposes create/query/count but
# not insert. Populate the store from another binding sharing the same db_path,
# or use AtelicoVectorMemoryStore (write_memory / query_memory) for in-engine
# writes. Once populated, kvstore_query below works against it.
string entries = JsonSerializer.Serialize(new[] { new {
id = "mem-001",
key_text = "The guard saw a thief enter the keep at midnight.",
key_embedding = vector, // float[]
facets = new { npc_id = "guard-01", location = "keep" },
priority = 0.8f,
}});
engine.KvStore.Insert("npc-memory", entries);
// Build the entries JSON (embedding array elided for brevity)
const FString Entries = FString::Printf(TEXT(R"([{
"id": "mem-001",
"key_text": "The guard saw a thief enter the keep at midnight.",
"key_embedding": [%s],
"facets": {"npc_id": "guard-01", "location": "keep"},
"priority": 0.8
}])"), *EmbeddingCsv);
Atelico->KvStoreInsert(TEXT("npc-memory"), Entries);
// entries_json built however you like; key_embedding is the vector from embed()
atelico_kvstore_insert(engine, "npc-memory", entries_json);
use atelico_sdk::KvEntry;
use std::collections::HashMap;
engine.kvstore().insert_sync("npc-memory", vec![KvEntry {
id: "mem-001".into(),
key_text: "The guard saw a thief enter the keep at midnight.".into(),
key_embedding: vector.clone(),
facets: HashMap::from([
("npc_id".into(), "guard-01".into()),
("location".into(), "keep".into()),
]),
values: HashMap::new(),
priority: Some(0.8),
}])?;
# Ok::<_, atelico_sdk::SdkError>(())
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).
- Python
- Godot (GDScript)
- Unity (C#)
- Unreal (C++)
- C FFI
- Rust SDK
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"])
var results: Array = JSON.parse_string(kv.kvstore_query("npc-memory", JSON.stringify({
"query_embedding": query_vector,
"query_text": "who broke into the castle?",
"facet_filters": {"npc_id": "guard-01"},
"limit": 5,
})))
for r in results:
print("%s sim=%.2f" % [r.key_text, r.similarity])
string q = JsonSerializer.Serialize(new {
query_embedding = queryVector, // float[]
query_text = "who broke into the castle?",
facet_filters = new { npc_id = "guard-01" },
limit = 5,
});
string resultsJson = engine.KvStore.Query("npc-memory", q);
const FString Q = FString::Printf(TEXT(R"({
"query_embedding": [%s],
"query_text": "who broke into the castle?",
"facet_filters": {"npc_id": "guard-01"},
"limit": 5
})"), *QueryEmbeddingCsv);
FString ResultsJson = Atelico->KvStoreQuery(TEXT("npc-memory"), Q);
const char *results = NULL;
atelico_kvstore_query(engine, "npc-memory", query_json, &results);
// results: JSON array of {id, key_text, similarity, priority, combined_score}
use atelico_sdk::KvQuery;
use std::collections::HashMap;
let results = engine.kvstore().query_sync("npc-memory", &KvQuery {
query_embedding: query_vector,
query_text: "who broke into the castle?".into(),
facet_filters: HashMap::from([("npc_id".into(), "guard-01".into())]),
vector_search_limit: 20,
limit: 5,
use_prefilter: true,
})?;
for r in &results {
println!("{} sim={:.2} combined={:.2}", r.key_text, r.similarity, r.combined_score);
}
# Ok::<_, atelico_sdk::SdkError>(())
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
facetsyou stored. Withuse_prefilter: truethey prune candidates before the ANN search (fast, good when a facet is very selective);falsefilters after (exact, better when facets match most rows). - Count:
kvstore_count(store_id, filter)returns the number of entries, optionally with a SQL-likeWHEREfilter (empty string = all). - Persistence: everything lives under
db_path. Restart the engine, callkvstore_createwith the samestore_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 deletedb_path).
Where to go next
- Embedding Models — pick a model, the
embed_dimit 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.rs—KvStoreConfig,KvEntry,KvQuery,KvResult, and thequeryreranker.atelico-sdk/src/kvstore.rs— the typed Rust SDK surface every binding wraps.atelico-ffi/src/lib.rs—atelico_kvstore_*C entry points.