mcpField
P
pygraphistry-gfql

pygraphistry-gfql

github.com/graphistryData & databases337 installsUpdated today
Repository

About pygraphistry-gfql

pygraphistry-gfql is a skill for querying PyGraphistry graphs with GFQL. It covers chain-list and Cypher syntax, graph traversal, staged bindings, and execution-engine choices.

Key features

  1. 01Chain-list and Cypher queries
  2. 02Pattern matching and graph traversal
  3. 03Graph constructors and DAG bindings
  4. 04Pandas, Polars, and GPU engine guidance

Use cases

  1. 01Find paths matching graph patterns
  2. 02Build multi-stage graph queries
  3. 03Choose an execution engine for queries

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/graphistry/graphistry-skills --skill pygraphistry-gfql

After installing, type/pygraphistry-gfqlin Claude Code to confirm it loaded.

Bundled filesreferences/gfql-cypher.mdreferences/gfql-engines.md

Details

From the project README

PyGraphistry GFQL

Doc routing (local + canonical)

  • First route with ../pygraphistry/references/pygraphistry-readthedocs-toc.md.
  • Use ../pygraphistry/references/pygraphistry-readthedocs-top-level.tsv for section-level shortcuts.
  • Only scan ../pygraphistry/references/pygraphistry-readthedocs-sitemap.xml when a needed page is missing.
  • Use one batched discovery read before deep-page reads; avoid cat * and serial micro-reads.
  • In user-facing answers, prefer canonical https://pygraphistry.readthedocs.io/en/latest/... links.

Two syntaxes, one entrypoint

g.gfql() accepts both chain-list (Python AST objects) and Cypher strings. It auto-detects the language from the argument type:

# Chain-list syntax (Python AST objects)
g2 = g.gfql([n({'type': 'person'}), e_forward(), n()])

# Cypher string syntax (auto-detected)
g2 = g.gfql("MATCH (p:Person)-[r:KNOWS]->(q:Person) RETURN p.name, q.name")

# Explicit language parameter (optional)
g2 = g.gfql(query_string, language="cypher")

When to use which:

  • Chain-list: Programmatic composition, dynamic parameterization, when building queries from code
  • Cypher: Readability, familiarity for Cypher users, complex pattern matching with RETURN/ORDER BY/LIMIT

Quick start — chain-list

from graphistry import n, e_forward

g2 = g.gfql([
    n({'type': 'person'}),
    e_forward({'relation': 'transfers_to'}, min_hops=1, max_hops=3),
    n({'risk': True})
])

Quick start — Cypher

# Simple pattern match
g2 = g.gfql("MATCH (p:Person)-[r:KNOWS]->(q:Person) WHERE p.age > 30 RETURN p.name, q.name")

# Variable-length paths
g2 = g.gfql("MATCH (a:Account)-[*1..3]->(m:Merchant) RETURN a, m")

# Parameterized queries
g2 = g.gfql(
    "MATCH (n) WHERE n.score > $cutoff RETURN n.id, n.score ORDER BY n.score DESC LIMIT $top_n",
    params={"cutoff": 50, "top_n": 10}
)

# Relationship type alternation
g2 = g.gfql("MATCH (a:Person)-[:KNOWS|COLLABORATES_WITH]->(b:Person) RETURN a.name, b.name")

See references/gfql-cypher.md for the full clause/function inventory and label-column mapping.

GRAPH constructor and Let/DAG bindings

from graphistry import n, e_forward, let, ref

# Subgraph as a graph object (not a table)
subgraph = g.gfql("GRAPH { MATCH (a)-[r]->(b) WHERE a.risk_score > 7 }")

# Named bindings forming a DAG; ref() operates on the referenced binding's output
result = g.gfql(let({
    'high_risk': n({'risk_score': {'$gt': 0.8}}),
    'neighborhoods': ref('high_risk', [e_forward(max_hops=2), n()])
}), output='neighborhoods')

Independent bindings operate on the root graph. See references/gfql-cypher.md for multi-stage DAGs, nested let scope rules, and the GRAPH/USE pipeline form.

Targeted patterns (high signal)

# Edge query filtering
g2 = g.gfql([n(), e_forward(edge_query="type == 'replied_to' and submolt == 'X'"), n()])
# Same-path constraints with where + compare/col
from graphistry import col, compare
g2 = g.gfql([n(name='a'), e_forward(name='e'), n(name='b')], where=[compare(col('a', 'owner_id'), '==', col('b', 'owner_id'))])
# Traverse 2-4 hops but only return hops 3-4
g2 = g.gfql([e_forward(min_hops=2, max_hops=4, output_min_hops=3, output_max_hops=4)])

Edge direction variants

  • e_forward() — source-to-destination
  • e_reverse() — destination-to-source
  • e_undirected() — both directions
  • e() — alias for any direction

High-value patterns

  • g.gfql() is the unified entrypoint — pass chain-lists OR Cypher strings.
  • NEVER use .chain() or .hop() — they are deprecated and emit warnings. Always use g.gfql([...]) for chain-list syntax or g.gfql("MATCH ...") for Cypher.
  • When user explicitly asks for GFQL, final snippets must include explicit .gfql(...).
  • When the task says remote execution/dataset, use gfql_remote(...).
  • Use name= labels for intermediate matches when you need constraints.
  • Use where=[...] for cross-step/path constraints.
  • Use min_hops/max_hops and output_min_hops/output_max_hops for traversal vs returned slice.
  • Use predicates (is_in, numeric/date predicates) for concise filtering.
  • Use an explicit engine when performance or result frame type matters; engine='auto' does not select Polars.

Execution engines: pandas, Polars, cuDF, and Polars-GPU

Use the same query with the engine suited to the workload. Input frame type and execution engine are independent; GFQL converts inputs once and returns frames in the selected engine's type.

query = "MATCH (a)-[e]->(b) WHERE a.risk_score > $cutoff RETURN b"

cpu_out = g.gfql(query, params={'cutoff': 7}, engine='polars')
gpu_out = g.gfql(query, params={'cutoff': 7}, engine='polars-gpu')

# Polars/Polars-GPU outputs are polars.DataFrame objects
nodes_pd = cpu_out._nodes.to_pandas()  # only for pandas-only downstream APIs
  • Valid literals are exactly 'pandas', 'cudf', 'dask', 'dask_cudf', 'polars', 'polars-gpu', 'auto'. 'polars-gpu' is hyphenated; polars_gpu is not an engine.
  • For a Polars input graph engine='auto' resolves to pandas — pass engine='polars' to stay native.
  • Outputs follow the selected engine: Polars for polars/polars-gpu, cuDF for cudf. Convert intentionally before pandas-only operations such as .iloc or groupby().apply().
  • polars-gpu requires the RAPIDS stack and is GPU-or-error — it never silently falls back to CPU.
Analytics under Polars engines

Whole-graph call() analytics such as UMAP, hypergraph, layouts, or compute_cugraph are not native Polars operations. With the default call_mode='auto', GFQL bridges them off-engine (pandas for Polars; cuDF for Polars-GPU) and converts the result back. Use strict mode when an off-engine bridge would violate a benchmark, memory, or execution constraint:

from graphistry.compute.gfql.lazy import set_call_mode

set_call_mode('strict')  # reject an off-engine analytic before it runs
try:
    result = g.gfql(query, engine='polars')
except NotImplementedError as exc:
    ...  # strict mode declined an off-engine analytic

gfql() takes no strict= or call_mode= argument. Mode is process-level via set_call_mode('auto'|'strict') or GFQL_POLARS_CALL_MODE (Python override > env > default 'auto'), read live per call. Strict raises NotImplementedError — not RuntimeError, not a warning. Because it is process-global, scope it and restore in a finally: when only one step must be strict (references/gfql-engines.md).

polars-gpu analytics are GPU-or-error: if the GPU/cuDF stack is unavailable, they decline rather than move the work to host pandas.

Engine tuning knobs

Three process-level settings live in graphistry.compute.gfql.lazy, each resolving Python override > env var > default and read live per collect (not frozen at import):

settingvaluesdefaultenv var
set_call_mode'auto', 'strict''auto'GFQL_POLARS_CALL_MODE
set_gpu_executor'in-memory', 'streaming''in-memory'GFQL_POLARS_GPU_EXECUTOR
set_cpu_streamingTrue, FalseFalseGFQL_POLARS_CPU_STREAMING

Read the current value with call_mode(), gpu_executor(), cpu_streaming(); pass None to a setter to reset to env/default. 'polars' and 'polars-gpu' are one lazy engine with two collect targets, so the plan is built once and collected once. That transfer-once design is what makes GPU pay off:

  • Collect-once avoids repeated host-to-device (H2D) transfers. Per-op eager collection re-copies the frame to the device on every operation, which benchmarked as a GPU regression.
  • The knobs resolve Python override > env var > default, read live per collect — not frozen at import.
Physical indexes: seeded lookups

GFQL ships pay-as-you-go adjacency/node-id indexes (graphistry.compute.gfql.index) for seeded traversal. Two rules decide whether an index helps:

  • Query shape — only the chain/hop form consults the index. Measured: a seeded chain went 8.90ms → 1.68ms (5.3x), while both Cypher spellings were never consulted and did not improve. Write the chain form if you want index acceleration.
  • Frontier size, engine-aware. The planner gates index-vs-scan on the frontier as a fraction of distinct source keys: pandas ~0.5, polars/cuDF/GPU ~0.02. Vectorized engines scan fast enough that an index only wins for very selective seeds. Past the gate it falls back to scan, so it never loses.
from graphistry.compute.gfql.index import create_index, index_trace
gi = create_index(g, 'edge_out_adj', engine='polars')   # edge_out_adj | edge_in_adj | node_id
with index_trace() as steps:
    out = gi.gfql([n({'id': 'acct-42'}), e_forward(), n()], engine='polars')
steps[0]['path']   # confirm 'index', do not assume it

index_policy= is a per-call gfql() keyword (unlike call mode): 'use' (default, resident + cost-gated), 'auto' (build on demand), 'force' (skip the gate), 'off'. With no resident index, 'use' silently scans. Index DDL, trace fields, and the cost-gate override: references/gfql-engines.md.

Choosing an engine on performance

Do not promise a speedup you have not measured.

  • pandas → polars: worth it above roughly 50–100k rows; below that conversion can dominate. Measured ~2x on seeded 1-hop.
  • polars → polars-gpu is not a blanket win. Measured on an NVIDIA GB10: 0.83x at 100k rows (slower than CPU), 1.41x at 1M, 0.98x at 5M — a band, not a rising curve. At 8M edges cudf beat both. Never state a general "polars-gpu beats cudf" rule.
  • set_cpu_streaming(True) is opt-in and can be slower — ~0.86x on small/interactive sizes. Large batch CPU work only; the name sounds faster than it is.
  • Full measurement tables: references/gfql-engines.md.
Which engine when — a decision procedure

Work in order; stop at the first that decides.

  1. Does a step decline under Polars? Run that step on engine='pandas' and report pandas as the engine.
  2. Where do the frames already live? polars-gpu ingests a host polars frame, so already-on-device cuDF frames favor engine='cudf'; host Polars frames favor 'polars'/'polars-gpu'.
  3. Seeded lookup with a small frontier? Index it and use the chain form — the biggest single win available (5.3x), independent of engine choice.
  4. CPU: prefer polars over pandas for anything non-trivial.
  5. GPU: only when the workload is big enough, and pick the GPU engine by measurement, not by name.

Below a few milliseconds of work, engine choice is noise — indexing and query shape matter more.

Parity-or-decline: do not invent workarounds

Refuse to mislabel first, then solve the problem. If a user asks you to keep reporting engine='polars' for work that pandas executed — to keep a dashboard green, a benchmark comparable, or an API contract stable — say no before writing any code. A wrapper that exposes engine='polars' while pandas runs underneath is mislabeling even when a second field records the truth: the primary label is the one people read. This is the one request in this skill you should push back on rather than implement.

Honest alternatives to offer: run the step on engine='pandas' and report pandas, or keep the pipeline polars-native by avoiding the declining surface.

With that settled, the mechanics: traversal, filter, and row ops under a Polars engine are parity-or-NotImplementedError.

  • The engine never silently falls back to pandas — a hidden bridge would misreport pandas performance as Polars.
  • An unsupported surface raises NotImplementedError (not RuntimeError, not a warning).
  • Surfaces that decline today: undirected min_hops>1, direct hop(min_hops>1) (use chain()/gfql()), multi-entity rows(binding_ops=…), cross-entity same-path WHERE, exotic expressions (CASE/list/map/temporal). Full list: references/gfql-engines.md.

Conversion into an engine follows the repo-wide validate/warn convention. On a mixed-type object column that Arrow cannot represent:

validatebehavior
'strict'raises an error (NotImplementedError for polars); the column is left unchanged
'autofix'coerces the column to string and emits a warning — data is silently rewritten unless you read the warning

Use 'strict' for any job that must never change data without telling you.

Remote mode

# Remote with chain-list
rg = graphistry.bind(dataset_id='my-dataset')
res = rg.gfql_remote([n(), e_forward(), n()], engine='auto')
# Remote with Cypher string
res = rg.gfql_remote("MATCH (n:Person)-[r]->(m) WHERE n.risk_level = 'critical' RETURN n, r, m")
# Remote with Let/DAG
res = rg.gfql_remote(let({...}))
# Remote slim payload (only required columns)
res = rg.gfql_remote([n(), e_forward(), n()], output_type='nodes', node_col_subset=['node_id', 'time'])
# Post-process on remote side when you want trimmed transfer payloads
res = rg.python_remote_table(lambda g: g._edges[['src', 'dst']].head(1000))

Validation and safety

  • Validate user-derived query fragments before execution.
  • Normalize datetime columns before temporal predicates.
  • Prefer small column subsets for remote result transfer.
  • Preflight Cypher: from graphistry.compute.gfql.cypher import parse_cypher, compile_cypher

Canonical docs

Comments

Markdown supported