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')andSELECT * FROM PAGERANK('Person','KNOWS')compose withAS OF,PURPOSE, and cell-level ACL.
The algorithm surface
| Algorithm | SQL TVF | gds.* / traverse.* | What it answers |
|---|---|---|---|
| PageRank | GRAPH_PAGERANK('Type','LABEL', DAMPING => 0.85, MAX_ITER => 20) | gds.pageRank.{stream|stats|write} | Who are the influential nodes? |
| Degree centrality | DEGREE_CENTRALITY(...) | gds.degreeCentrality.* | Who has the most connections? |
| Triangle count | TRIANGLE_COUNT(...) | gds.triangleCount.* | How clustered is the network? |
| Connected components / WCC | CONNECTED_COMPONENTS(...) | gds.wcc.* | Which nodes form one island? |
| Label propagation (community) | LABEL_PROPAGATION(...) | gds.labelPropagation.* | What communities self-organize? |
| Louvain community | community detection TVF | — | Higher-quality community detection |
| Strongly connected components (SCC) | SCC(...) | — | Mutually-reachable clusters |
| Cycle detection | CYCLES(...) | — | Where's the feedback loop? |
| Shortest path (SSSP) | PATHS_BETWEEN('a','b', max_hops) + PLL index | — | How are A and B connected? |
| All-pairs shortest path (APSS) | — | — | Distance matrix across the graph |
| Spanning tree / diameter | — | — | Backbone + reachability radius |
| Node similarity | GRAPH_NODE_SIMILARITY('Type', node) | — | Nodes structurally like X |
| Link prediction | LINK_PREDICT('Type') | — | Likely missing edges |
| HubAuthority (HITS) | — | via MCP hub_authority | Hubs 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 103. 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 10Supported 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 => 20is the textbook starting point; raiseMAX_ITERonly if scores haven't converged (the response tells you). - Use
PATHS_BETWEENfor "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. Usegds.*when porting existing Neo4j workloads; switch to SQL TVFs ortraverse.*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
- Cypher & SQL-PGQ —
MATCHqueries over the same graph - SQL reference —
PATHS_BETWEEN,LOOKUP_IDENTITY,RESOLVE_IDENTITY - Identity — how the graph forms itself from standardized identities
- Hybrid search — graph rank as the third RRF signal
- MCP tools — the graph investigation verbs
- Use cases — AML, LEA, maritime, OSINT