Embedded single-file knowledge graph database with vector search and full-text search for AI/RAG apps
668
stars
485
commits
Zig
primary language
Aug 31, 2026
updated
Embedded property-graph database with native vector and full-text indexing.
LatticeDB is a single-file local database for connected, semantic, and textual data. It lets you traverse relationships, run vector similarity search, and do BM25 full-text search over the same dataset in one engine and one query layer. It is designed for relationship-heavy workloads on a single machine, with zero-config operation and an embedded single-writer model.
LatticeDB is an embedded, single-file graph database that lets local applications query the same data by relationship, semantics, and text, then consume durable graph and application events from the same file. Workloads like Graph RAG, agent memory, and local knowledge tools are examples built on those primitives, not the definition of the engine.
// Find chunks similar to a query, traverse to their document, then to the author
MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query_vector < 0.3
AND doc.content @@ "neural networks"
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query_vector
LIMIT 10
CLI
curl -fsSL https://raw.githubusercontent.com/jeffhajewski/latticedb/main/dist/install.sh | bash
Python
pip install latticedb
Published wheels are expected to bundle liblattice on supported platforms. Source installs can also bundle a staged native library during wheel builds with LATTICE_BUNDLE_LIB_DIR=/path/to/lib.
TypeScript / Node.js
npm install @hajewski/latticedb
Published package tarballs are expected to bundle liblattice on supported platforms. Source checkouts can stage the native library into the package with LATTICE_BUNDLE_LIB_DIR=/path/to/lib npm run bundle:native.
Java
Requires JDK 21+. See bindings/java/README.md for the Maven build, which compiles the JNI bridge and stages liblattice from zig-out/lib. A runnable knowledge-graph example is in bindings/java/src/main/java/io/latticedb/examples.
Go
See bindings/go/README.md for the current cgo workflow. The default consumer path uses installed pkg-config metadata; in-repo development can use -tags repolocal against zig-out/lib.
There is also a runnable graph/vector/text retrieval example in examples/go.
Recent binding-surface cleanups moved embedding helpers into dedicated modules and subpackages. See docs/client_api_migration.md for the preferred imports and current compatibility aliases.
A complete example: create a small knowledge graph with documents and authors, store embeddings, index text, then query across all three search modes.
The examples use the built-in hash_embed / hashEmbed / HashEmbed helper so they run with no
external service. It is a deterministic placeholder, not a semantic embedding: similar text does not
produce nearby vectors, so a distance threshold is arbitrary and a similarity query may match nothing.
Use a real embedding model for anything where the results should mean something — see
Working with Embeddings.
from latticedb import Database
from latticedb.embedding import hash_embed
with Database("knowledge.db", create=True, enable_vectors=True, vector_dimensions=128) as db:
# --- Build the graph ---
db.create_node_fts_index("Chunk", "text")
with db.write() as txn:
# Create authors
alice = txn.create_node(labels=["Person"], properties={"name": "Alice", "field": "ML"})
bob = txn.create_node(labels=["Person"], properties={"name": "Bob", "field": "Systems"})
txn.create_edge(alice.id, bob.id, "COLLABORATES_WITH")
# Create documents with chunks
for title, text, author in [
("Attention Is All You Need", "The transformer architecture uses self-attention...", alice),
("Scaling Laws for LLMs", "We find that model performance scales predictably...", alice),
("Log-Structured Merge Trees", "LSM trees optimize write-heavy workloads...", bob),
]:
doc = txn.create_node(labels=["Document"], properties={"title": title})
chunk = txn.create_node(labels=["Chunk"], properties={"text": text})
# The chunk's text property is already indexed by the declaration above.
txn.set_vector(chunk.id, "embedding", hash_embed(text, dimensions=128))
txn.create_edge(chunk.id, doc.id, "PART_OF")
txn.create_edge(doc.id, author.id, "AUTHORED_BY")
txn.commit()
# --- Query: vector search + text match + graph traversal ---
results = db.query("""
MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query < 0.5
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query
LIMIT 5
""", parameters={"query": hash_embed("transformer attention mechanism", dimensions=128)})
for row in results:
print(f"{row['doc.title']} by {row['author.name']}")
# --- Full-text search ---
for r in db.fts_search("Chunk", "text", "self-attention transformer"):
print(f"Node {r.node_id}: score={r.score:.4f}")
# --- Aggregations ---
stats = db.query("""
MATCH (doc:Document)-[:AUTHORED_BY]->(p:Person)
RETURN p.name, count(doc) AS papers
ORDER BY papers DESC
""")
for row in stats:
print(f"{row['p.name']}: {row['papers']} papers")
import { Database } from "@hajewski/latticedb";
import { hashEmbed } from "@hajewski/latticedb/embedding";
const db = new Database("knowledge.db", {
create: true,
enableVectors: true,
vectorDimensions: 128,
});
await db.open();
// Build a graph
await db.write(async (txn) => {
const alice = await txn.createNode({
labels: ["Person"],
properties: { name: "Alice", field: "ML" },
});
const doc = await txn.createNode({
labels: ["Document"],
properties: { title: "Attention Is All You Need" },
});
const chunk = await txn.createNode({
labels: ["Chunk"],
properties: { text: "The transformer architecture uses self-attention..." },
});
await txn.setVector(chunk.id, "embedding", hashEmbed("transformer self-attention", 128));
await txn.createEdge(chunk.id, doc.id, "PART_OF");
await txn.createEdge(doc.id, alice.id, "AUTHORED_BY");
});
// Query across vector search + graph traversal
const results = await db.query(
`MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query < 0.5
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query
LIMIT 5`,
{ query: hashEmbed("attention mechanism", 128) }
);
for (const row of results.rows) {
console.log(`${row["doc.title"]} by ${row["author.name"]}`);
}
await db.close();
db, err := latticedb.Open("knowledge.db", latticedb.OpenOptions{
Create: true,
EnableVectors: true,
VectorDimensions: 128,
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.CreateNodeFTSIndex("Chunk", "text"); err != nil {
log.Fatal(err)
}
err = db.Update(func(tx *latticedb.Tx) error {
node, err := tx.CreateNode(latticedb.CreateNodeOptions{
Labels: []string{"Chunk"},
Properties: map[string]latticedb.Value{"text": "The transformer architecture uses self-attention..."},
})
if err != nil {
return err
}
embedding, err := latticedb.HashEmbed("The transformer architecture uses self-attention...", 128)
if err != nil {
return err
}
if err := tx.SetVector(node.ID, "embedding", embedding); err != nil {
return err
}
return tx.SetProperty(node.ID, "text", "The transformer architecture uses self-attention...")
})
if err != nil {
log.Fatal(err)
}
import io.latticedb.*;
import java.util.List;
import java.util.Map;
try (Database db = Database.open("knowledge.db",
OpenOptions.defaults().create(true).enableVectors(true).vectorDimensions(128))) {
// The Chunk.text property is what full-text search reads.
db.createNodeFtsIndex("Chunk", "text");
// Build a graph
try (Transaction txn = db.beginWrite()) {
Node chunk = txn.createNode(List.of("Chunk"),
Map.of("text", "The transformer architecture uses self-attention..."));
txn.setVector(chunk.id(), "embedding",
Embedding.hashEmbed("transformer self-attention", 128));
txn.commit();
}
// Query across vector search + graph traversal
QueryResult results = db.query(
"MATCH (chunk:Chunk) " +
"WHERE chunk.embedding <=> $query < 0.5 " +
"RETURN chunk.text " +
"ORDER BY chunk.embedding <=> $query LIMIT 5",
Map.of("query", Embedding.hashEmbed("attention mechanism", 128)));
for (Map<String, Object> row : results.rows()) {
System.out.println(row.get("chunk.text"));
}
}
See bindings/java/README.md for build instructions and the full runnable example.
Benchmarked on Apple M1, single-threaded, with auto-scaled buffer pool. Run zig build benchmark to reproduce.
For the repeated-term FTS indexing workload that previously exposed quadratic append behavior, run zig build fts-benchmark.
| Operation | Latency | Throughput | Target | Status |
|---|---|---|---|---|
| Node lookup | 0.13 μs | 7.9M ops/sec | < 1 μs | PASS |
| Node creation | 0.65 μs | 1.5M ops/sec | — | — |
| Edge traversal | 9 μs | 111K ops/sec | — | — |
| Full-text search (100 docs) | 19 μs | 53K ops/sec | — | — |
| 10-NN vector search (1M vectors) | 0.83 ms | 1.2K ops/sec | < 10 ms @ 1M | PASS |
128-dimensional cosine vectors, M=16, ef_construction=200, ef_search=64, k=10. Run zig build vector-benchmark to reproduce.
| Scale | Mean Latency | P99 Latency | Recall@10 | Memory |
|---|---|---|---|---|
| 1,000 | 65 μs | 70 μs | 100% | 1 MB |
| 10,000 | 174 μs | 695 μs | 99% | 10 MB |
| 100,000 | 438 μs | 1.2 ms | 99% | 101 MB |
| 1,000,000 | 832 μs | 1.8 ms | 100% | 1,040 MB |
Search latency scales sub-linearly (O(log N)) with 99–100% recall@10. Uses heuristic neighbor selection (HNSW paper Algorithm 4) for diverse graph connectivity, connection page packing for ~4.5x memory reduction, and pre-normalized dot product for fast cosine distance.
ef_search Sensitivity (1M vectors)
| ef_search | Mean Latency | Recall@10 |
|---|---|---|
| 16 | 506 μs | 57% |
| 32 | 1.9 ms | 79% |
| 64 | 990 μs | 100% |
| 128 | 3.2 ms | 100% |
| 256 | 11.6 ms | 100% |
| System | Latency | Type | Source |
|---|---|---|---|
| LatticeDB | 0.13 μs | Embedded | zig build benchmark |
| RocksDB (in-memory) | 0.14 μs | Embedded | RocksDB wiki |
| SQLite (in-memory) | ~0.2 μs | Embedded | Turso blog |
| SQLite (WAL, disk) | 3 μs (p90) | Embedded | marending.dev |
| Neo4j | 28 ms (p99) | Server | Memgraph comparison |
LatticeDB's B+Tree achieves sub-microsecond cached lookups, matching RocksDB in-memory and outperforming SQLite on disk by 23x.
| System | Latency (10-NN) | Scale | Type | Source |
|---|---|---|---|---|
| LatticeDB | 0.83 ms mean, 100% recall | 1M | Embedded | zig build vector-benchmark |
| FAISS HNSW (single-thread) | 0.5–3 ms | 1M | Library | FAISS wiki |
| Weaviate | 1.4 ms mean, 3.1 ms P99 | 1M | Server | Weaviate benchmarks |
| Qdrant | ~1–2 ms | 1M | Server | Qdrant benchmarks |
| Milvus + SQ8 | 2.2 ms P99 | 1M | Server | VectorDBBench |
| pgvector HNSW | ~5 ms @ 99% recall | 1M | Extension | Jonathan Katz |
| LanceDB | 3–5 ms | 1M | Embedded | LanceDB blog |
| Chroma | 4–5 ms mean | 1M | Embedded | Chroma docs |
| Pinecone P2 | ~15 ms (incl. network) | 1M | Cloud | Pinecone blog |
| sqlite-vec (brute force) | 17 ms | 1M | Extension | Alex Garcia |
LatticeDB at 1M achieves 0.83 ms mean with 100% recall@10 — faster than FAISS single-threaded HNSW and competitive with Weaviate and Qdrant server-based systems (which add network overhead in practice).
| System | 2-hop (100K nodes) | Type | Source |
|---|---|---|---|
| LatticeDB | 39 μs | Embedded | zig build sqlite-benchmark |
| SQLite (recursive CTE) | 548 μs | Embedded | zig build sqlite-benchmark |
| Kuzu (archived Oct 2025) | 19 ms | Embedded | The Data Quarry |
| Neo4j | 10 ms (1M nodes) | Server | Neo4j blog |
Only the SQLite rows are measured head to head on the same machine in the same harness. The Kuzu and Neo4j figures come from third-party posts on hardware and with methodology we do not control, so treat them as order-of-magnitude orientation rather than a benchmark result.
LatticeDB vs SQLite — Social network graph with power-law degree distribution, adjacency cache pre-warmed:
Small Scale (10K nodes, 50K edges)
| Workload | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 1-hop traversal | 560 ns | 13.0 μs | 23x |
| 2-hop traversal | 3.0 μs | 37.5 μs | 13x |
| 3-hop traversal | 19.1 μs | 178.5 μs | 9x |
| Variable path (1..5) | 82.4 μs | 4.3 ms | 52x |
Medium Scale (100K nodes, 500K edges)
| Workload | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 1-hop traversal | 8.0 μs | 290.0 μs | 36x |
| 2-hop traversal | 38.7 μs | 548.3 μs | 14x |
| 3-hop traversal | 197.3 μs | 1.2 ms | 6x |
| Variable path (1..5) | 134.4 μs | 10.1 ms | 75x |
Depth-Limited Traversal (10K nodes, 50K edges)
| Depth | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 10 | 311 μs | 121 ms | 390x |
| 15 | 380 μs | 271 ms | 713x |
| 25 | 318 μs | 587 ms | 1,848x |
| 50 | 500 μs | 1.4 s | 2,819x |
LatticeDB uses BFS with adjacency cache and bitset visited tracking. SQLite uses a recursive CTE with UNION deduplication. Both compute identical reachable node sets (~8K nodes). The gap widens at deeper depths as SQLite's CTE overhead grows with each recursion level. Run zig build graph-benchmark -- --quick to reproduce.
| System | Search Latency | Type | Source |
|---|---|---|---|
| LatticeDB | 19 μs | Embedded | zig build benchmark |
| SQLite FTS5 | < 6 ms | Embedded | SQLite Cloud |
| Elasticsearch | 1–10 ms | Server | Various |
| Tantivy | 10–100 μs | Library | Various |
LatticeDB's inverted index with BM25 scoring is ~300x faster than SQLite FTS5 and competitive with Tantivy (a dedicated Rust search library).
Graph
*1..3)count, sum, avg, min, max, collect)Vector Search
Full-Text Search
Cypher Query Language
<=>@@$nameOperations
lattice backup, taken without closing the database:memory:, touching no files at alllattice compact for safe physical tail reclamationLatticeDB is fast, but speed is not the only thing that matters. Here are cases where a different tool is the better choice.
You need multiple applications writing to the same database at the same time. LatticeDB is embedded with a single-writer model. One process opens the file and owns it. If you need many clients connecting over a network, use Neo4j, PostgreSQL, or another client-server database.
Your data is fundamentally tabular. If your data fits naturally into rows and columns — sales records, user accounts, time series — a relational database like SQLite or PostgreSQL will be simpler and just as fast. Graph databases shine when relationships between records are the point, not an afterthought.
You need to scale beyond a single machine. LatticeDB stores everything in one file on one machine. It can ship that file's changes elsewhere continuously, so a disk failure costs you seconds rather than everything, but that is backup rather than clustering. If you need sharding, multi-node replicas serving reads, or distributed queries across billions of nodes, look at Neo4j cluster, Dgraph, or a managed service like Neptune.
You need the full Cypher language.
LatticeDB supports most of Cypher but not all of it. Features like OPTIONAL MATCH and CALL procedures are not yet implemented. If your queries depend on these, Neo4j is the complete implementation.
You need mature tooling and ecosystem. Neo4j has visualization tools, admin dashboards, monitoring, drivers in every language, and years of community resources. PostgreSQL has decades of tooling. LatticeDB is new and lean — which is a strength for embedding, but a weakness if you need a rich operational ecosystem around your database.
Written in Zig. No dependencies.
git clone https://github.com/jeffhajewski/latticedb.git
cd latticedb
zig build # build everything
zig build test # run tests
zig build -Doptimize=ReleaseFast # optimized build
The full documentation lives at docs.latticedb.org — the Cypher reference, the C, Python, TypeScript, and Go API references, guides, and the storage engine internals. latticedb.org is the project site.
The links below are the in-repo copies and design notes.
Zig
71.7%
HTML
9.6%
Python
5.7%
TypeScript
4.1%
Go
3.4%
C
2.7%
Java
1.8%
Embedded single-file knowledge graph database with vector search and full-text search for AI/RAG apps
668
stars
485
commits
Zig
primary language
Aug 31, 2026
updated
Embedded property-graph database with native vector and full-text indexing.
LatticeDB is a single-file local database for connected, semantic, and textual data. It lets you traverse relationships, run vector similarity search, and do BM25 full-text search over the same dataset in one engine and one query layer. It is designed for relationship-heavy workloads on a single machine, with zero-config operation and an embedded single-writer model.
LatticeDB is an embedded, single-file graph database that lets local applications query the same data by relationship, semantics, and text, then consume durable graph and application events from the same file. Workloads like Graph RAG, agent memory, and local knowledge tools are examples built on those primitives, not the definition of the engine.
// Find chunks similar to a query, traverse to their document, then to the author
MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query_vector < 0.3
AND doc.content @@ "neural networks"
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query_vector
LIMIT 10
CLI
curl -fsSL https://raw.githubusercontent.com/jeffhajewski/latticedb/main/dist/install.sh | bash
Python
pip install latticedb
Published wheels are expected to bundle liblattice on supported platforms. Source installs can also bundle a staged native library during wheel builds with LATTICE_BUNDLE_LIB_DIR=/path/to/lib.
TypeScript / Node.js
npm install @hajewski/latticedb
Published package tarballs are expected to bundle liblattice on supported platforms. Source checkouts can stage the native library into the package with LATTICE_BUNDLE_LIB_DIR=/path/to/lib npm run bundle:native.
Java
Requires JDK 21+. See bindings/java/README.md for the Maven build, which compiles the JNI bridge and stages liblattice from zig-out/lib. A runnable knowledge-graph example is in bindings/java/src/main/java/io/latticedb/examples.
Go
See bindings/go/README.md for the current cgo workflow. The default consumer path uses installed pkg-config metadata; in-repo development can use -tags repolocal against zig-out/lib.
There is also a runnable graph/vector/text retrieval example in examples/go.
Recent binding-surface cleanups moved embedding helpers into dedicated modules and subpackages. See docs/client_api_migration.md for the preferred imports and current compatibility aliases.
A complete example: create a small knowledge graph with documents and authors, store embeddings, index text, then query across all three search modes.
The examples use the built-in hash_embed / hashEmbed / HashEmbed helper so they run with no
external service. It is a deterministic placeholder, not a semantic embedding: similar text does not
produce nearby vectors, so a distance threshold is arbitrary and a similarity query may match nothing.
Use a real embedding model for anything where the results should mean something — see
Working with Embeddings.
from latticedb import Database
from latticedb.embedding import hash_embed
with Database("knowledge.db", create=True, enable_vectors=True, vector_dimensions=128) as db:
# --- Build the graph ---
db.create_node_fts_index("Chunk", "text")
with db.write() as txn:
# Create authors
alice = txn.create_node(labels=["Person"], properties={"name": "Alice", "field": "ML"})
bob = txn.create_node(labels=["Person"], properties={"name": "Bob", "field": "Systems"})
txn.create_edge(alice.id, bob.id, "COLLABORATES_WITH")
# Create documents with chunks
for title, text, author in [
("Attention Is All You Need", "The transformer architecture uses self-attention...", alice),
("Scaling Laws for LLMs", "We find that model performance scales predictably...", alice),
("Log-Structured Merge Trees", "LSM trees optimize write-heavy workloads...", bob),
]:
doc = txn.create_node(labels=["Document"], properties={"title": title})
chunk = txn.create_node(labels=["Chunk"], properties={"text": text})
# The chunk's text property is already indexed by the declaration above.
txn.set_vector(chunk.id, "embedding", hash_embed(text, dimensions=128))
txn.create_edge(chunk.id, doc.id, "PART_OF")
txn.create_edge(doc.id, author.id, "AUTHORED_BY")
txn.commit()
# --- Query: vector search + text match + graph traversal ---
results = db.query("""
MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query < 0.5
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query
LIMIT 5
""", parameters={"query": hash_embed("transformer attention mechanism", dimensions=128)})
for row in results:
print(f"{row['doc.title']} by {row['author.name']}")
# --- Full-text search ---
for r in db.fts_search("Chunk", "text", "self-attention transformer"):
print(f"Node {r.node_id}: score={r.score:.4f}")
# --- Aggregations ---
stats = db.query("""
MATCH (doc:Document)-[:AUTHORED_BY]->(p:Person)
RETURN p.name, count(doc) AS papers
ORDER BY papers DESC
""")
for row in stats:
print(f"{row['p.name']}: {row['papers']} papers")
import { Database } from "@hajewski/latticedb";
import { hashEmbed } from "@hajewski/latticedb/embedding";
const db = new Database("knowledge.db", {
create: true,
enableVectors: true,
vectorDimensions: 128,
});
await db.open();
// Build a graph
await db.write(async (txn) => {
const alice = await txn.createNode({
labels: ["Person"],
properties: { name: "Alice", field: "ML" },
});
const doc = await txn.createNode({
labels: ["Document"],
properties: { title: "Attention Is All You Need" },
});
const chunk = await txn.createNode({
labels: ["Chunk"],
properties: { text: "The transformer architecture uses self-attention..." },
});
await txn.setVector(chunk.id, "embedding", hashEmbed("transformer self-attention", 128));
await txn.createEdge(chunk.id, doc.id, "PART_OF");
await txn.createEdge(doc.id, alice.id, "AUTHORED_BY");
});
// Query across vector search + graph traversal
const results = await db.query(
`MATCH (chunk:Chunk)-[:PART_OF]->(doc:Document)-[:AUTHORED_BY]->(author:Person)
WHERE chunk.embedding <=> $query < 0.5
RETURN doc.title, chunk.text, author.name
ORDER BY chunk.embedding <=> $query
LIMIT 5`,
{ query: hashEmbed("attention mechanism", 128) }
);
for (const row of results.rows) {
console.log(`${row["doc.title"]} by ${row["author.name"]}`);
}
await db.close();
db, err := latticedb.Open("knowledge.db", latticedb.OpenOptions{
Create: true,
EnableVectors: true,
VectorDimensions: 128,
})
if err != nil {
log.Fatal(err)
}
defer db.Close()
if err := db.CreateNodeFTSIndex("Chunk", "text"); err != nil {
log.Fatal(err)
}
err = db.Update(func(tx *latticedb.Tx) error {
node, err := tx.CreateNode(latticedb.CreateNodeOptions{
Labels: []string{"Chunk"},
Properties: map[string]latticedb.Value{"text": "The transformer architecture uses self-attention..."},
})
if err != nil {
return err
}
embedding, err := latticedb.HashEmbed("The transformer architecture uses self-attention...", 128)
if err != nil {
return err
}
if err := tx.SetVector(node.ID, "embedding", embedding); err != nil {
return err
}
return tx.SetProperty(node.ID, "text", "The transformer architecture uses self-attention...")
})
if err != nil {
log.Fatal(err)
}
import io.latticedb.*;
import java.util.List;
import java.util.Map;
try (Database db = Database.open("knowledge.db",
OpenOptions.defaults().create(true).enableVectors(true).vectorDimensions(128))) {
// The Chunk.text property is what full-text search reads.
db.createNodeFtsIndex("Chunk", "text");
// Build a graph
try (Transaction txn = db.beginWrite()) {
Node chunk = txn.createNode(List.of("Chunk"),
Map.of("text", "The transformer architecture uses self-attention..."));
txn.setVector(chunk.id(), "embedding",
Embedding.hashEmbed("transformer self-attention", 128));
txn.commit();
}
// Query across vector search + graph traversal
QueryResult results = db.query(
"MATCH (chunk:Chunk) " +
"WHERE chunk.embedding <=> $query < 0.5 " +
"RETURN chunk.text " +
"ORDER BY chunk.embedding <=> $query LIMIT 5",
Map.of("query", Embedding.hashEmbed("attention mechanism", 128)));
for (Map<String, Object> row : results.rows()) {
System.out.println(row.get("chunk.text"));
}
}
See bindings/java/README.md for build instructions and the full runnable example.
Benchmarked on Apple M1, single-threaded, with auto-scaled buffer pool. Run zig build benchmark to reproduce.
For the repeated-term FTS indexing workload that previously exposed quadratic append behavior, run zig build fts-benchmark.
| Operation | Latency | Throughput | Target | Status |
|---|---|---|---|---|
| Node lookup | 0.13 μs | 7.9M ops/sec | < 1 μs | PASS |
| Node creation | 0.65 μs | 1.5M ops/sec | — | — |
| Edge traversal | 9 μs | 111K ops/sec | — | — |
| Full-text search (100 docs) | 19 μs | 53K ops/sec | — | — |
| 10-NN vector search (1M vectors) | 0.83 ms | 1.2K ops/sec | < 10 ms @ 1M | PASS |
128-dimensional cosine vectors, M=16, ef_construction=200, ef_search=64, k=10. Run zig build vector-benchmark to reproduce.
| Scale | Mean Latency | P99 Latency | Recall@10 | Memory |
|---|---|---|---|---|
| 1,000 | 65 μs | 70 μs | 100% | 1 MB |
| 10,000 | 174 μs | 695 μs | 99% | 10 MB |
| 100,000 | 438 μs | 1.2 ms | 99% | 101 MB |
| 1,000,000 | 832 μs | 1.8 ms | 100% | 1,040 MB |
Search latency scales sub-linearly (O(log N)) with 99–100% recall@10. Uses heuristic neighbor selection (HNSW paper Algorithm 4) for diverse graph connectivity, connection page packing for ~4.5x memory reduction, and pre-normalized dot product for fast cosine distance.
ef_search Sensitivity (1M vectors)
| ef_search | Mean Latency | Recall@10 |
|---|---|---|
| 16 | 506 μs | 57% |
| 32 | 1.9 ms | 79% |
| 64 | 990 μs | 100% |
| 128 | 3.2 ms | 100% |
| 256 | 11.6 ms | 100% |
| System | Latency | Type | Source |
|---|---|---|---|
| LatticeDB | 0.13 μs | Embedded | zig build benchmark |
| RocksDB (in-memory) | 0.14 μs | Embedded | RocksDB wiki |
| SQLite (in-memory) | ~0.2 μs | Embedded | Turso blog |
| SQLite (WAL, disk) | 3 μs (p90) | Embedded | marending.dev |
| Neo4j | 28 ms (p99) | Server | Memgraph comparison |
LatticeDB's B+Tree achieves sub-microsecond cached lookups, matching RocksDB in-memory and outperforming SQLite on disk by 23x.
| System | Latency (10-NN) | Scale | Type | Source |
|---|---|---|---|---|
| LatticeDB | 0.83 ms mean, 100% recall | 1M | Embedded | zig build vector-benchmark |
| FAISS HNSW (single-thread) | 0.5–3 ms | 1M | Library | FAISS wiki |
| Weaviate | 1.4 ms mean, 3.1 ms P99 | 1M | Server | Weaviate benchmarks |
| Qdrant | ~1–2 ms | 1M | Server | Qdrant benchmarks |
| Milvus + SQ8 | 2.2 ms P99 | 1M | Server | VectorDBBench |
| pgvector HNSW | ~5 ms @ 99% recall | 1M | Extension | Jonathan Katz |
| LanceDB | 3–5 ms | 1M | Embedded | LanceDB blog |
| Chroma | 4–5 ms mean | 1M | Embedded | Chroma docs |
| Pinecone P2 | ~15 ms (incl. network) | 1M | Cloud | Pinecone blog |
| sqlite-vec (brute force) | 17 ms | 1M | Extension | Alex Garcia |
LatticeDB at 1M achieves 0.83 ms mean with 100% recall@10 — faster than FAISS single-threaded HNSW and competitive with Weaviate and Qdrant server-based systems (which add network overhead in practice).
| System | 2-hop (100K nodes) | Type | Source |
|---|---|---|---|
| LatticeDB | 39 μs | Embedded | zig build sqlite-benchmark |
| SQLite (recursive CTE) | 548 μs | Embedded | zig build sqlite-benchmark |
| Kuzu (archived Oct 2025) | 19 ms | Embedded | The Data Quarry |
| Neo4j | 10 ms (1M nodes) | Server | Neo4j blog |
Only the SQLite rows are measured head to head on the same machine in the same harness. The Kuzu and Neo4j figures come from third-party posts on hardware and with methodology we do not control, so treat them as order-of-magnitude orientation rather than a benchmark result.
LatticeDB vs SQLite — Social network graph with power-law degree distribution, adjacency cache pre-warmed:
Small Scale (10K nodes, 50K edges)
| Workload | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 1-hop traversal | 560 ns | 13.0 μs | 23x |
| 2-hop traversal | 3.0 μs | 37.5 μs | 13x |
| 3-hop traversal | 19.1 μs | 178.5 μs | 9x |
| Variable path (1..5) | 82.4 μs | 4.3 ms | 52x |
Medium Scale (100K nodes, 500K edges)
| Workload | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 1-hop traversal | 8.0 μs | 290.0 μs | 36x |
| 2-hop traversal | 38.7 μs | 548.3 μs | 14x |
| 3-hop traversal | 197.3 μs | 1.2 ms | 6x |
| Variable path (1..5) | 134.4 μs | 10.1 ms | 75x |
Depth-Limited Traversal (10K nodes, 50K edges)
| Depth | LatticeDB | SQLite | Speedup |
|---|---|---|---|
| 10 | 311 μs | 121 ms | 390x |
| 15 | 380 μs | 271 ms | 713x |
| 25 | 318 μs | 587 ms | 1,848x |
| 50 | 500 μs | 1.4 s | 2,819x |
LatticeDB uses BFS with adjacency cache and bitset visited tracking. SQLite uses a recursive CTE with UNION deduplication. Both compute identical reachable node sets (~8K nodes). The gap widens at deeper depths as SQLite's CTE overhead grows with each recursion level. Run zig build graph-benchmark -- --quick to reproduce.
| System | Search Latency | Type | Source |
|---|---|---|---|
| LatticeDB | 19 μs | Embedded | zig build benchmark |
| SQLite FTS5 | < 6 ms | Embedded | SQLite Cloud |
| Elasticsearch | 1–10 ms | Server | Various |
| Tantivy | 10–100 μs | Library | Various |
LatticeDB's inverted index with BM25 scoring is ~300x faster than SQLite FTS5 and competitive with Tantivy (a dedicated Rust search library).
Graph
*1..3)count, sum, avg, min, max, collect)Vector Search
Full-Text Search
Cypher Query Language
<=>@@$nameOperations
lattice backup, taken without closing the database:memory:, touching no files at alllattice compact for safe physical tail reclamationLatticeDB is fast, but speed is not the only thing that matters. Here are cases where a different tool is the better choice.
You need multiple applications writing to the same database at the same time. LatticeDB is embedded with a single-writer model. One process opens the file and owns it. If you need many clients connecting over a network, use Neo4j, PostgreSQL, or another client-server database.
Your data is fundamentally tabular. If your data fits naturally into rows and columns — sales records, user accounts, time series — a relational database like SQLite or PostgreSQL will be simpler and just as fast. Graph databases shine when relationships between records are the point, not an afterthought.
You need to scale beyond a single machine. LatticeDB stores everything in one file on one machine. It can ship that file's changes elsewhere continuously, so a disk failure costs you seconds rather than everything, but that is backup rather than clustering. If you need sharding, multi-node replicas serving reads, or distributed queries across billions of nodes, look at Neo4j cluster, Dgraph, or a managed service like Neptune.
You need the full Cypher language.
LatticeDB supports most of Cypher but not all of it. Features like OPTIONAL MATCH and CALL procedures are not yet implemented. If your queries depend on these, Neo4j is the complete implementation.
You need mature tooling and ecosystem. Neo4j has visualization tools, admin dashboards, monitoring, drivers in every language, and years of community resources. PostgreSQL has decades of tooling. LatticeDB is new and lean — which is a strength for embedding, but a weakness if you need a rich operational ecosystem around your database.
Written in Zig. No dependencies.
git clone https://github.com/jeffhajewski/latticedb.git
cd latticedb
zig build # build everything
zig build test # run tests
zig build -Doptimize=ReleaseFast # optimized build
The full documentation lives at docs.latticedb.org — the Cypher reference, the C, Python, TypeScript, and Go API references, guides, and the storage engine internals. latticedb.org is the project site.
The links below are the in-repo copies and design notes.
Zig
71.7%
HTML
9.6%
Python
5.7%
TypeScript
4.1%
Go
3.4%
C
2.7%
Java
1.8%