diff --git a/.gitignore b/.gitignore index d564780..e683baf 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ venv/ data/ work/ results/ +tmp/ # Other files and directories to ignore .DS_Store @@ -19,3 +20,6 @@ results/ .codex .idea/ .vscode/ +*.mdb +*.idb +*.lbdb diff --git a/AGENTS.md b/AGENTS.md index 34b86ec..0901e6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Priorities, in order: - Use Oxford commas in inline lists: "a, b, and c" not "a, b, c". - Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead. - Avoid colorful adjectives and adverbs. Write "instruction decoder" not "elegant instruction decoder". -- Use noun phrases for checklist items, not imperative verbs. Write "opcode timing table" not "build the opcode timing table". +- Prefer using noun phrases for checklist items, not imperative verbs. Write "opcode timing table" not "build the opcode timing table". - Headings in Markdown files must be in title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, on, at, to, by, of) stay lowercase unless they are the first word. diff --git a/Makefile b/Makefile index d111ea5..587765e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -SCALE ?= 10000 +SCALE ?= 100000 SEED ?= 0 ENGINES ?= issundb,ladybug,lance-graph,neo4j -SCALES ?= 1000,10000,100000 +SCALES ?= 10000,100000,300000 MIN_ROUNDS ?= 20 TIME_BUDGET ?= 2.0 WARMUP ?= 3 @@ -10,10 +10,10 @@ WARMUP ?= 3 help: @echo "graphbench targets:" - @echo " make gen SCALE=10000 # Generate a synthetic graph dataset (SCALE=number of Person nodes)" + @echo " make gen SCALE=100000 # Generate a synthetic graph dataset (SCALE=number of Person nodes)" @echo " make engines # List which graph database engines are available" @echo " make run [ENGINES=issundb,ladybug] # Run the benchmark for specified engines (default: all)" - @echo " make sweep [SCALES=1000,10000,100000] # Benchmark a series of scales and plot scaling curves" + @echo " make sweep [SCALES=10000,100000,300000] # Benchmark a series of scales and plot scaling curves" @echo " make report # Generate a report from the results in the results/ directory" @echo " make test # Run the unit tests" @echo " make neo4j-up or neo4j-down # Start and stop the Neo4j container" diff --git a/README.md b/README.md index 3e1cb40..5046cb7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ against IssunDB. | 3 | **Lance-graph** | [lancedb/lance-graph](https://github.com/lancedb/lance-graph) | | 4 | **Neo4j** | [neo4j.com](https://neo4j.com/) | +> [!NOTE] +> Technically `Lance-graph` is not a graph database, but an in-memory graph query engine over Apache Arrow tables. +> In this repository when the word `engine` or `graph engine` are used, it referse to `Lance-graph` plus the other graph databases in the table above. + ### Schema and Queries #### Benchmark Graph Dataset @@ -57,36 +61,28 @@ See the [query definitions](graphbench/queries.py) for more details. ### Methodology -The benchmarks are created, so the published numbers are reproducible and hard to manipulate. -That's achieved by: - -- **Engine-independent correctness oracle.** Every query is independently re-implemented in - [`graphbench/oracle.py`](graphbench/oracle.py) with polars over the raw Parquet dataset. Each engine's - result rows (over several parameter instantiations) are diffed against the oracle; no engine, including - IssunDB, is ever used as the reference. Mismatches are reported, never silently omitted from timing. -- **Process isolation.** Each engine is built and timed in its own worker process - ([`graphbench/_worker.py`](graphbench/_worker.py)), so heap state, allocator fragmentation, and caches - never leak between engines, and the peak RSS reported per engine is attributable to that engine alone. -- **Statistics.** Per query: a cold run (first execution after build) is reported separately; timed rounds - run with the garbage collector disabled until both a minimum round count and a time budget are met; the - report shows median latency with a distribution-free 95% confidence interval for the median (the - order-statistic method, not a normal approximation on the mean), and the plot carries p25 to p75 whiskers. -- **Honest comparisons.** Engines are labeled by kind (embedded / in-memory / client-server) and ingestion - method; load times are never ranked across kinds, the client-server network round-trip caveat is stated - in every report, and Neo4j's server memory settings are captured from the live server into the results. -- **Indexing differences.** Index models differ by engine and cannot be fully equalized: IssunDB - auto-indexes every scalar property, Neo4j uses a uniqueness index on `id` plus an explicit range index - on the filtered column, Ladybug indexes only its primary key, and lance-graph holds no index. The report - spells this out so a filtered-query result is read as the engine's indexing model, not raw speed alone. -- **Determinism.** The dataset is generated from a single seed, byte-for-byte reproducible, with edge rows - shuffled so no engine gains a locality advantage from sorted insertion order. Hardware (CPU model, cores, - and RAM) is recorded in every result file. -- **Scaling.** `make sweep` benchmarks a series of dataset scales and plots median latency vs scale per - query, so results are never a single-scale snapshot. - -Known limitations (deliberately out of scope so far): the suite measures single-threaded read-only latency; -no concurrent throughput and no write/update workloads. Engines may not all support every query -(e.g. variable-length patterns); unsupported queries show as `ERR` in the report rather than being dropped. +To ensure reproducible, objective, and comparable performance metrics, the benchmark suite follows these practices: + +- **Correctness Oracle**: Every query is re-implemented in [`graphbench/oracle.py`](graphbench/oracle.py) using Polars. Engine result rows are diffed + against this oracle to verify correctness before timing, and mismatching queries fail validation. +- **Process Isolation**: Each engine executes queries in a dedicated worker process ([`graphbench/_worker.py`](graphbench/_worker.py)) to prevent + cache, allocator, and heap contamination. +- **Statistical Rigor**: Query timing runs with the garbage collector disabled until a minimum round count and a time budget are met. Reports display + the median latency, a distribution-free 95% confidence interval, and p25 to p75 error bars. Cold runs are measured and reported separately. +- **Categorization**: Engines are categorized by architecture (embedded, in-memory, or client-server) and ingestion method. Latency reports include + network round-trip caveats for client-server engines and log live server settings. +- **Index Disclosure**: Engine index models are documented (such as IssunDB auto-indexing, Neo4j range indexing, LadybugDB primary key indexing, and + Lance-graph no-indexing) to provide context for query latency differences. +- **Determinism**: Datasets are generated from a single seed, and edge rows are shuffled to eliminate insertion-order locality benefits. CPU, core + count, and RAM specifications are saved with every run. +- **Multi-Scale Scaling**: The suite measures scaling characteristics by running a sweep across dataset sizes rather than relying on a single-point + snapshot. + +#### Scope and Limitations + +The suite currently measures single-threaded read-only latency. +Concurrent throughput, write workloads, and update workloads are out of scope. +Unsupported queries are reported as errors rather than being omitted. > [!IMPORTANT] > Benchmarking different systems (with different design philosophies, architectures, feature sets, etc.) is not straightforward and is tricky. diff --git a/graphbench/engines/issundb_engine.py b/graphbench/engines/issundb_engine.py index 7b55ae8..3992c67 100644 --- a/graphbench/engines/issundb_engine.py +++ b/graphbench/engines/issundb_engine.py @@ -36,7 +36,7 @@ def __init__(self, schema: Schema, workdir: Path): self._db_path = workdir / "social.issundb" if self._db_path.exists(): shutil.rmtree(self._db_path) - self._db = IssunDB(str(self._db_path)) + self._db = IssunDB(str(self._db_path), map_size_gb=16) @classmethod def probe(cls) -> EngineInfo: diff --git a/graphbench/engines/neo4j_engine.py b/graphbench/engines/neo4j_engine.py index 6f69906..c794b1c 100644 --- a/graphbench/engines/neo4j_engine.py +++ b/graphbench/engines/neo4j_engine.py @@ -72,7 +72,7 @@ def probe(cls) -> EngineInfo: def _reset(self) -> None: # Batched delete so a wipe at large scales does not exhaust the heap. self._session.run( - "MATCH (n) CALL { WITH n DETACH DELETE n } IN TRANSACTIONS OF 50000 ROWS" + "MATCH (n) CALL (n) { DETACH DELETE n } IN TRANSACTIONS OF 50000 ROWS" ) for label in self.schema.nodes: self._session.run( diff --git a/pyproject.toml b/pyproject.toml index 61f9819..0ee824b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "pyarrow>=17.0", "matplotlib>=3.9", "polars>=1.0", - "issundb>=0.1.0a8", + "issundb>=0.1.0a9", "patchelf>=0.17.2", ] @@ -16,7 +16,7 @@ dependencies = [ ladybug = ["ladybug>=0.17"] lance-graph = ["lance-graph>=0.5"] neo4j = ["neo4j>=5.0"] -all = ["ladybug>=0.15", "lance-graph>=0.5", "neo4j>=5.0"] +all = ["ladybug>=0.17", "lance-graph>=0.5", "neo4j>=5.0"] [dependency-groups] dev = ["pytest>=8.0"] diff --git a/uv.lock b/uv.lock index cf13da7..3458a53 100644 --- a/uv.lock +++ b/uv.lock @@ -689,7 +689,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.137.2" +version = "0.138.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -698,9 +698,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/29/cc5819dc24d3daa80cdaa1aec023bf8652a70dd7fd1c96b0b225c99a7690/fastapi-0.137.2.tar.gz", hash = "sha256:b9d893bebc97dcfbdcb1917e88a292d062844ea19445a5fa4f7eb28c4baea9e3", size = 410332, upload-time = "2026-06-18T06:58:24.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/58/ff455d9fe47c60abadb34b9e05a304b1f05f5ab8000ac01565156b6f5e43/fastapi-0.138.0.tar.gz", hash = "sha256:d445a4877636ad191e7053e08c9bf98cb921a6756776848400bb773d1740c061", size = 419240, upload-time = "2026-06-20T01:18:05.259Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/ed/0c6b644e99fb5697d8bdcd36cdb47c52e77a63fc7a1514b1f03a6ecab955/fastapi-0.137.2-py3-none-any.whl", hash = "sha256:791d36261e916a98b25ac85ee591bc3db159394070f6d3d096d94fb378f60ce2", size = 122252, upload-time = "2026-06-18T06:58:26.074Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ff/8496d9847a5fedae775eb49460722d3efaa80487854273e9647ae876218c/fastapi-0.138.0-py3-none-any.whl", hash = "sha256:b6f54fd1bd72c80b0f899f172c61a600f6f7af9b43d4d772a018f35624048cb0", size = 126779, upload-time = "2026-06-20T01:18:03.483Z" }, ] [[package]] @@ -1106,8 +1106,8 @@ dev = [ [package.metadata] requires-dist = [ - { name = "issundb", specifier = ">=0.1.0a8" }, - { name = "ladybug", marker = "extra == 'all'", specifier = ">=0.15" }, + { name = "issundb", specifier = ">=0.1.0a9" }, + { name = "ladybug", marker = "extra == 'all'", specifier = ">=0.17" }, { name = "ladybug", marker = "extra == 'ladybug'", specifier = ">=0.17" }, { name = "lance-graph", marker = "extra == 'all'", specifier = ">=0.5" }, { name = "lance-graph", marker = "extra == 'lance-graph'", specifier = ">=0.5" }, @@ -1270,13 +1270,13 @@ wheels = [ [[package]] name = "issundb" -version = "0.1.0a8" +version = "0.1.0a10" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/fa/c9d8778fca8f917bd0ba13e102bd64e710738939ea8937dd8b562f1504b8/issundb-0.1.0a8-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:6ccb62740d4f5c866a81a8dccd1134085e75c06ffed9e9c1d1cdac1837c37280", size = 6829438, upload-time = "2026-06-19T09:31:41.397Z" }, - { url = "https://files.pythonhosted.org/packages/03/ce/18691354d7639b50a917ea14a721ec9f5ddea246f73225461ae63bbfe4fe/issundb-0.1.0a8-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f6cbc83b0d5424975291ce175e826d188bd853153a09d79a425d44697cfd4ac8", size = 8956521, upload-time = "2026-06-19T09:31:42.729Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f4/ddea84c25c616e6f55b2fb5c6cfc36ee5df3088a2cc9448d3e0c2fffbdab/issundb-0.1.0a8-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74621c47a12c7e4635867738a226be57c4791c18abd8d1c31185e614989b8129", size = 10063051, upload-time = "2026-06-19T09:31:44.447Z" }, - { url = "https://files.pythonhosted.org/packages/36/9d/dd1a92996e4a88b6d2a6520ccc1e68a0c5cc62036cd619ae7d96241c29e1/issundb-0.1.0a8-cp310-abi3-win_amd64.whl", hash = "sha256:d6fa280acccf019e4af51161ba5e0c990d4b0187bd75215b3ed1ad9a6881bd74", size = 8301712, upload-time = "2026-06-19T09:31:46.308Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4f/1b46c123ff8516a8ee3f68c75d85cfd2b73ae66ba3e3b81721cdbc4a9104/issundb-0.1.0a10-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2b79c997bb5179d577235529b55088de6743d05e705e8607bce41004e13b9774", size = 6892881, upload-time = "2026-06-21T09:40:39.144Z" }, + { url = "https://files.pythonhosted.org/packages/1e/fd/df2342a47e7b8198a7a160e213e9d59a934773d923e5e03dd3cf5c02aa03/issundb-0.1.0a10-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3ba17acd6bd35724095b51708cc177ccc749a50723fe2af108b077c003f483a8", size = 9034621, upload-time = "2026-06-21T09:40:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/3f/02/618080f6ca322b2d69979b8ff6a0642625ed861ab7f1cf8151c6ca91396c/issundb-0.1.0a10-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2d192a6048d9366d653480c6fdac8a26635e58c2e1a8bdc10a7baf98fb93333b", size = 10143203, upload-time = "2026-06-21T09:40:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/45c7b612f170cf9d3f89432a2d21f697f184c6c1a3ae83471585d8f3bc66/issundb-0.1.0a10-cp310-abi3-win_amd64.whl", hash = "sha256:b58ed9c2bf672fec14454f86be74676a68c8ac565a2cc4567e6fbadc59a807f2", size = 8375827, upload-time = "2026-06-21T09:40:45.757Z" }, ] [[package]] @@ -2645,7 +2645,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2656,9 +2656,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]]