Skip to content

Performance & Benchmarks

The July 2026 query-engine overhaul made point lookups and selective patterns on the live delta overlay effectively constant-time (~10 µs) regardless of store size, and accelerated heavy analytical queries by 2× to four orders of magnitude — with bit-identical results on every benchmark query.


The igbench Harness

Benchmarks are produced by igbench, IndentiaDB's engine-level SPARQL 1.1/1.2 + RDF 1.2 benchmark suite. It builds a deterministic synthetic dataset (persons with a social graph, RDF 1.2 triple-term annotations, language-and-direction tagged literals, documents in named tenant graphs) through the production ingest path, then times 21 queries, 2 updates, load, and parse.

Two properties keep the numbers honest:

  • Correctness proof — every query's result set is hashed (order-insensitive SHA-256); old and new engines must produce identical hashes at every scale before a timing counts.
  • Realistic path — data lives in the live in-memory delta overlay (no offline base indexes), mirroring a live standalone/HA node, with the production allocator.
igbench --persons 20000 --graphs 4 --iters 5 --out results.json

Headline Results

Measured at 20,000 persons / 352K quads; factors in parentheses at 1.77M quads. All result sets bit-identical between old and new engines.

Query class Example Speedup
Point lookup (predicate+object bound) lookup_po 485× (2,280×)
Two-hop chain chain2 759× (5,351×)
Temporal point query tt_point 901× (6,902×)
ASK on an edge ask_edge 476× (3,061×)
Property path (ex:reportsTo+) path_down 16,384× — 35 s → 2.1 ms
Named-graph scan graph_docs 12.9× (18.3×)
CONSTRUCT subgraph construct_sub 39.9×
Language/direction filter (RDF 1.2) langdir_filter 13.6×
ORDER BY order_age 7.4×
GROUP BY / DISTINCT / scans group_city, distinct_city, scan_p 2.9–3.7×
Full graph dump graph_dump 6.5×
Loads and updates load_trig, u_insert_2k ~1× (unchanged)

Through the full HTTP stack (including JSON serialization) the same suite measures 1.4×–3.9×. The ACL-aware retrieval-corpus cache brings /context/assemble retrieval from 14.6 ms to 1.7 ms p50 (85.4 ms → 8.5 ms on a 25K-document corpus window).


What Changed

All optimizations are behaviour-neutral — only physical plans and data structures changed:

Optimization Effect
Delta-aware permutation selection The planner now uses all six sorted in-memory permutations of the delta overlay with seek instead of scan, one lock acquisition per pattern (was 8), zero-allocation prefix buffers, and graph-filter pushdown. This is what turns bound-pattern lookups constant-time.
Columnar query solutions Result rows share their variable schema and move terms out of the engine instead of deep-cloning a hash map per row.
Real-cardinality join planning Join ordering uses real pattern counts from the store (not hardcoded heuristics) and picks the join algorithm — hash join vs lateral (bind) join — on cost.
ORDER BY precompute + fused top-k Sort keys are evaluated once per row, and LIMIT over ORDER BY fuses into a bounded top-k selection. Inline-encoded terms decode without vocabulary locks.
Graph-leading overlay mirror A graph-ordered mirror of the insert overlay (~8% overlay memory) lets GRAPH <g> { … } scans seek directly to the graph.
ACL-aware retrieval-corpus cache RAG retrieval lanes reuse one cached ACL-filtered corpus per (graph × actor view), invalidated immediately on any write or ACL revoke.

Search via the Elasticsearch API

The search layer on port 9200 is measured with BEIR datasets (SciFact, FiQA, TREC-COVID) against Elasticsearch 9 and Typesense, with the same queries and mappings. FiQA (57,638 documents, 648 judged queries; IndentiaDB native, Elasticsearch and Typesense in local containers, October 2026):

IndentiaDB Elasticsearch Typesense
Bulk ingest 53 s (1,080 docs/s) 32 s (1,740 docs/s) 42 s (1,380 docs/s)
BM25 nDCG@10 / p50 0.238 / 1.3 ms 0.239 / 2.4 ms 0.068 / 4.7 ms
Filtered BM25 p50 1.2 ms 1.8 ms 12.9 ms
Fuzzy p50 10.2 ms 7.9 ms 75 ms
Phrase p50 0.5 ms 1.5 ms 1.9 ms
kNN recall@10 / p50 0.982 / 0.8 ms 0.976 / 1.3 ms 0.981 / 7.9 ms
Exact kNN p50 4.0 ms 5.4 ms 3.6 ms
Hybrid (RRF) nDCG@10 / p50 0.342 / 1.9 ms 0.341 / 1.7 ms 0.124 / 6.2 ms

Search quality matches Elasticsearch. Bulk ingest is the largest remaining gap. Because IndentiaDB ran natively here and the other two in containers, the latencies are indicative; compare within one column, and between IndentiaDB builds.

What made the difference, briefly: the Tantivy text index answers match, multi_match, phrase prefix, fuzzy, and sorting on dates and numbers; the HNSW graph picks neighbours with the diversity heuristic (FiQA recall@10 from 0.942 to 0.982); exact kNN runs from the proven in-memory vectors; and bulk writes commit a chunk as one transaction while no other writer touches the same table.


Load test and sizing

Ingest and search under load, IndentiaDB next to Elasticsearch 9 in Docker with the same limit per engine (8 vCPU, 16 GB), 1024-dimensional vectors with int8_hnsw, and TREC-COVID documents (1.1 KB of text on average) repeated to 1 million. IndentiaDB runs with its text index on disk (ES_TANTIVY_PERSISTENT_DIR) and without the second, SurrealDB-native text index (ES_SURREAL_FULLTEXT_DEFAULT=false, see below). October 2026.

IndentiaDB Elasticsearch
Ingest, 1 / 4 / 8 writers (docs/s) 772 / 2,288 / 3,768 1,967 / 7,024 / 10,382
Ingest at 1M documents, 4 writers 1,459 docs/s 5,891 docs/s
Memory at 1M (peak) 6.1 GiB (9.7) 9.5 GiB
Disk at 1M 16.9 GB 9.4 GB
Disk per document at 2 / 20 / 100 KB of text 14.9 / 33.9 / 82.0 KB 7.2 / 23.5 / 95.4 KB
BM25 at 1M, 16 clients: QPS / p50 540 / 23 ms 237 / 73 ms
kNN at 1M, 16 clients: QPS / p50 243 / 62 ms 202 / 49 ms
Hybrid at 1M, 16 clients: QPS / p50 26 / 603 ms 110 / 120 ms

The Elasticsearch rows at 1M come from a run with hnsw (float); the disk-per-document row uses int8_hnsw for both.

With these settings ingest is 2.7 times faster than at the previous measurement with default settings (4 writers: 863 to 2,288 docs/s). Leaving out the SurrealDB text index alone takes 1M documents from 254 to 1,459 docs/s and from 43.5 to 16.9 GB of disk. Ingest stays at about a third of Elasticsearch. BM25 is faster than Elasticsearch, kNN comparable; hybrid search is the gap. IndentiaDB refuses a _bulk with HTTP 429 when its write buffer is full (IG_ES_WRITE_INFLIGHT_BYTES, default 256 MB); clients should retry after Retry-After, as the Elasticsearch bulk helpers do.

Ingest-heavy indexes

index.surreal_fulltext: false (per index) or ES_SURREAL_FULLTEXT_DEFAULT=false (the default for new indexes; the value is recorded in the index when it is created) skips SurrealDB's BM25 index. The Tantivy text index answers the text queries. Without that index every bulk chunk commits as one transaction, also with several writers. Four query types still need the SurrealDB index and get a 400 without it: match_bool_prefix, search_after with a text query, function_score with random_score, and point-in-time with slicing.

Sizing for 30 million documents

Extrapolated from the measurements above, assuming this mix of file types and extracted text per type (the files themselves are not in the search index):

Type File Text Share
E-mail 20–200 KB 5 KB 40%
Word 50–500 KB 20 KB 15%
PDF, digital 100 KB–2 MB 50 KB 12%
PDF, scanned (OCR) 1–20 MB 25 KB 5%
Excel 50 KB–2 MB 60 KB 6%
PowerPoint 1–10 MB 12 KB 4%
Text files 5–100 KB 30 KB 3%
Images (OCR/caption) 500 KB–5 MB 0.5 KB 15%

That is 17.3 KB of text per document on average. With one document vector per document (what the indexer does today):

Documents Engine Disk per copy RAM per copy (vectors in memory) Initial load per 8 vCPU
30M Elasticsearch 0.57 TiB 32 GiB + heap 3.4 h
30M IndentiaDB 0.77 TiB 45 GiB 9.2 h
100M Elasticsearch 1.9 TiB 108 GiB + heap 11 h
100M IndentiaDB 2.6 TiB 132 GiB 34 h
300M Elasticsearch 5.7 TiB 323 GiB + heap 34 h
300M IndentiaDB 7.7 TiB 380 GiB 109 h

With a vector per 2 KB of text as well (chunk embeddings), 30M documents become 300M vectors: 2.3 TiB and 297 GiB for Elasticsearch, 4.4 TiB and 380 GiB for IndentiaDB.

Recommendation for 30M documents, one vector per document:

  • Elasticsearch: 3 nodes with 16 vCPU, 64 GB RAM (31 GB heap) and 1 TB NVMe each; 6 primary shards and 1 replica.
  • IndentiaDB: 1 node with 32 vCPU, 128 GB RAM and 2 TB NVMe per copy, plus a second identical node for availability. The Elasticsearch API searches one shard per index, so one index has to fit on one node. Beyond that, spread the documents over several indexes (per source or tenant) on separate nodes and let the application search across them.

The least certain inputs are the text per file type, search load at 30 times the measured size, and ingest at large counts: IndentiaDB went from 2,140 docs/s at 250k to 1,459 at 1M documents.


See Also