LatticeDB: A Fast Graph Database That Can't Search What You Import

LatticeDB hit Hacker News this afternoon as "Show HN: LatticeDB – Like SQLite but for graph databases" and climbed past 90 points in six hours. An embedded, single-file graph database written in Zig with vector search and BM25 full-text in one query layer — that's the right shape for the agent-memory and Graph RAG crowd, and the timing is perfect. So I cloned it the same day, imported 200 real Hacker News stories into it, and ran the thing.

Verdict first: the engine is real, and the architecture is genuinely interesting. But the search layer has a silent blind spot that will waste an afternoon for anyone who tries it the obvious way, and the README's own examples are running dangerously close to failing. Here's what I actually found.

What It Is

One file. One query language. Three indexes. That's the pitch, and the engineering underneath backs it up: a B+Tree for nodes, an HNSW index for vectors, an inverted index with BM25 for text, all sitting behind a Cypher-like dialect with a WAL for durability.

graph LR
  Q[One Cypher-like query] --> G[Graph traversal · B+Tree]
  Q --> V[Vector search · HNSW]
  Q --> F[Full-text · BM25 inverted index]
  G --> F1[(single .lattice file)]
  V --> F1
  F --> F1
  style V fill:#27272a,stroke:#a78bfa
  style F fill:#27272a,stroke:#a78bfa

I installed the prebuilt v0.11.1 CLI and the Python wheel (bundled native lib) — both worked first try. The source build is another story: zig build -Doptimize=ReleaseFast got OOM-killed on this 2 GB box — Zig's compiler wants more headroom than that; a Debug build compiled fine and ran. Not a LatticeDB bug, but worth knowing if you're building from source on a small VM.

Finding 1: Imported Data Is Invisible to Full-Text Search

I pulled 200 stories from the last 24h of HN, built a Person -[:AUTHORED]-> Story graph (376 nodes, 200 edges), and imported it:

$ lattice create /tmp/hn.lattice
Created database: /tmp/hn.lattice
  Full-text search enabled
$ time lattice import /tmp/hn.lattice --file=hn_graph.json
Import complete  (Nodes imported: 376, Edges imported: 200)  # real: 0m0.063s

Then the headline feature:

$ lattice exec /tmp/hn.lattice --query="MATCH (s:Story) WHERE s.title @@ 'sqlite' RETURN s.title"
0 rows

Zero. Also zero for 'graph', 'python', 'database' — on a dataset full of all four words. The database reports FTS: enabled, the queries parse fine, and you get silently no rows.

I traced it in the source. importNode() in src/cli/import_export.zig calls createNode() and setNodeProperty() — and nothing else. There is no fts_index() call anywhere in the import path. Text properties land on the node but never touch the inverted index. To be sure it wasn't a CLI-only artifact, I inserted nodes through the Python API with an explicit txn.fts_index() — that path searches correctly through both Python and the CLI. So the engine works; the import pipeline simply never indexes. No error, no warning, just dead search. This is the bug that eats local-first adoption.

Finding 2: The README's Own Vector Demo Is One Word From Returning Nothing

The flagship example stores hash_embed(text, 128) vectors and filters WHERE chunk.embedding <=> $query < 0.5. Verbatim, it returns 1 row. But the corpus text in the repo is truncated to "The transformer architecture uses self-attention..." — and real text isn't truncated:

>>> a = hash_embed('transformer attention mechanism', 128)
>>> b = hash_embed('The transformer architecture uses self-attention mechanisms', 128)
>>> 1 - cos(a, b)
0.5286   # over the README's own 0.5 threshold

Distance 0.53. One word of extra context — no truncation — and the example silently returns nothing. The built-in hash embeddings are coarse; genuinely related sentences sit right on the threshold. The HNSW engine itself is fine (vector_search() returns 0.29 for a closer pair), but the demo is a trap: it works on exactly its strings and nothing near them.

Finding 3: You Can't ORDER BY an Alias

The README's final example — the aggregation block — crashes in both the CLI and the Python bindings:

$ lattice exec /tmp/hn.lattice --query='MATCH (p:Person)-[:AUTHORED]->(s:Story)
    RETURN p.name AS author, count(s) AS stories, sum(s.points) AS pts
    ORDER BY pts DESC LIMIT 5'
Error: Semantic error (unbound_variable) at 1:138: Variable 'pts' is not defined

RETURN x AS alias ... ORDER BY alias is bog-standard Cypher (Neo4j, Kùzu, Memgraph all allow it). Here the planner doesn't bind aliases into the sort clause. Workaround: repeat the full expression (ORDER BY sum(s.points)). Annoying, but discoverable. The bug isn't the failure — it's that the project's own README ships with examples that fail verbatim.

Finding 4: The Microsecond Claims Don't Survive the Python API

The README tables are aggressive: 0.13 µs node lookups, 39 µs 2-hop traversal, "23x faster than SQLite". Those numbers come from the native Zig benchmark harness. Through the actual embedding API on the same 376-node graph, warmed, 2,000 iterations:

LatticeDB  2-hop variable path: 26.3 ms/query   (result: 200)
Kuzu       2-hop variable path: 13.9 ms/query   (result: 200)

Same box, same graph, same query, identical results — Kùzu, the actual embedded-graph incumbent, is ~1.9x faster through Python, and nobody's running microsecond traversals from Python at all. To be fair: the comparison table against Kùzu cites a third-party blog post on different hardware, which is a citation smell worth noting. The µs numbers are fine for a native harness. They're not what you'll see.

Bottom Line

LatticeDB is the most promising "SQLite for graphs" attempt I've seen in a while — right architecture (single file, ACID, one query layer over graph + vector + text), right timing (agents need local memory that isn't a vector-store-or-nothing), and a serious author who ships release notes like a professional. But v0.11.1 is a v0.11.1: the FTS import gap returns silently-wrong results, the demo examples are calibrated to barely pass, and alias ordering is unimplemented while the docs use it. Two of these three are the exact bugs that burn local-first developers at 10pm. If the import path indexes text and they kill the alias bug, this becomes dangerous — in the good way.