Faster graph queries with GFQL indexes#

Run the examples from the indexing release article on a local sample graph. This notebook covers all five graph index kinds, index creation inside a graph pipeline, and indexed lookups on business keys. No Graphistry account or graph database is required.

Install the latest PyGraphistry release with Polars support in your notebook environment:

python -m pip install --upgrade "graphistry[polars]"

With uv, use uv pip install --upgrade "graphistry[polars]".

See the indexing guide for the supported query forms.

Set up accounts and transfers#

Choose pandas or polars. The same examples run on both.

[2]:
engine = 'pandas'
[3]:
import pandas as pd
import graphistry

nodes = pd.DataFrame({
    "id": range(10000),
    "account_number": range(48112, 58112),
})
edges = pd.DataFrame({
    "src": range(9999),
    "dst": range(1, 10000),
    "transaction_id": [f"txn-{i}" for i in range(9999)],
})
if engine == "polars":
    import polars as pl
    nodes = pl.from_pandas(nodes)
    edges = pl.from_pandas(edges)

g = graphistry.edges(edges, "src", "dst").nodes(nodes, "id")
g = g.gfql_index_all()
[4]:
g.show_indexes()[["kind", "key_col", "valid"]]
[4]:
kind key_col valid
0 edge_in_adj dst True
1 edge_out_adj src True
2 node_id id True

Create each index and use it#

Outgoing and incoming hops use the adjacency indexes alongside the node-id index built above.

edge_out_adj#

Outgoing hops from known nodes.

[5]:
g.gfql(
    'CREATE GFQL INDEX FOR edge_out_adj; MATCH (a {id: 101})-->(b) RETURN b'
)._nodes
[5]:
b.id b.account_number
102 102 48214

edge_in_adj#

Incoming hops to known nodes.

[6]:
g.gfql(
    'CREATE GFQL INDEX FOR edge_in_adj; MATCH (a {id: 102})<--(b) RETURN b'
)._nodes
[6]:
b.id b.account_number
101 101 48213

node_id#

Lookups on the graph’s node id column.

[7]:
g.gfql(
    'CREATE GFQL INDEX FOR node_id; MATCH (a {id: 101}) RETURN a'
)._nodes
[7]:
a.id a.account_number
101 101 48213

node_prop#

Filters on a node property.

[8]:
g.gfql(
    'CREATE GFQL INDEX FOR node_prop ON (account_number); MATCH (a {account_number: 48213})-->(b) RETURN b'
)._nodes
[8]:
b.id b.account_number
102 102 48214

edge_prop#

Filters on an edge property.

[9]:
g.gfql(
    "CREATE GFQL INDEX FOR edge_prop ON (transaction_id); MATCH (a)-[e {transaction_id: 'txn-101'}]->(b) RETURN b"
)._nodes
[9]:
b.id b.account_number
0 102 48214

Start from a list of accounts#

[10]:
account_ids = list(range(101, 151))
query = f"MATCH (u)-[e]->(other) WHERE u.id IN {account_ids} RETURN other"
contacts = g.gfql(query)
contacts._nodes
[10]:
other.id other.account_number
0 102 48214
1 103 48215
2 104 48216
3 105 48217
4 106 48218
5 107 48219
6 108 48220
7 109 48221
8 110 48222
9 111 48223
10 112 48224
11 113 48225
12 114 48226
13 115 48227
14 116 48228
15 117 48229
16 118 48230
17 119 48231
18 120 48232
19 121 48233
20 122 48234
21 123 48235
22 124 48236
23 125 48237
24 126 48238
25 127 48239
26 128 48240
27 129 48241
28 130 48242
29 131 48243
30 132 48244
31 133 48245
32 134 48246
33 135 48247
34 136 48248
35 137 48249
36 138 48250
37 139 48251
38 140 48252
39 141 48253
40 142 48254
41 143 48255
42 144 48256
43 145 48257
44 146 48258
45 147 48259
46 148 48260
47 149 48261
48 150 48262
49 151 48263

Build and query in one call#

[11]:
g.gfql("CREATE GFQL INDEX IF NOT EXISTS FOR edge_out_adj; "
       "CREATE GFQL INDEX IF NOT EXISTS FOR node_id; "
       "MATCH (u)-[e]->(other) WHERE u.id IN [101, 102] RETURN other")
[11]:
{'bindings': {'edges': Empty DataFrame
  Columns: [e, src, dst, transaction_id]
  Index: [],
  'nodes':    other.id  other.account_number
  0       102                 48214
  1       103                 48215,
  'source': 'src',
  'destination': 'dst',
  'node': 'id',
  'edge_label': None,
  'edge_color': None,
  'edge_size': None,
  'edge_weight': None,
  'edge_title': None,
  'edge_icon': None,
  'edge_opacity': None,
  'edge_source_color': None,
  'edge_destination_color': None,
  'point_label': None,
  'point_color': None,
  'point_size': None,
  'point_weight': None,
  'point_title': None,
  'point_icon': None,
  'point_opacity': None,
  'point_x': None,
  'point_y': None},
 'settings': {'height': 500, 'url_params': {'info': 'true'}}}

Index an intermediate graph#

[12]:
# Narrow the graph, index it, then reuse it for a hop and degree.
pipeline_ids = list(range(101, 201))
ring = g.gfql(
    f"GRAPH reached = GRAPH {{ MATCH (u)-[e1]->(mid)-[e2]->(other) WHERE u.id IN {pipeline_ids} }} "
    "GRAPH adjacency = GRAPH { USE reached CALL graphistry.create_index.write({kind: 'edge_out_adj'}) } "
    "GRAPH indexed = GRAPH { USE adjacency CALL graphistry.create_index.write({kind: 'node_id'}) } "
    "GRAPH contacts = GRAPH { USE indexed MATCH (u {id: 101})-[e]->(other) } "
    "GRAPH { USE contacts CALL graphistry.degree.write() }"
)
ring._nodes
[12]:
id mid account_number u other degree_in degree_out degree
0 101 False 48213 True False 0 1 1
1 102 True 48214 False True 1 0 1

Look up business keys#

[13]:
account_contacts = g.gfql(
    "CREATE GFQL INDEX IF NOT EXISTS FOR edge_out_adj; "
    "CREATE GFQL INDEX IF NOT EXISTS FOR node_id; "
    "CREATE GFQL INDEX IF NOT EXISTS FOR node_prop ON (account_number); "
    "MATCH (u {account_number: 48213})-[e]->(other) RETURN other"
)
account_contacts._nodes

transactions = g.gfql(
    "CREATE GFQL INDEX IF NOT EXISTS FOR edge_prop ON (transaction_id); "
    "MATCH (u)-[e {transaction_id: 'txn-101'}]->(other) RETURN other"
)
transactions._nodes
[13]:
other.id other.account_number
0 102 48214

Check index use#

[14]:
report = g.gfql_explain("MATCH (u {id: 101})-[e]->(other) RETURN other")
report["used_index"]   # True
[14]:
True

Other property keys#

The same property index kinds support categorical labels, exact floats and timestamps. These examples keep the graph’s integer ids and index the business columns separately.

[15]:
key_nodes = pd.DataFrame({
    "id": range(10000),
    "segment": pd.Categorical([f"segment-{i}" for i in range(10000)]),
    "amount": [i + 0.5 for i in range(10000)],
    "event_time": pd.date_range("2024-01-01", periods=10000, freq="min"),
})
if engine == "polars":
    key_nodes = pl.from_pandas(key_nodes)
key_graph = graphistry.nodes(key_nodes, "id")
for column in ["segment", "amount", "event_time"]:
    key_graph = key_graph.create_index("node_prop", column=column)

key_graph.gfql("MATCH (a {segment: 'segment-101'}) RETURN a")._nodes
[15]:
a.id a.segment a.amount a.event_time
101 101 segment-101 101.5 2024-01-01 01:41:00
[16]:
key_graph.gfql("MATCH (a {amount: 101.5}) RETURN a")._nodes
[16]:
a.id a.segment a.amount a.event_time
101 101 segment-101 101.5 2024-01-01 01:41:00
[17]:
key_graph.filter_nodes_by_dict(
    {"event_time": pd.Timestamp("2024-01-01 01:41:00")}, engine=engine
)._nodes
[17]:
id segment amount event_time
101 101 segment-101 101.5 2024-01-01 01:41:00

Integer id lists through the Python API#

[18]:
from graphistry import n, e_forward, is_in

native_query = [n({"id": is_in([101, 201])}), e_forward(), n()]
g.gfql(native_query)._nodes
[18]:
id account_number
101 101 48213
102 102 48214
201 201 48313
202 202 48314

Filters across two nodes#

Index both query branches and both hop directions. The account-id branch and the destination’s account-number branch can then supply candidates for the same query.

[19]:
or_graph = g.create_index("node_prop", column="account_number")
or_query = (
    "MATCH (a)-[e]->(b) WHERE a.id IN [101, 201] OR b.account_number = 48214 "
    "RETURN a.id AS src, b.id AS dst"
)
or_graph.gfql(or_query)._nodes
[19]:
src dst
0 101 102
1 201 202
[20]:
or_report = or_graph.gfql_explain(or_query)
assert or_report["used_index"]
or_report["steps"]
[20]:
[{'op': 'fast_path',
  'operation': 'fast_path',
  'seam': 'single_hop_grouped_aggregate',
  'engine': 'auto',
  'served': False,
  'reason': 'declined; caller falls back',
  'path': 'scan',
  'decision_reason': 'declined; caller falls back',
  'decision_code': 'index_path_unavailable'},
 {'op': 'fast_path',
  'operation': 'fast_path',
  'seam': 'two_hop_count',
  'engine': 'auto',
  'served': False,
  'reason': 'declined; caller falls back',
  'path': 'scan',
  'decision_reason': 'declined; caller falls back',
  'decision_code': 'index_path_unavailable'},
 {'op': 'fast_path',
  'operation': 'fast_path',
  'seam': 'seeded_typed_hop',
  'engine': 'auto',
  'served': False,
  'reason': 'declined; caller falls back',
  'path': 'scan',
  'decision_reason': 'declined; caller falls back',
  'decision_code': 'index_path_unavailable'},
 {'op': 'fast_path',
  'operation': 'fast_path',
  'seam': 'seeded_node_lookup',
  'engine': 'auto',
  'served': False,
  'reason': 'declined; caller falls back',
  'path': 'scan',
  'decision_reason': 'declined; caller falls back',
  'decision_code': 'index_path_unavailable'},
 {'op': 'property_lookup',
  'role': 'nodes',
  'column': 'account_number',
  'index_kind': 'node_prop',
  'engine': 'pandas',
  'policy': 'use',
  'est_result_rows': 1,
  'path': 'index',
  'decision_code': 'index_selected',
  'decision_reason': 'property candidates gathered'},
 {'op': 'indexed_traversal',
  'operation': 'indexed_traversal',
  'seam': 'cross_alias_or',
  'engine': 'pandas',
  'served': True,
  'reason': 'served',
  'hops': 1,
  'hop_count': 1,
  'public_seed_scan': False,
  'hop_details': [{'hop': 1, 'estimated_rows': 2}],
  'path': 'index',
  'decision_reason': 'served',
  'decision_code': 'index_selected'}]

Return only the rows you need#

For an unfiltered one-hop query on pandas or cuDF, LIMIT avoids building the remaining result rows. Endpoint columns still scan. Polars uses its existing path.

[21]:
g.gfql("MATCH (a)-[e]->(b) RETURN a.id AS src, b.id AS dst LIMIT 10")._nodes
[21]:
src dst
0 0 1
1 1 2
2 2 3
3 3 4
4 4 5
5 5 6
6 6 7
7 7 8
8 8 9
9 9 10

Check that indexes preserve the answer#

[22]:
indexed = or_graph.gfql(or_query)._nodes
scanned = or_graph.gfql(or_query, index_policy="off")._nodes
if engine == "pandas":
    pd.testing.assert_frame_equal(indexed, scanned)
else:
    assert indexed.equals(scanned)
assert len(indexed) == 2

Read the indexing guide for index lifetime, memory use and planner controls. Continue with Cypher filtering and PageRank for a larger graph pipeline.