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.
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 |
|---|---|---|---|
| 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¶
- Scale & Sharding — bounded ingest, distributed queries, and the 250M-document roadmap
- Vector Search — DiskIVF disk-backed vector index
- Bitemporal Queries — temporal queries now answered by the authoritative bitemporal store