Seeded Traversal Indexes (CSR Adjacency)#
A seeded graph query starts from a known set of nodes — “the neighbors of these
50 accounts”, “2 hops out from this device” — rather than scanning the whole graph.
By default GFQL answers a seeded hop with an O(E) pass over every edge. With an
opt-in CSR adjacency index, the same hop becomes an O(degree) gather: its cost
depends on how many edges the seeds touch, not on how big the graph is — so a seeded
lookup stays interactive as the graph grows.
Nothing changes about the answer. The index is a pay-as-you-go accelerator: a query either uses a resident index or falls back to the scan, and any feature the index does not cover also falls back — never a different result.
When to use it#
Seeded traversals: you start from specific node ids (a watchlist, a session, a fraud ring’s known members) and hop out 1–3 steps.
Repeated queries against the same graph: build the index once, amortize it over many seeded lookups.
Interactive / point-lookup latency: neighbor expansion whose cost tracks the seeds rather than the graph.
It does not help a full-graph scan (a property filter over every node, a global PageRank). For those, choose an engine instead — see Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU.
Quick start#
import graphistry
from graphistry import n, e_forward, is_in
g = graphistry.edges(edges_df, "src", "dst").nodes(nodes_df, "id")
# Build the indexes once (out+in adjacency, plus a node-id accelerator when ids are unique)
g = g.gfql_index_all()
# Seeded traversal — the index is used automatically (default index_policy='use')
my_seed_ids = ["a", "b"] # your seed node ids
out = g.gfql([n({"id": is_in(my_seed_ids)}), e_forward(), n()])
gfql_index_all() is the one-liner. For finer control, build a single kind:
g = g.create_index("edge_out_adj") # outgoing adjacency (forward hops)
g = g.create_index("edge_in_adj") # incoming adjacency (reverse hops)
g = g.create_index("node_id") # node-id lookup accelerator (unique ids only)
g = g.gfql_index_col_stats() # verified column-stat facts (see below)
g.show_indexes() # inspect what's resident
g = g.drop_index() # drop all (or drop_index("edge_out_adj"))
The index is a sidecar over edge row positions — it never reorders your .edges /
.nodes frames, and it is fingerprint-validated: rebinding .edges() safely
invalidates a stale index (treated as absent, never a wrong answer).
Column-stat facts#
gfql_index_col_stats() records verified facts (min/max/null count; integer
columns in v1) for the bound node id and edge endpoint columns — the columns
count-shaped query plans consult. Fast paths use them as under-approximations of
provability: a valid fact can prove a per-query invariant (e.g. every filtered edge
endpoint lies inside a dense id interval) and skip the O(E) scan that would re-prove
it; a missing or insufficient fact just means the scan runs. A fact can therefore
save work but never change an answer. Facts follow the same fingerprint validity
contract as the physical indexes, and gfql_index_all() includes them. Pass
node_columns= / edge_columns= to fact additional integer columns —
explicitly named columns raise if they can’t be fact-ed, while the binding defaults
skip silently.
Controlling the planner#
gfql(..., index_policy=...) decides whether a resident index is used:
|
Behavior |
|---|---|
|
Use a resident index when one covers the query; never build one. Zero overhead if no index exists. |
|
Build an index on the fly when the planner predicts it pays off (selective seed set). |
|
Require the index path (useful for benchmarking / asserting it is engaged). |
|
Ignore indexes entirely (the plain |
Use g.gfql_explain(query, index_policy=...) to see whether the index path was taken.
The indexes are engine-uniform: numpy host arrays for pandas / Polars, cupy on-device
for cuDF. They are also exposed as Cypher DDL (CREATE GFQL INDEX FOR edge_out_adj,
DROP GFQL INDEX, SHOW GFQL INDEXES — the mandatory GFQL token distinguishes them
from standard property CREATE INDEX) and in the JSON wire protocol
({"type": "CreateIndex", ...} ops plus index_policy in the request envelope), so a
remote gfql_remote call can carry the same index intent.
Performance#
The index changes the complexity class. An indexed seeded hop is an O(degree)
gather into a sorted adjacency, so its cost tracks the size of the seeds’ neighborhood.
The default scan is O(E) and grows with the whole graph. The bigger the graph is
relative to the seeds’ neighborhood, the wider that gap.
Selective traversal is CPU’s game. The indexed hop is tiny work, so a GPU engine’s
kernel-launch floor dominates it and a CPU engine — pandas or Polars, both backed by a
searchsorted gather — wins. That is the clean inverse of bulk analytics, where the
GPU pulls ahead (see Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU). Pick the index for selective traversal and a CPU
engine to drive it.
Latency figures for this lane are not published yet: it has not been run under the
provenance-carrying harness described on GFQL Performance: Vectorization and GPU Acceleration, and this page publishes
nothing it cannot trace to a committed artifact. Reproducers:
benchmarks/gfql/index_takeover_bench.py, benchmarks/gfql/index_vs_dbs.py,
benchmarks/gfql/index_vs_kuzu_prepared.py.
Honesty and cost#
Build cost is one
O(E log E)sort, amortized over subsequent queries.index_policy='auto'only builds when the planner predicts a selective query will pay it back.No change to default behavior. With no index resident and
index_policy='use'(the default), queries run exactly as before.Parity-or-fallback. The index accelerates the seeded scan sites it covers (forward / reverse hop, the Polars hop, the single-hop chain fast path). Any uncovered feature — edge / source / destination match,
target_wave_front,min_hops>1, labeling — falls back to the scan/join path. The indexed subgraph is verified equal to the scan subgraph in differential tests across pandas / cuDF / Polars / Polars-GPU. It is an accelerator, never a source of a different answer.
See also#
Choosing a GFQL Engine: pandas, Polars, cuDF, Polars-GPU — choosing pandas / Polars / cuDF / Polars-GPU for non-seeded work.
GFQL Performance: Vectorization and GPU Acceleration — the vectorization + GPU design behind GFQL.
GFQL Cypher Benchmark: CPU/GPU DataFrames vs Neo4j — an end-to-end filter → PageRank → filter comparison vs Neo4j.