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.
Where Postgres, DuckDB, and Neo4j hit a wall.
Each of these is a great database — for its job. None of them do this job: search by meaning, keyword, and relationship in one request; follow long chains of connections without slowing down; and remember what used to be true, not just what's true now — all inside one lightweight engine you can embed straight into your app.
| What matters | Postgres | DuckDB | Neo4j | mnestic |
|---|---|---|---|---|
| Where it runs | Needs a separate database server | Runs inside your app — but has no way to follow relationships | Needs a separate server (built on Java) | Runs inside your app, no server needed (though it can run as one, too) |
| Following chains of connections | Possible, but limited and slow once the chains get long | Not supported at all | Good at direct connections, weaker at general looping logic | Built in, and stays fast even on long, looping chains |
| Search by meaning + keyword + connections, together | Bolt-on add-ons (for meaning, for keyword) you wire together yourself | Similar bolt-on add-ons — and no relationship search at all | Has meaning-based search, but you still combine it with the rest yourself | One built-in request that combines all three and ranks the results for you |
| Remembering what used to be true | Not built in — you'd have to build this yourself | Not built in | Not built in | Built in — ask what it believed at any past moment, and see every correction it ever made |
These are structural differences, not a speed test — see the measured recall numbers further down for real benchmarks. Each column reflects that database's own built-in and official tooling (Postgres: pgvector + full-text search; DuckDB: its vector and full-text extensions; Neo4j: its native Cypher and vector index).
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.
Bitemporal time travel
Relations carry valid time and, since 0.10.0, an engine-stamped transaction time. Query the graph as it was, or as it was believed, at any point: memory you can rewind and audit, not just overwrite.
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 recall-focused wins below.
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.
The RocksDB options you set are the ones RocksDB now uses
Every BlockBasedTableOptions a host configured — block cache, block size, index/filter caching — was default-constructed away on each open, silently. The read cache reverted to RocksDB's 8 MB default and block_size to 4 KB; block_cache_size was ignored outright. Inherited from upstream, latent since the options-file path was introduced. Fixed in mnestic-rocks 0.1.10 — and it means every read-path benchmark run before it, ours included, measured a slower engine than mnestic actually is.
→ block_size 4 KB → configured 16 KB · index/filter blocks now cached · the read cache was 8 MB, not what you asked for
A float in a validity is now an error
Validity timestamps are integer microseconds; now() and parse_timestamp() return float seconds. The engine coerced one into the other silently, so a fact meant for 2024 was written — permanently — at 1970, visible only under time travel. Three of the four sites are verbatim upstream code, write path included: the bug is as old as Cozo's time travel. The one caller of the broken idiom we found was upstream's own test.
→ the write path stored 1970 and returned Ok · reads returned zero rows and no error
Six inherited bugs, found and named
A line-by-line audit of the inherited engine found full-text postings leaking on every in-place update, a transaction API that reported success for a failed commit, and change events fired for writes that never landed. All predate the fork. ::reindex is the repair path.
→ 55% BM25 score error, fixed · 200 {"ok": true} on a failed commit, fixed
Fill a context window straight from the graph
BudgetedTraversal expands cheapest-first from what search found, under a hard budget of distinct nodes — deterministic, gated mid-expansion, interruptible. One call replaces a host-side BFS loop.
→ 2–4× vs a production host-side BFS · one call replaces ~2·depth round-trips
Cache a graph once, reuse it across queries
::graph create names an in-memory adjacency that twelve graph algorithms reuse instead of rebuilding it on every call — always fresh, never stale under write churn.
→ 400k edges: ConnectedComponents 16× · PageRank 15× · ClusteringCoefficients 3×
Naive join order stops being pathological
A deterministic greedy pre-pass reorders join atoms by fewest new variables before evaluation — exactly the shape an LLM agent tends to author. Results are unchanged; hand-tuned plans stay byte-identical.
→ 54.5× on the repro · N³ → N² · default on, results unchanged
Queries you can always stop
::kill and :timeout now interrupt a query that's genuinely stuck, checked every 4096 pulls inside even a long single-rule join.
→ ::kill / :timeout interrupt mid-join · deadline = min of the budgets set
Answers that carry their evidence
Provenance-semiring aggregates return the k best derivations per answer, each with its exact evidence chain. :reconcile keeps derived results consistent when base facts are retracted.
→ min_cost_k top-k proofs · register_custom_aggr · :reconcile belief revision
Full-text search that ranks correctly
Keyword search now scores with Okapi BM25, the ranking standard behind modern search engines, instead of raw term counts.
→ fused recall climbs 0.75 → 0.954 on a 40k-chunk corpus
Key lookups skip the full scan
Filtering by an exact key now compiles to a direct keyed seek instead of scanning every row — constant-time instead of a full table walk.
→ ~28× faster single-row primary-key lookups
Full release history → — every fork release, 0.8.0 through 0.13.0.
What did we believe
at time T about period Y?
Valid time says when a fact is true in the world. Transaction time says when the database learned it — and 0.10.0 adds it in-engine, stamped at commit by a crash-safe monotonic clock. Reproduce last Tuesday's answer exactly. Audit every correction. Tell “the world changed” apart from “we were wrong.” No other embedded engine serves this natively.
- tt is engine-assigned, never user-set — nobody can backdate what the system knew
- ::history / ::history_gc / ::evict manage the record's lifecycle; GC keeps as-of reads exact behind a persisted floor
- Current-belief reads measured within ~4–12% of a single-axis relation; opt-in, zero cost if unused
1# opt in: valid time + an engine-stamped transaction time2:create beliefs {claim, vld: Validity, tt: TxTime => conf: Float}3 4# writes are stamped by a crash-safe commit clock — tt is never user-set5?[c, v, conf] <- [['q3 pipeline is healthy', '2026-09-01', 0.35]]6:put beliefs {c, v => conf}7 8# time travel on BOTH axes:9# what did we believe on June 30 about September?10?[conf] := *beliefs{claim: 'q3 pipeline is healthy', conf11 @ (vt: '2026-09-01', tt: '2026-06-30')}12 13# the audit trail: every belief, correction, and cessation14::history beliefs [['q3 pipeline is healthy']]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})?;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.
- Graph proximity is the single largest effect in the run: the graph-less LanceDB lands far below on recall alone.
- mnestic fuses all three signals from one embedded store in a single transactional call; SQLite, DuckDB and Kuzu fuse in app code, and LanceDB still needs a second system for graph.
- mnestic's indexes update in the same transaction as the write, so a new memory is recallable on every signal immediately. DuckDB's full-text index is a build-time snapshot, unsearchable by keyword until rebuilt.
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, in context
mnestic's one-call 3-way fusion runs at ~42 ms p50, about 4× faster than hand-decomposing the same query, and it's the only call here fusing three signals at once; LanceDB's native call covers just two. It isn't the lowest absolute latency at this scale, but re-measured on the RocksDB backend it actually runs, with real sentence-transformer embeddings, the tail improves (p99 181 ms vs 258 on the test wheel) — matching the best indexed engine on quality while fusing a signal the others can't.
† DuckDB's full-text index is a build-time snapshot: 0% fused read-your-writes on the keyword leg until a rebuild. *SQLite's recall reflects an exact brute-force scan, not an indexed ANN search. Small scale (40k chunks, 1,000 queries, k=10, 2-hop graph), 2026-05-31, macOS arm64; numbers are hardware-specific. Recall@10 uses synthetic embeddings on the SQLite wheel; latency is separately validated on the RocksDB backend with real sentence-transformer embeddings. Kuzu is excluded: its extension host has been offline since the project's Oct-2025 archival. Full methodology · raw results.
What people build with it.
The queries above aren't syntax demos — they're the core of three problems people run into once their app needs more than a single database can offer.
Agent memory that survives the session
An AI agent has a conversation, then a week later needs to remember what was decided — by the gist of it, by exact keywords, and by what it's connected to. Normally that means juggling several databases and stitching the results together by hand, and the combination goes stale the moment something new comes in. With mnestic, that's a single request — the one shown above — that runs against everything the agent has ever stored, including what it wrote a second ago.
An audit trail you can actually trust
Say a system judged a transaction "low risk" back in June, but by September that call looks wrong. Was the system mistaken, or did the facts change after the fact? Postgres, DuckDB, and Neo4j have no built-in way to answer that — you'd have to build your own history-tracking on top. mnestic keeps a permanent record of both what was true and when the database learned it, so the query above can replay exactly what it knew and when, and nobody can quietly rewrite that history.
Relationship graphs without a second database
Fraud rings share things — a device, an address, a payment method — often several steps removed from the account that got flagged. Finding those hidden connections is a job for a graph database. But running Neo4j alongside your main database means keeping two systems in sync. mnestic finds those same multi-step connections inside the database that already holds the rest of your data — nothing extra to install, sync, or maintain.
Add it to your project
1# default = in-memory + SQLite backends2cargo add mnestic3 4# or, with the RocksDB backend:5# mnestic = { version = "0.13.0", 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.