Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,21 @@ The following questions are asked of both graphs:

## High-level results

| Query | neo4j-2025.12.1 (ms) | kuzu-0.11.3 (ms) | ladybug-0.14.1 (ms) | lance-graph-0.4.0 (ms) |
| Query | neo4j-2025.12.1 (ms) | kuzu-0.11.3 (ms) | ladybug-0.14.1 (ms) | lance-graph-0.5.1 (ms) |
| --- | --- | --- | --- | --- |
| q1 | 1552ms | 138ms (11.2x) | 135ms (11.5x) | 22ms (69.8x) |
| q2 | 395ms | 228ms (1.7x) | 215ms (1.8x) | 45ms (8.7x) |
| q3 | 39ms | 6ms (6.1x) | 6ms (6.0x) | 7ms (5.7x) |
| q4 | 38ms | 10ms (3.9x) | 10ms (3.9x) | 5ms (7.8x) |
| q5 | 8ms | 11ms (0.7x) | 11ms (0.7x) | 5ms (1.7x) |
| q6 | 21ms | 27ms (0.8x) | 27ms (0.8x) | 5ms (4.0x) |
| q7 | 117ms | 11ms (10.5x) | 11ms (10.2x) | 6ms (18.1x) |
| q8 | 2831ms | 7ms (435.3x) | 7ms (428.5x) | 132ms (21.4x) |
| q1 | 1552ms | 138ms (11.2x) | 135ms (11.5x) | 19ms (80.0x) |
| q2 | 395ms | 228ms (1.7x) | 215ms (1.8x) | 41ms (9.7x) |
| q3 | 39ms | 6ms (6.1x) | 6ms (6.0x) | 5ms (8.3x) |
| q4 | 38ms | 10ms (3.9x) | 10ms (3.9x) | 3ms (13.3x) |
| q5 | 8ms | 11ms (0.7x) | 11ms (0.7x) | 3ms (2.9x) |
| q6 | 21ms | 27ms (0.8x) | 27ms (0.8x) | 3ms (6.4x) |
| q7 | 117ms | 11ms (10.5x) | 11ms (10.2x) | 4ms (27.1x) |
| q8 | 2831ms | 7ms (435.3x) | 7ms (428.5x) | 124ms (22.9x) |
| q9 | 2987ms | 86ms (34.7x) | 88ms (34.1x) | 117ms (25.5x) |

![](./results/benchmark_plot.png)

> 🔥 The n-hop path-finding queries (8 and 9) in Kuzu/Ladybug benefit from hybrid joins (WCOJ + bionary) and factorization, which are query processing innovations described in the [Kùzu research paper](https://www.cidrdb.org/cidr2023/papers/p48-jin.pdf).
> 🔥 The n-hop path-finding queries (8 and 9) in Kuzu/Ladybug benefit from hybrid joins (WCOJ + binary) and factorization, which are query processing innovations described in the [Kùzu research paper](https://www.cidrdb.org/cidr2023/papers/p48-jin.pdf).

## Explanation of results

Expand Down
26 changes: 13 additions & 13 deletions lance_graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,34 +216,34 @@ Queries completed in 0.4013s

```
❯ uv run pytest benchmark_query.py --benchmark-min-rounds=5 --benchmark-warmup-iterations=5 --benchmark-disable-gc --benchmark-sort=fullname
========================== test session starts ==========================
============================================ test session starts =============================================
platform darwin -- Python 3.13.7, pytest-9.0.2, pluggy-1.6.0
benchmark: 5.2.3 (defaults: timer=time.perf_counter disable_gc=True min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=5)
rootdir: /Users/prrao/code/graph-benchmark
configfile: pyproject.toml
plugins: anyio-4.12.1, benchmark-5.2.3, asyncio-1.3.0, Faker-40.1.2
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 9 items
collected 9 items

benchmark_query.py ......... [100%]
benchmark_query.py ......... [100%]


------------------------------------------------------------------------------------- benchmark: 9 tests -------------------------------------------------------------------------------------
Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_benchmark_query1 20.6354 (4.85) 25.6317 (4.55) 22.2333 (4.81) 1.2217 (6.93) 21.9964 (4.80) 1.4978 (6.72) 6;2 44.9776 (0.21) 33 1
test_benchmark_query2 41.4012 (9.73) 51.8032 (9.20) 45.2330 (9.79) 2.8306 (16.06) 44.8203 (9.78) 3.7309 (16.73) 8;0 22.1077 (0.10) 20 1
test_benchmark_query3 6.4170 (1.51) 7.9923 (1.42) 6.8688 (1.49) 0.2365 (1.34) 6.8342 (1.49) 0.2836 (1.27) 31;3 145.5860 (0.67) 131 1
test_benchmark_query4 4.4555 (1.05) 5.8523 (1.04) 4.8983 (1.06) 0.2561 (1.45) 4.8509 (1.06) 0.3373 (1.51) 61;4 204.1510 (0.94) 185 1
test_benchmark_query5 4.2564 (1.0) 5.6318 (1.0) 4.6197 (1.0) 0.2092 (1.19) 4.5850 (1.0) 0.2615 (1.17) 48;5 216.4630 (1.0) 221 1
test_benchmark_query6 4.8089 (1.13) 5.9086 (1.05) 5.3321 (1.15) 0.1799 (1.02) 5.3154 (1.16) 0.2409 (1.08) 55;4 187.5428 (0.87) 186 1
test_benchmark_query7 6.1270 (1.44) 6.8842 (1.22) 6.4687 (1.40) 0.1763 (1.0) 6.4630 (1.41) 0.2230 (1.0) 50;0 154.5906 (0.71) 155 1
test_benchmark_query8 128.0999 (30.10) 137.7457 (24.46) 132.2697 (28.63) 3.3987 (19.28) 132.6173 (28.92) 5.3804 (24.13) 3;0 7.5603 (0.03) 8 1
test_benchmark_query9 114.3414 (26.86) 120.1265 (21.33) 117.2609 (25.38) 1.6865 (9.57) 117.1174 (25.54) 1.5854 (7.11) 3;0 8.5280 (0.04) 9 1
test_benchmark_query1 18.0486 (7.20) 22.2868 (7.25) 19.3914 (7.21) 1.0582 (11.89) 19.2554 (7.18) 1.2732 (11.68) 13;1 51.5692 (0.14) 38 1
test_benchmark_query2 36.6314 (14.61) 48.1124 (15.65) 40.7901 (15.16) 3.8781 (43.58) 39.0270 (14.56) 6.4041 (58.75) 5;0 24.5158 (0.07) 20 1
test_benchmark_query3 4.3630 (1.74) 5.2840 (1.72) 4.6917 (1.74) 0.1538 (1.73) 4.6684 (1.74) 0.2108 (1.93) 66;1 213.1407 (0.57) 196 1
test_benchmark_query4 2.6609 (1.06) 3.5896 (1.17) 2.8882 (1.07) 0.1386 (1.56) 2.8485 (1.06) 0.1314 (1.21) 66;23 346.2319 (0.93) 316 1
test_benchmark_query5 2.5078 (1.0) 3.0733 (1.0) 2.6907 (1.0) 0.0890 (1.0) 2.6801 (1.0) 0.1155 (1.06) 100;7 371.6558 (1.0) 345 1
test_benchmark_query6 3.0825 (1.23) 3.6825 (1.20) 3.3007 (1.23) 0.0896 (1.01) 3.2928 (1.23) 0.1090 (1.0) 88;7 302.9681 (0.82) 300 1
test_benchmark_query7 4.1497 (1.65) 4.8740 (1.59) 4.3351 (1.61) 0.1002 (1.13) 4.3246 (1.61) 0.1172 (1.08) 56;5 230.6732 (0.62) 215 1
test_benchmark_query8 120.5493 (48.07) 128.7757 (41.90) 123.6525 (45.96) 2.5810 (29.00) 123.3281 (46.02) 3.6186 (33.20) 2;0 8.0872 (0.02) 9 1
test_benchmark_query9 109.1843 (43.54) 132.9462 (43.26) 116.9727 (43.47) 6.2596 (70.34) 116.3760 (43.42) 4.0111 (36.80) 2;1 8.5490 (0.02) 10 1
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Legend:
Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
OPS: Operations Per Second, computed as 1 / Mean
========================== 9 passed in 10.82s ===========================
============================================= 9 passed in 10.65s =============================================
```
44 changes: 28 additions & 16 deletions lance_graph/benchmark_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
```
uv run pytest benchmark_query.py --benchmark-min-rounds=5 --benchmark-warmup-iterations=5 --benchmark-disable-gc --benchmark-sort=fullname
```

Note: `query.get_engine()` uses an identity-based, module-level cache intended
for short-lived benchmark runs (session-scoped fixture). It's not designed as a
general-purpose cache for long-running processes.
"""

from pathlib import Path
Expand All @@ -20,43 +24,48 @@ def graph_context():
raise RuntimeError("Missing graph_lance data. Run build_graph.py first.")
cfg = query.build_config()
datasets = query.load_datasets(query.GRAPH_ROOT)
return cfg, datasets
engine = None
if getattr(query, "CypherEngine", None) is not None:
engine = query.get_engine(cfg, datasets)
# Verify engine caching returns the same instance.
assert engine is query.get_engine(cfg, datasets)
return cfg, datasets, engine


def test_benchmark_query1(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query1, cfg, datasets)
cfg, datasets, engine = graph_context
result = benchmark(query.run_query1, cfg, datasets, engine)
result = result.to_dicts()

assert len(result) == 3


def test_benchmark_query2(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query2, cfg, datasets)
cfg, datasets, engine = graph_context
result = benchmark(query.run_query2, cfg, datasets, engine)
result = result.to_dicts()

assert len(result) == 1


def test_benchmark_query3(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query3, cfg, datasets, {"country": "United States"})
cfg, datasets, engine = graph_context
result = benchmark(query.run_query3, cfg, datasets, {"country": "United States"}, engine)
result = result.to_dicts()

assert len(result) == 5


def test_benchmark_query4(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query4, cfg, datasets, {"age_lower": 30, "age_upper": 40})
cfg, datasets, engine = graph_context
result = benchmark(query.run_query4, cfg, datasets, {"age_lower": 30, "age_upper": 40}, engine)
result = result.to_dicts()

assert len(result) == 3


def test_benchmark_query5(benchmark, graph_context):
cfg, datasets = graph_context
cfg, datasets, engine = graph_context
result = benchmark(
query.run_query5,
cfg,
Expand All @@ -67,14 +76,15 @@ def test_benchmark_query5(benchmark, graph_context):
"country": "United Kingdom",
"interest": "Fine Dining",
},
engine,
)
result = result.to_dicts()

assert len(result) == 1


def test_benchmark_query6(benchmark, graph_context):
cfg, datasets = graph_context
cfg, datasets, engine = graph_context
result = benchmark(
query.run_query6,
cfg,
Expand All @@ -83,14 +93,15 @@ def test_benchmark_query6(benchmark, graph_context):
"gender": "female",
"interest": "Tennis",
},
engine,
)
result = result.to_dicts()

assert len(result) == 5


def test_benchmark_query7(benchmark, graph_context):
cfg, datasets = graph_context
cfg, datasets, engine = graph_context
result = benchmark(
query.run_query7,
cfg,
Expand All @@ -101,23 +112,24 @@ def test_benchmark_query7(benchmark, graph_context):
"age_upper": 30,
"interest": "Photography",
},
engine,
)
result = result.to_dicts()

assert len(result) == 1


def test_benchmark_query8(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query8, cfg, datasets)
cfg, datasets, engine = graph_context
result = benchmark(query.run_query8, cfg, datasets, engine)
result = result.to_dicts()

assert len(result) == 1


def test_benchmark_query9(benchmark, graph_context):
cfg, datasets = graph_context
result = benchmark(query.run_query9, cfg, datasets, {"age_1": 50, "age_2": 25})
cfg, datasets, engine = graph_context
result = benchmark(query.run_query9, cfg, datasets, {"age_1": 50, "age_2": 25}, engine)
result = result.to_dicts()

assert len(result) == 1
Loading