Speedup Case Study: Cypher + PageRank, GFQL vs Neo4j + GDS

Speedup Case Study: Cypher + PageRank, GFQL vs Neo4j + GDS#

GFQL mascot

Real graph work is usually a pipeline, not one query: select a subgraph, run an algorithm over it, then select from the result. This case study times that shape, filter then PageRank then filter, against Neo4j + Graph Data Science.

GFQL turns minutes into seconds, and seconds into subseconds. On GPlus (30M edges) the pipeline drops from 354.47 s on Neo4j + GDS to 34.11 s on CPU. On Twitter (2.4M edges) it drops from 11.72 s to 2.62 s on CPU, and to 0.23 s on GPU. Both systems select the same nodes.

Neo4j + GDS

GFQL Cypher (CPU)

GFQL Cypher (GPU)

GFQL GPU vs CPU

Twitter (81,306 nodes / 2.4M edges)

11.72 s

2.62 s

0.23 s

11.3x

GPlus (107,614 nodes / 30M edges)

354.47 s

34.11 s

2.47 s (diagnostic)

not published (selection differs)

Twitter warm pipeline time: Neo4j + GDS 11.72 s, GFQL Cypher CPU 2.62 s, GFQL Cypher GPU 0.23 s GPlus warm pipeline time: Neo4j + GDS 354.47 s, GFQL Cypher CPU 34.11 s, GFQL Cypher GPU 2.47 s (diagnostic)

The pipeline#

A three-phase graph pipeline: filter, run PageRank, filter again. The query is standard Cypher extended with GFQL’s graph pipeline syntax. Each GRAPH { } block takes a graph in and passes a graph on.

# pip install graphistry
result = g.gfql("""
    GRAPH g1 = GRAPH {
      MATCH (n)-[e]-(m)
      WHERE n.degree >= $degree_cutoff
    }
    GRAPH g2 = GRAPH {
      USE g1
      CALL graphistry.cugraph.pagerank.write()
    }
    GRAPH {
      USE g2
      MATCH (n)-[e]-(m)
      WHERE n.pagerank >= $pagerank_cutoff
    }
""",
    params={
        "degree_cutoff": degree_cutoff,
        "pagerank_cutoff": pagerank_cutoff,
    },
    engine="cudf",  # or "pandas" with igraph backend
)
  • GRAPH g1: keep high-degree nodes and their neighbors.

  • GRAPH g2: add PageRank scores to g1 (igraph on CPU, cugraph on GPU).

  • Final GRAPH: keep high-PageRank nodes and their neighbors.

The query does not change between engines:

  • CPU: engine="pandas", backend="igraph"

  • GPU: engine="cudf", backend="cugraph"

Intermediate graphs stay in Arrow, pandas, or cuDF memory in the same Python process. GFQL returns the same result on every engine or rejects the query before execution; see Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU.

Neo4j + GDS analog#

The Neo4j version writes marker properties at each stage and projects a separate in-memory graph for GDS:

// 1. Mark seed nodes by degree
MATCH (n:Node)
SET n.seed = n.degree >= $cutoff;

// 2. Expand one hop from seeds
UNWIND $seed_ids AS sid
MATCH (s:Node) WHERE id(s) = sid
MATCH (s)-[r:LINK]-(target:Node)
SET target.in_subgraph = true, r.in_subgraph = true;

// 3. Project subgraph and run PageRank
CALL gds.graph.project.cypher(
  'subgraph',
  'MATCH (n:Node) WHERE n.in_subgraph RETURN id(n) AS id',
  'MATCH (a)-[r:LINK]->(b) WHERE r.in_subgraph
   RETURN id(a) AS source, id(b) AS target
   UNION ALL
   MATCH (a)-[r:LINK]->(b) WHERE r.in_subgraph
   RETURN id(b) AS source, id(a) AS target'
);
CALL gds.pageRank.write('subgraph', {writeProperty: 'pagerank'});

// 4. Keep high-PageRank core + one hop
MATCH (n:Node) WHERE n.pagerank >= $cutoff
SET n.core = true;
UNWIND $core_ids AS cid
MATCH (c:Node) WHERE id(c) = cid
MATCH (c)-[r:LINK]-(target:Node)
SET target.final = true, r.final = true;

Method and limits#

  • Workload: one pipeline (filter, PageRank, filter) on two SNAP graphs. Selected-node agreement is measured (Jaccard, threshold 0.95): on Twitter the GFQL CPU arm matches Neo4j at 0.9999; on GPlus the GFQL CPU arm selects exactly the reference run’s set; the GPlus GPU arm is at 0.91 against CPU and is therefore diagnostic-only (see the caveats below).

  • Timing: warm runs after warm-up (2 warm-ups, 5 timed runs, median). The GFQL arms were measured at the release commit named in the Measurement block; the Twitter Neo4j arm is the 2026-07-28 measurement, and the GPlus Neo4j time comes from a later locked run of twelve position-balanced slots on one machine. The Measurement block below records every run.

  • Profiles differ: GFQL reuses frames already resident in Python. Neo4j includes server round trips, writes marker properties in both filter stages, and rebuilds the GDS in-memory projection on every timed iteration. The Neo4j column is therefore a direct pipeline time, not an engine-primitive time. The page states which system finished first but publishes no GFQL-vs-Neo4j ratio.

  • Comparable ratio: the GPU-vs-CPU column compares the same GFQL query and the same profile, so that ratio is published.

  • Scope: for the four-engine CPU/GPU comparison and engine choice, see Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU. For queries from known nodes, see When indexes help. For the Spark GraphFrames comparison, see GFQL vs Apache Spark GraphFrames on One Node.

Provenance#

Every figure on this page is printed from docs/source/_data/gfql_benchmarks.json, which pyg-bench publishes.

Measurement

Measured:

2026-09-04 and 2026-08-30

Host:
  • 2026-09-04: dgx-spark (NVIDIA GB10), 20 CPU

  • 2026-08-30: dgx-spark (NVIDIA GB10, driver 580.173.02), 20 CPU

Notes:
  • The Neo4j arm writes marker properties during both filter stages and rebuilds its GDS projection per timed iteration; the paired GFQL arm retains resident frames and writes nothing. Twelve position-balanced slots (six per arm) produced exact selected-node parity, but the measurement profiles differ, so this is a direct pipeline time and no GFQL-vs-Neo4j ratio is published.

  • In the Neo4j arm each filter stage writes a marker property onto every matched node and relationship, 32 seeds per batch, and GDS scores an explicitly symmetrised projection; the GDS in-memory projection is rebuilt per timed iteration while the GFQL arms retain their resident frames. The GFQL arms materialise a subgraph and write nothing. The figure is a pipeline time, not an engine-primitive time.

  • The Twitter Neo4j + GDS arm was measured on 2026-07-28 and is reused unchanged; the GFQL arms were re-measured on 2026-09-04 at PyGraphistry 0.59.0 under the pinned PageRank contract.

  • Its selected-node set differs from the GFQL CPU (pandas + igraph) arm’s on gplus (Jaccard 0.91065, gate 0.95); this is a direct pipeline time, not a comparison.