embedded Datalog.
performant graphs.
a substrate for machine memory.
mnestic is a transactional relational–graph–vector database that speaks Datalog, the same engine that called itself “the hippocampus for AI,” now actively maintained and tuned as the recall layer for agents.
MPL-2.0 · Rust · RocksDB / SQLite / in-memory backends
1# every memory reachable from a seed, then the 12 nearest by meaning2recall[to] := *recalls{ from: $seed, to }3recall[to] := recall[via], *recalls{ from: via, to }4 5?[memory, dist] :=6 recall[memory],7 ~memory:embedding{ memory |8 query: $cue, k: 12, ef: 80, bind_distance: dist9 }10:order dist11:limit 12Why a fork
CozoDB went quiet after December 2024. The design is too good to let drift. So we forked it, openly, under MPL-2.0, and pointed it at one job: being the memory an agent can trust. Every divergence is documented, every original copyright preserved.
The engine you inherit
Datalog, not SQL
Queries compose piece by piece. Recursion is first-class, and runs faster than the SQL equivalent. A safe subset of aggregations is allowed inside recursion.
Relational · graph · vector
One engine, one model. The relational algebra handles graph structure implicit several levels deep, with no shoehorning your data into a labelled-property graph.
Embeddable like SQLite
Runs in-process (no server, no setup), yet scales to large data and high concurrency, and can also run client-server when you want it to.
Vector search (HNSW)
Disk-resident HNSW indices unify with Datalog: search by meaning inside a recursive query, filter with the rest of your rules, all in one pass.
Full-text & near-dup
Built-in full-text search and MinHash-LSH for near-duplicate detection: the keyword and dedup legs of retrieval, native to the engine.
Time travel
Relations can carry validity time. Query the graph as it was at any historical point: memory you can rewind, not just overwrite.
Faster, safer, and built for recall.
The query language and semantics are unchanged. What changed is the stuff that bites you in production (lock contention, full scans, seven-rule retrieval pipelines) and the things agents actually need.
Flat in-RAM parallel index builds — 15× faster ::hnsw create
The bulk build now constructs the graph in flat, integer-indexed memory (contiguous vector slab + per-node adjacency, the hnswlib/pgvector layout) with parallel insertion under per-node locks, then serialises once into the unchanged on-disk format. ::fts create drops a redundant second tokenisation pass and tokenises in parallel. Same search path, same incremental maintenance, still non-blocking.
→ 40k × 384-dim: 294 s → 19 s synthetic · 89.1 s → 8.1 s real-embedding corpus (RocksDB), recall@10 unchanged
Plain-snapshot reads — read-only scripts skip the transaction
On RocksDB, read-only scripts no longer open a pessimistic transaction: they read through a plain snapshot (the standard MVCC read pattern), so reads structurally cannot wait on writer locks. HNSW search batches neighbour vector fetches through one RocksDB MultiGet per expansion step. Also: ::describe was documented and implemented upstream but never reachable from the grammar — wired in, with a read-only guard.
→ keyed point reads p50 28.5 → 23.9 µs (−16%) · p99 −19%
Per-leg fusion detail — recall that explains itself
ReciprocalRankFusion(detailed: true) and HybridSearch::detailed return one row per (item, contributing leg): which legs surfaced each result, the within-leg rank the fusion used, and the leg's raw score. The fused score reconstructs exactly as Σ 1/(k + rank) — the mechanism behind “why was this retrieved” surfaces. Also fixes a 0.8.3 defect where the durable avgdl counter made concurrent writers to FTS-indexed relations contend on one key.
→ every fused score decomposes into its legs · Python: detailed=True
Native 3-way fused recall
Graph proximity is now a typed leg of hybrid_search: add GraphLeg seeds and a bounded-hop edge relation, and vector, keyword, and graph signals fuse in one call, one transaction. No hand-written recursion, no app-side stitching.
→ all 3 signals in one call, ~4× faster than decomposing it by hand
BM25-correct full-text search
The default ::fts scorer is now Okapi BM25 with term-frequency saturation and document-length normalization, and OR-disjunction sums per-term contributions. avgdl is an O(1) read (process-cached since 0.8.4), not a per-query index scan. (tf and tf_idf stay selectable for byte-identical upstream scoring.)
→ fused recall jumps 0.75 → 0.954, cold-start tail cut ~10× (40k chunks)
Non-blocking HNSW index builds
::hnsw create no longer holds the base relation's write lock while it constructs the graph. The graph is built off-lock under a snapshot and bulk-published via SstFileWriter; mutations during the build reconcile under a brief final lock.
→ 90k reads served (slowest 0.8 ms) during a build that previously blocked them all
One-call hybrid retrieval
DbInstance::hybrid_search runs HNSW + FTS + optional graph traversal, fuses them with Reciprocal Rank Fusion, and optionally diversifies with MMR, all in a single typed call. Previously ~7 hand-written Datalog rules.
→ RRF + MMR fusion · one typed call replaces ~7 hand-written rules
HNSW builds ~3× faster
The build no longer round-trips the whole graph through the transaction's write-batch overlay. The resulting index is byte-identical to before, just built in a third of the time.
→ 20k × 128-dim: 135 s → 43.6 s (release, measured)
Equality pushdown
*rel[k, ..], k == <value> now compiles to a keyed stored_prefix_join instead of a full scan. Numeric equalities keep cross-type op_eq semantics, so nothing silently changes meaning.
→ ~28–29× faster single-row primary-key lookups (5k rows, measured)
ULID identifiers
rand_ulid() and ulid_timestamp() give you lexicographically-sortable, time-ordered keys, ideal for append-only memory streams you scan by recency.
→ lexicographically sortable · time-ordered scans
Correctness, inherited & added
Forked 30 commits ahead of the published 0.7.6, including the stored_prefix_join correctness fix. Plus a parser fix for identifiers that begin with a keyword (nullable_column, trueValue).
→ upstream fixes for free + a slimmer dependency graph
Hybrid retrieval,
one typed call.
Vector similarity, keyword match, and graph proximity are three different signals about “what should I remember right now.” As of 0.8.3 mnestic fuses all three natively: a typed graph leg joins the vector and keyword legs in one call, combined by Reciprocal Rank Fusion and de-duplicated by Maximal Marginal Relevance.
- Graph proximity is a typed GraphLeg: bounded-hop, ranked by min distance
- Query vector, text & seeds passed as params, never string-interpolated
- One call, one transaction; generated CozoScript inspectable via hybrid_search_script
1use cozo::{DbInstance, GraphLeg, HybridSearch, MmrParams};2 3// One typed call: HNSW + FTS + graph proximity, fused natively4// with Reciprocal Rank Fusion, then MMR-diversified.5let recalls = db.hybrid_search(&HybridSearch {6 relation: "memory".into(),7 vector_index: "embedding".into(),8 query_vector: cue, // Vec<f32> from your embedder9 vector_k: 24,10 ef: 80,11 fts_index: "summary_fts".into(),12 query_text: "pricing decision",13 fts_k: 24,14 // graph leg: expand 2 hops from a seed over *recalls,15 // rank by min hop distance — fused in the same call.16 graph_legs: vec![GraphLeg {17 edge_relation: "recalls".into(),18 seeds: vec![seed.into()],19 max_hops: 2,20 ..GraphLeg::default()21 }],22 rrf_k: 60.0,23 mmr: Some(MmrParams { lambda: 0.5, k: 12, embedding_col: "embedding".into() }),24 ..HybridSearch::default()25})?;Built to be fast
upstream figures · 2020 Mac mini · RocksDB backend
mixed read / write / update transactions
read-only queries
on a 1.6M-vertex, 31M-edge graph
at OLTP load
Backup ≈ 1M rows/s · restore ≈ 400K rows/s · PageRank on 1.6M vertices ≈ 30 s. mnestic keeps these and adds the fork wins above.
One engine for all three signals.
The task is fusing vector, keyword, and graph proximity into one ranking. Raw latency isn't what separates the field at this scale. Three structural things are, and mnestic is the only embedded engine here that gets all three right.
It has a graph signal at all
Graph proximity is correlated but distinct from vector and keyword: drop it and you lose recall the other two can't recover. It's the single largest effect in the run, with the graph-less engines (LanceDB) landing far below. This is why graph-augmented retrieval exists.
One store, one call, no glue
mnestic serves all three signals from one embedded store and fuses them in a single transactional call. SQLite, DuckDB and Kuzu keep them in one process but fuse in app code (three queries + a hand-rolled RRF); LanceDB fuses natively but needs a second system for graph.
Read-your-writes on every signal
An agent writes a memory and must recall it immediately. mnestic's indexes update in the same transaction, giving 100% fused read-your-writes. DuckDB's full-text index is a build-time snapshot: a new memory is unsearchable by keyword (0%) until a rebuild. A static-corpus drag race hides this entirely.
On quality, mnestic hits recall@10 of 0.954, level with DuckDB's 0.957 and far above the graph-less LanceDB (0.501). It's the only engine here that fuses all three signals in one transaction:
| Engine | recall@10 | Signals | Fusion | Fused read-your-writes |
|---|---|---|---|---|
| mnestic | 0.954 | vec · FTS · graph | native · one call | 100% |
| duckdb 1.5.3 | 0.957 | vec · FTS · graph | app-side glue | 0% full-text † |
| sqlite 0.1.9 | 1.000* | vec · FTS · graph | app-side glue | 100% |
| lancedb 0.33 | 0.501 | vec · FTS | native · no graph | n/a — no graph |
The native call is the fast path
mnestic's one-call 3-way fusion runs at ~42 ms p50, faster than DuckDB's decomposed path and about 4× faster than hand-decomposing it yourself. It's the only engine here that fuses three signals in a single call; LanceDB's native call covers just two.
Latency, in context
mnestic isn't the lowest absolute latency at this scale, but the numbers hold up off the test wheel. Re-measured on the RocksDB backend it actually runs, with real sentence-transformer embeddings, the decomposed path's tail falls (p99 181 ms vs 258 on the wheel) and the native 3-way call stays around 40 ms. What holds is quality and capability: matching the best indexed engine while fusing a signal the others can't.
† DuckDB's full-text index is a build-time snapshot — its fused read-your-writes is 99% overall but 0% for the keyword leg until a rebuild. Source: the mnestic-benchmarks hybrid suite, summarized in the 0.8.5 changelog. Small scale (40k chunks, 10k entities, 50k edges, dim 384) · 1,000 queries, k=10, 2-hop graph · 2026-05-31 · macOS arm64. Numbers are hardware-specific. Recall@10 is the synthetic text-derived-embedding run on the SQLite-backed wheel, where the vector signal is meaningful by construction; latency is additionally validated on the RocksDB backend with real sentence-transformer embeddings. *SQLite's recall reflects an exact brute-force KNN scan (no ANN index), not a like-for-like indexed search. Kuzu did not complete (extension host offline since its Oct-2025 archival).
Add it to your project
1# default = in-memory + SQLite backends2cargo add mnestic3 4# or, with the RocksDB backend:5# mnestic = { version = "0.8", features = ["storage-rocksdb"] }1use cozo::DbInstance;2 3let db = DbInstance::new("mem", "", "")?;4db.run_default("?[x] := x in [1, 2, 3]")?;1pip install mnestic # the engine (abi3 wheels)2pip install langchain-mnestic # LangChain vector store3pip install llama-index-vector-stores-mnesticNaming, on purpose
The published crate is mnestic, but the importable library name stays cozo. Every use cozo::… in your existing code, and in downstream crates, keeps working unchanged. A drop-in, not a rewrite.
Credit where it’s due
mnestic is not the official CozoDB and is not affiliated with or endorsed by its authors. All credit for the original design belongs to Ziyang Hu and the Cozo Project Authors.