Performance Tuning

This guide covers the key parameters for tuning LatticeDB performance.

Cache Size

The cache_size_mb parameter controls how many database pages are cached in memory. Larger caches reduce disk I/O.

db = Database("mydb.db", cache_size_mb=200)  # 200 MB cache

Guidelines:

  • Default is 100 MB, which handles most workloads well
  • For large databases (1M+ nodes), increase to 200-500 MB
  • For memory-constrained environments, reduce to 50 MB or less
  • The cache stores fixed-size pages (4 KB each), so 100 MB holds ~25,000 pages

The ef_search parameter controls the accuracy/speed tradeoff for HNSW vector search.

ef_searchMean Latency (1M vectors)Recall@10
16506 us57%
321.9 ms79%
64990 us100%
1283.2 ms100%
25611.6 ms100%

Guidelines:

  • Default is 64, which achieves 100% recall at 1M vectors
  • For latency-sensitive applications, try 32 (79% recall)
  • For maximum recall in critical applications, use 128
  • Values above 128 rarely improve recall but increase latency
# Programmatic API
results = db.vector_search(query_vec, k=10, ef_search=128)

Batch Insert

When loading large amounts of data, use batch_insert instead of individual creates:

import numpy as np

with db.write() as txn:
    # Fast: ~248 inserts/sec at 1M scale
    vectors = np.random.rand(10000, 128).astype(np.float32)
    node_ids = txn.batch_insert("Document", vectors)
    txn.commit()

Batch insert is significantly faster than individual node creation because it amortizes HNSW index updates.

Query Plan Caching

Use parameterized queries to enable plan caching:

# Good: plan is cached and reused
for name in names:
    db.query("MATCH (n:Person) WHERE n.name = $name RETURN n", parameters={"name": name})

# Bad: new plan compiled for each query
for name in names:
    db.query(f"MATCH (n:Person) WHERE n.name = '{name}' RETURN n")

Monitor cache effectiveness:

stats = db.cache_stats()
print(f"Hit rate: {stats['hits'] / (stats['hits'] + stats['misses']):.1%}")

Indexing Strategy

Only index text that you need to search. Indexing unnecessary text wastes memory and slows inserts:

# Index only the searchable text field
txn.fts_index(chunk.id, chunk_text)
# Don't index metadata, IDs, etc.

Labels

Use specific labels for nodes you query frequently. Label scans are fast because they use a dedicated index:

-- Fast: scans only Chunk nodes
MATCH (c:Chunk) WHERE c.embedding <=> $q < 0.5 RETURN c

-- Slower: scans all nodes
MATCH (n) WHERE n.embedding <=> $q < 0.5 RETURN n

Transaction Scope

Keep write transactions short. Long-running write transactions hold the write lock and block other writes:

# Good: small, focused transactions
for batch in chunks(data, 1000):
    with db.write() as txn:
        for item in batch:
            txn.create_node(labels=["Item"], properties=item)
        txn.commit()

# Bad: one giant transaction
with db.write() as txn:
    for item in all_data:  # millions of items
        txn.create_node(labels=["Item"], properties=item)
    txn.commit()

Memory Usage

Vector storage dominates memory at scale:

ScaleMemory
1,000 vectors (128d)1 MB
10,000 vectors10 MB
100,000 vectors101 MB
1,000,000 vectors1,040 MB

Plan your vector_dimensions and scale accordingly. Lower dimensions use less memory but capture less semantic information.