Graph analytics — one engine, ten+ algorithms, three query languages

RelataDB builds the graph for you out of standardized identities (any two records sharing a validated identifier are auto-linked), then lets you run production graph analytics over it without a separate Neo4j/TigerGraph/GDS product. The same governed plan that runs your SQL SELECT also runs PageRank, community detection, shortest path, and cycle detection — with ACL, org isolation, and provenance firing identically.

You can query the graph three ways: SQL TVFs (first-class), CALL traverse.<algo> (native Cypher/GQL procedures), or CALL gds.<algo> (Neo4j GDS portability alias — your existing gds.* scripts port with minimal change).

Why this matters: in a polyglot stack you'd run graph analytics in a separate Neo4j + GDS instance, ETL the rows over, and lose governance + identity + temporal joins along the way. Here, graph analytics is a query against the same governed store — PATHS_BETWEEN('alice','bob') and SELECT * FROM PAGERANK('Person','KNOWS') compose with AS OF, PURPOSE, and cell-level ACL.

The algorithm surface

AlgorithmSQL TVFgds.* / traverse.*What it answers
PageRankGRAPH_PAGERANK('Type','LABEL', DAMPING => 0.85, MAX_ITER => 20)gds.pageRank.{stream|stats|write}Who are the influential nodes?
Degree centralityDEGREE_CENTRALITY(...)gds.degreeCentrality.*Who has the most connections?
Triangle countTRIANGLE_COUNT(...)gds.triangleCount.*How clustered is the network?
Connected components / WCCCONNECTED_COMPONENTS(...)gds.wcc.*Which nodes form one island?
Label propagation (community)LABEL_PROPAGATION(...)gds.labelPropagation.*What communities self-organize?
Louvain communitycommunity detection TVFHigher-quality community detection
Strongly connected components (SCC)SCC(...)Mutually-reachable clusters
Cycle detectionCYCLES(...)Where's the feedback loop?
Shortest path (SSSP)PATHS_BETWEEN('a','b', max_hops) + PLL indexHow are A and B connected?
All-pairs shortest path (APSS)Distance matrix across the graph
Spanning tree / diameterBackbone + reachability radius
Node similarityGRAPH_NODE_SIMILARITY('Type', node)Nodes structurally like X
Link predictionLINK_PREDICT('Type')Likely missing edges
HubAuthority (HITS)via MCP hub_authorityHubs vs authorities

Incremental warm-start: PageRank / WCC / label-propagation variants avoid full recompute on a small edge delta — they pick up from the prior score vector. Cheap "add one edge, get new scores."

Three ways to call them

1. SQL TVF (first-class — composes with everything)

PURPOSE 'analytics'
SELECT id, pagerank
FROM GRAPH_PAGERANK('Person', 'KNOWS', DAMPING => 0.85, MAX_ITER => 20)
ORDER BY pagerank DESC
LIMIT 10;
-- Composes with temporal + identity predicates, same query
PURPOSE 'investigation'
SELECT id, pagerank
FROM GRAPH_PAGERANK('Person', 'KNOWS')
WHERE id IN (
  SELECT object_id FROM IdentityIndex AS OF '2026-01-01T00:00:00'
  WHERE payload = '+44 7700 900123'
)
ORDER BY pagerank DESC;

2. CALL traverse.<algo>.<mode> (native Cypher/GQL procedure)

// Over Bolt (port 7687) with the official Neo4j driver, or POST /query
CALL traverse.pageRank.stream('Person', {maxIterations: 20, dampingFactor: 0.85})
YIELD nodeId, score
RETURN nodeId, score
ORDER BY score DESC
LIMIT 10

3. CALL gds.<algo>.<mode> (Neo4j GDS portability — port existing scripts)

// Identical shape to Neo4j GDS — minimal rewrite to port an existing pipeline
CALL gds.pageRank.stream('myGraph', {maxIterations: 20, dampingFactor: 0.85})
YIELD nodeId, score
RETURN nodeId, score
ORDER BY score DESC
LIMIT 10

Supported gds.* procedures (mode defaults to stream when omitted): gds.pageRank.{stream,stats,write}, gds.degreeCentrality.{stream,stats,write}, gds.triangleCount.*, gds.wcc.*, gds.labelPropagation.*. An unrecognized gds.<algo> returns a typed error pointing at the equivalent SQL TVF to use directly (use GRAPH_PAGERANK / DEGREE_CENTRALITY / TRIANGLE_COUNT / CONNECTED_COMPONENTS / LABEL_PROPAGATION SQL operators directly).

From the SDKs

All three SDKs expose the graph operators as one-shot methods (they compile to the SQL TVFs server-side):

# Python — page rank over the Person/KNOWS graph
pr = client.graph_pagerank("Person", damping=0.85, max_iter=20, purpose="analytics")
# → [{"id": "p1", "score": 0.18}, ...]
 
# Shortest path between two entities (PLL-indexed)
path = client.graph_shortest_path("alice-id", "bob-id", purpose="investigation")
 
# Communities
comms = client.graph_community("Person", purpose="analytics")
// TypeScript
const pr = await relata.graphPageRank("Person", { damping: 0.85, maxIter: 20, purpose: "analytics" });
const path = await relata.graphShortestPath("alice-id", "bob-id");
// Go
pr, _ := client.GraphPageRank(ctx, "analytics", "Person",
    &relata.GraphPageRankOptions{Damping: 0.85, MaxIter: 20})
path, _ := client.GraphShortestPath(ctx, "alice-id", "bob-id", &relata.GraphShortestPathOptions{})

MCP tools (for agent-driven investigation)

# rank_key_nodes — "who are the influencers in this Person graph?"
mcp.call_tool("rank_key_nodes", {"entity_type": "Person", "metric": "pagerank"})
 
# detect_communities — "show me the clusters"
mcp.call_tool("detect_communities", {"entity_type": "Person", "algo": "louvain"})
 
# predict_links — "what edges are likely missing?"
mcp.call_tool("predict_links", {"entity_type": "Person"})
 
# find_scc, hub_authority, paths_between, find_connections ...

The full MCP surface: rank_key_nodes, detect_communities, predict_links, find_scc, hub_authority, paths_between, find_connections, get_relationships. See MCP Tools.

GraphTrigger — edges from rows, automatically

Don't want to manage edges at all? Declare a graph_triggers block on the type and the graph builds itself from the rows you were ingesting anyway:

curl -X POST http://127.0.0.1:9090/types \
  -d '{
    "name": "CdrRecord",
    "graph_triggers": [
      {"link_type": "CALLED", "src_field": "caller_id", "dst_field": "callee_id"}
    ]
  }'

Every CdrRecord insert now also creates a governed CALLED edge between the caller and callee — no separate edge-loading pipeline, no server restart. PATHS_BETWEEN and Cypher read the edge natively.

Tips & takeaways

  • PageRank's defaults are sane. DAMPING => 0.85, MAX_ITER => 20 is the textbook starting point; raise MAX_ITER only if scores haven't converged (the response tells you).
  • Use PATHS_BETWEEN for "how are these two connected?" It's PLL-indexed — sub-millisecond on typical graphs, no traversal cost.
  • Community detection ≠ clustering. Label propagation is fast and deterministic; Louvain finds higher-modularity communities but costs more. Try both.
  • Graph + temporal is the killer combo. GRAPH_PAGERANK(...) AS OF '<ts>' answers "who was influential at the time of the incident?" — impossible in a polyglot stack without snapshots.
  • gds.* is for portability, traverse.* is native. Both lower to the same governed plan. Use gds.* when porting existing Neo4j workloads; switch to SQL TVFs or traverse.* for new code (cleaner governance + composition).
  • Link prediction surfaces likely missing edges — great for "who probably knows whom" in OSINT / AML work, but treat the output as leads, not facts (no provenance on a predicted edge until you promote it to a real one).

See also