Skip to content

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 ​

ItemValue
Nodes67,280
Edges40,921
Embeddings67,298
Vector indexHNSW, CPU-only
Request count800 per query type
Query typesVector 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 ​

WorkloadTransportThroughputMeanP50P95P99Max
Vector onlyHTTP19,342 req/s511 us470 us750 us869 us1.02 ms
Vector onlyBolt22,309 req/s444 us428 us629 us814 us968 us
Vector + 1 hopHTTP11,523 req/s859 us699 us1.54 ms3.46 ms4.71 ms
Vector + 1 hopBolt13,291 req/s747 us637 us1.29 ms3.24 ms4.47 ms

Traversal Queries ​

DepthTransportThroughputMeanP50P95P99Max
1HTTP23,492 req/s419 us365 us773 us1.00 ms1.50 ms
1Bolt24,668 req/s402 us386 us575 us784 us2.59 ms
2HTTP19,257 req/s514 us415 us1.00 ms2.29 ms5.81 ms
2Bolt25,188 req/s393 us390 us508 us617 us747 us
3HTTP18,105 req/s548 us541 us816 us1.22 ms2.47 ms
3Bolt22,212 req/s446 us427 us572 us754 us2.42 ms
4HTTP21,793 req/s453 us368 us789 us1.35 ms4.23 ms
4Bolt25,035 req/s396 us387 us517 us612 us764 us
5HTTP21,884 req/s450 us369 us786 us1.10 ms4.09 ms
5Bolt25,230 req/s393 us389 us499 us627 us985 us
6HTTP18,715 req/s528 us412 us1.15 ms3.19 ms3.53 ms
6Bolt24,487 req/s403 us399 us509 us607 us720 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.

WorkloadEnvironmentP50
Vector onlyRemote GCP110.7 ms
Vector + 1 hopRemote GCP112.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 ​

ItemValue
Nodes300,825
Deterministic edges1,000,000
Stored embeddings221,625
Embedding dimensions1,024
ModelLocal BGE-M3 GGUF with Metal GPU offload
Vector indexHNSW
Request count800 per query shape and transport
Query shapesFull RRF retrieval; Full RRF retrieval + hop

Local environment:

  • Apple M3 Max
  • 64 GB RAM
  • Source build with the localllm tag

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 ​

WorkloadTransportThroughputMinMeanP50P95P99Max
Full RRF retrievalBolt51.357 ops/s18.445 ms19.468 ms19.272 ms21.130 ms23.400 ms24.363 ms
Full RRF retrievalHTTP49.011 ops/s18.668 ms20.401 ms19.549 ms23.963 ms32.526 ms39.108 ms
Full RRF retrieval + 1 hopBolt50.005 ops/s18.636 ms19.995 ms19.604 ms22.509 ms25.352 ms30.784 ms
Full RRF retrieval + 1 hopHTTP49.312 ops/s18.794 ms20.276 ms19.719 ms22.851 ms31.073 ms42.243 ms

Reproduce the focused run:

bash
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:

  1. embed the query
  2. call a vector store
  3. move the results into a graph store or application layer
  4. 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:

bash
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:

bash
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:

bash
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:

bash
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"]
      }
    ]
  }'
Analytics privacy