Hybrid Query Benchmarks¶
This benchmark focuses on the query shape that matters most for NornicDB's positioning: semantic retrieval followed by graph expansion in the same engine.
The goal is not to claim a universal leaderboard result. The goal is to show what happens when vector search and one-hop graph traversal share one execution path instead of being stitched together across multiple systems.
Summary¶
- Vector-only queries stayed in sub-millisecond to low-millisecond territory locally, depending on transport.
- Vector + one-hop graph traversal added a small incremental cost locally.
- Full RRF retrieval averaged 19.47 ms over Bolt and 20.40 ms over HTTP on the production-scale fixture.
- Full RRF retrieval + one-hop graph traversal averaged 20.00 ms over Bolt and 20.28 ms over HTTP.
- Remote latency tracked client-to-server RTT, which means end-to-end latency became network-bound rather than database-bound.
Original 67K Direct-Vector Benchmark¶
Test Setup¶
| Item | Value |
|---|---|
| Nodes | 67,280 |
| Edges | 40,921 |
| Embeddings | 67,298 |
| Vector index | HNSW, CPU-only |
| Request count | 800 per query type |
| Query types | Vector top-k; Vector top-k + 1-hop traversal |
Local environment:
- Apple M3 Max
- 64 GB RAM
- Native macOS installer
Remote environment:
- GCP
- 8 vCPU
- 32 GB RAM
Local Results¶
| Workload | Transport | Throughput | Mean | P50 | P95 | P99 | Max |
|---|---|---|---|---|---|---|---|
| Vector only | HTTP | 19,342 req/s | 511 us | 470 us | 750 us | 869 us | 1.02 ms |
| Vector only | Bolt | 22,309 req/s | 444 us | 428 us | 629 us | 814 us | 968 us |
| Vector + 1 hop | HTTP | 11,523 req/s | 859 us | 699 us | 1.54 ms | 3.46 ms | 4.71 ms |
| Vector + 1 hop | Bolt | 13,291 req/s | 747 us | 637 us | 1.29 ms | 3.24 ms | 4.47 ms |
Traversal Queries¶
| Depth | Transport | Throughput | Mean | P50 | P95 | P99 | Max |
|---|---|---|---|---|---|---|---|
| 1 | HTTP | 23,492 req/s | 419 us | 365 us | 773 us | 1.00 ms | 1.50 ms |
| 1 | Bolt | 24,668 req/s | 402 us | 386 us | 575 us | 784 us | 2.59 ms |
| 2 | HTTP | 19,257 req/s | 514 us | 415 us | 1.00 ms | 2.29 ms | 5.81 ms |
| 2 | Bolt | 25,188 req/s | 393 us | 390 us | 508 us | 617 us | 747 us |
| 3 | HTTP | 18,105 req/s | 548 us | 541 us | 816 us | 1.22 ms | 2.47 ms |
| 3 | Bolt | 22,212 req/s | 446 us | 427 us | 572 us | 754 us | 2.42 ms |
| 4 | HTTP | 21,793 req/s | 453 us | 368 us | 789 us | 1.35 ms | 4.23 ms |
| 4 | Bolt | 25,035 req/s | 396 us | 387 us | 517 us | 612 us | 764 us |
| 5 | HTTP | 21,884 req/s | 450 us | 369 us | 786 us | 1.10 ms | 4.09 ms |
| 5 | Bolt | 25,230 req/s | 393 us | 389 us | 499 us | 627 us | 985 us |
| 6 | HTTP | 18,715 req/s | 528 us | 412 us | 1.15 ms | 3.19 ms | 3.53 ms |
| 6 | Bolt | 24,487 req/s | 403 us | 399 us | 509 us | 607 us | 720 us |
Bolt is nearly zero allocation. This was measured under concurrent load with mixed HTTP and Bolt queries. The tail-latency spikes came from garbage collection triggered by the HTTP path. Bolt is more efficient than HTTP for tail latency.
Remote Results¶
Client-to-server latency was about 110 ms.
| Workload | Environment | P50 |
|---|---|---|
| Vector only | Remote GCP | 110.7 ms |
| Vector + 1 hop | Remote GCP | 112.9 ms |
Once local direct-vector compute is in the sub-millisecond range, network RTT dominates the user-visible latency budget.
Larger 300K-Node Full-RRF Benchmark¶
Run on 2026-08-28 at revision c27fff0fbfd2 using the real BGE-M3 fixture in data/test-200kembed. The writable data directory was cloned from that source before mutation. The benchmark waited for the search readiness endpoint to report ready and verified the node and edge counts before measuring.
Test Setup¶
| Item | Value |
|---|---|
| Nodes | 300,825 |
| Deterministic edges | 1,000,000 |
| Stored embeddings | 221,625 |
| Embedding dimensions | 1,024 |
| Model | Local BGE-M3 GGUF with Metal GPU offload |
| Vector index | HNSW |
| Request count | 800 per query shape and transport |
| Query shapes | Full RRF retrieval; Full RRF retrieval + hop |
Local environment:
- Apple M3 Max
- 64 GB RAM
- Source build with the
localllmtag
The server used no search-result or embedding cache and had zero background embedding workers. Every request used a unique query cache key, so every sample executed vector retrieval, BM25 retrieval, and RRF fusion. Each response asserted search_method = rrf_hybrid, a positive RRF score, at least one source rank, and fallback_triggered = false; the hop shape also required a neighbor.
Results¶
| Workload | Transport | Throughput | Min | Mean | P50 | P95 | P99 | Max |
|---|---|---|---|---|---|---|---|---|
| Full RRF retrieval | Bolt | 51.357 ops/s | 18.445 ms | 19.468 ms | 19.272 ms | 21.130 ms | 23.400 ms | 24.363 ms |
| Full RRF retrieval | HTTP | 49.011 ops/s | 18.668 ms | 20.401 ms | 19.549 ms | 23.963 ms | 32.526 ms | 39.108 ms |
| Full RRF retrieval + 1 hop | Bolt | 50.005 ops/s | 18.636 ms | 19.995 ms | 19.604 ms | 22.509 ms | 25.352 ms | 30.784 ms |
| Full RRF retrieval + 1 hop | HTTP | 49.312 ops/s | 18.794 ms | 20.276 ms | 19.719 ms | 22.851 ms | 31.073 ms | 42.243 ms |
Reproduce the focused run:
NORNICDB_LARGE_RRF_E2E=1 \
NORNICDB_LARGE_RRF_HTTP_ADDR=127.0.0.1:17474 \
NORNICDB_LARGE_RRF_BOLT_ADDR=127.0.0.1:17687 \
NORNICDB_LARGE_RRF_DATABASE=translations \
NORNICDB_LARGE_RRF_EDGES=1000000 \
NORNICDB_LARGE_RRF_WARMUP=3 \
NORNICDB_LARGE_RRF_ITERS=800 \
go test -tags=e2e ./testing/e2e \
-run '^TestLargeDatasetRRFRoundTrip_BoltVsHTTP$' -count=1 -v
The address variables attach the test to an already configured benchmark server. Omit them to let the test copy-on-write clone data/test-200kembed, build the local-embedding binary, and manage the server itself.
Why This Matters¶
Most systems make this query shape a composition problem:
- embed the query
- call a vector store
- move the results into a graph store or application layer
- expand neighbors and shape the result there
NornicDB keeps that inside one execution engine. The benchmark does not prove every workload is constant-time, but it does show that shallow hybrid retrieval can stay tight enough locally that deployment topology matters more than extra database-side micro-optimizations.
Caveats¶
- These are single-node measurements.
- The dataset is not billion-scale.
- Remote throughput is latency-bound, not compute-bound.
- These numbers are useful for query-shape comparison, not as a blanket claim for every graph or vector workload.
Verification Queries¶
Vector-only:
curl -s -u "$NORNIC_USERNAME:$NORNIC_PASSWORD" "$ENDPOINT" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"statements":[
{
"statement":"CALL db.index.vector.queryNodes('\''idx_original_text'\'', $topK, $text) YIELD node, score RETURN node.originalText AS originalText, score ORDER BY score DESC LIMIT $topK",
"parameters":{"text":"get it delivered","topK":5},
"resultDataContents":["row"]
}
]
}'
Vector + one-hop graph traversal:
curl -s -u "$NORNIC_USERNAME:$NORNIC_PASSWORD" "$ENDPOINT" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"statements":[
{
"statement":"CALL db.index.vector.queryNodes('\''idx_original_text'\'', $topK, $text) YIELD node, score MATCH (node:OriginalText)-[:TRANSLATES_TO]->(t:TranslatedText) WHERE t.language = $targetLang RETURN node.originalText AS originalText, score, t.language AS language, coalesce(t.auditedText, t.translatedText) AS translatedText ORDER BY score DESC, language LIMIT $topK",
"parameters":{"text":"get it delivered","topK":5,"targetLang":"es"},
"resultDataContents":["row"]
}
]
}'
Full-RRF Verification Queries¶
Full RRF retrieval:
curl -s -u "$NORNIC_USERNAME:$NORNIC_PASSWORD" "$ENDPOINT" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"statements":[
{
"statement":"CALL db.retrieve($request) YIELD node, score, rrf_score, vector_rank, bm25_rank, search_method, fallback_triggered RETURN node.originalText AS originalText, score, rrf_score, vector_rank, bm25_rank, search_method, fallback_triggered ORDER BY rrf_score DESC LIMIT 5",
"parameters":{"request":{"query":"chain baseline","embedding":[0.95,0.05,0.0],"limit":5,"types":["OriginalText"]}},
"resultDataContents":["row"]
}
]
}'
Full RRF retrieval + one-hop graph traversal:
curl -s -u "$NORNIC_USERNAME:$NORNIC_PASSWORD" "$ENDPOINT" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"statements":[
{
"statement":"CALL db.retrieve($request) YIELD node, score, rrf_score, vector_rank, bm25_rank, search_method, fallback_triggered MATCH (node)-[:BENCH_HOP]->(neighbor:BenchmarkHop) RETURN node.originalText AS originalText, elementId(neighbor) AS neighborID, score, rrf_score, vector_rank, bm25_rank, search_method, fallback_triggered ORDER BY rrf_score DESC LIMIT 5",
"parameters":{"request":{"query":"chain baseline","embedding":[0.95,0.05,0.0],"limit":5,"types":["OriginalText"]}},
"resultDataContents":["row"]
}
]
}'