Graph Algorithms

Shortest Path

result = graph.shortest_path(source_type='Person', source_id=1, target_type='Person', target_id=100)
if result:
    for node in result["path"]:
        print(f"{node['type']}: {node['title']}")
    print(f"Connections: {result['connections']}")
    print(f"Path length: {result['length']}")

Lightweight variants when you don’t need full path data:

graph.shortest_path_length(...)    # → int | None (hop count only)
graph.shortest_path_ids(...)       # → list[id] | None (node IDs along path)
graph.shortest_path_indices(...)   # → list[int] | None (raw graph indices, fastest)

All path methods support connection_types, via_types, and timeout_ms for filtering and safety.

Weighted shortest path

Pass weight_property to switch from BFS (hop count) to Dijkstra (sum of edge weights). Edges missing the property default to weight 1.0; negative weights cause the path to be reported as missing.

# Cheapest path by edge.cost
result = graph.shortest_path(
    "Stop", "A", "Stop", "Z",
    weight_property="cost",
)
# {'path': [...], 'connections': [...], 'length': 3, 'weight': 4.7}

# Length-only variant returns float when weighted, int otherwise
graph.shortest_path_length("Stop", "A", "Stop", "Z", weight_property="cost")  # → 4.7

Batch variant for computing many distances at once:

distances = graph.shortest_path_lengths_batch('Person', [(1, 5), (2, 8), (3, 10)])
# → [2, None, 5]  (None where no path exists, same order as input)

All Paths

paths = graph.all_paths(
    source_type='Play', source_id=1,
    target_type='Wellbore', target_id=100,
    max_hops=4,
    max_results=100  # Prevent OOM on dense graphs
)

Connected Components

components = graph.connected_components()
# Returns list of lists: [[node_dicts...], [node_dicts...], ...]
print(f"Found {len(components)} connected components")
print(f"Largest component: {len(components[0])} nodes")

graph.are_connected(source_type='Person', source_id=1, target_type='Person', target_id=100)

Cypher procedures: scoped subgraph algorithms

Several algorithms are also exposed as Cypher CALL procedures so you can run them over a subgraph — one node type and one (or several) relationship types — instead of the whole graph. This is the idiomatic way to ask “components among Person nodes connected by KNOWS” without first extracting a separate graph.

All three share the same optional {node_type, relationship} scoping. Each field accepts a string or a list of strings; omit the map to run over the whole graph.

Edge-scope key: relationship and connection_types are interchangeable on every algorithm procedure — the centrality/community procedures historically read connection_types and the components/k-core ones read relationship, but either term now works anywhere. Unknown config keys are rejected with a did-you-mean (CALL pagerank(): unknown config key 'connection_typ'. Did you mean 'connection_types'?) rather than silently producing an empty result. (where predicate-scoping is supported by the centrality + community procedures; the components/k-core/clustering group scopes by node_type + relationship only.)

Connected components

-- Whole graph
CALL connected_components() YIELD node, component
RETURN component, count(*) AS size ORDER BY size DESC

-- Scoped to one node type + relationship
CALL connected_components({node_type: 'Person', relationship: 'KNOWS'})
YIELD node, component
RETURN component, collect(node.name) AS members

-- Multiple relationship types
CALL connected_components({node_type: ['Person'], relationship: ['KNOWS', 'OWNS']})
YIELD node, component
RETURN count(DISTINCT component) AS num_components

K-core decomposition (coreness)

The coreness of a node is the largest k for which it survives in the k-core (the maximal subgraph where every node has degree ≥ k). High coreness marks structurally central, resilient nodes. k_core and coreness are aliases.

CALL k_core() YIELD node, coreness
RETURN node.name AS name, coreness ORDER BY coreness DESC LIMIT 10

-- Scoped
CALL k_core({node_type: 'Person', relationship: 'KNOWS'})
YIELD node, coreness
RETURN coreness, count(*) AS n ORDER BY coreness DESC

Local clustering coefficient

The fraction of a node’s neighbour pairs that are themselves connected — the local triangle-closure rate (0.0 = no neighbours linked, 1.0 = neighbourhood is a clique). clustering_coefficient and local_clustering_coefficient are aliases.

CALL clustering_coefficient() YIELD node, coefficient
RETURN node.name AS name, coefficient ORDER BY coefficient DESC

-- Scoped, then averaged
CALL clustering_coefficient({node_type: 'Person', relationship: 'KNOWS'})
YIELD node, coefficient
RETURN avg(coefficient) AS global_avg

Scoping is computed lazily over the live graph (no copy), so these run identically across the in-memory, mapped, and disk storage modes.

Centrality Algorithms

All centrality methods return a ResultView of {type, title, id, score} rows, sorted by score descending.

graph.betweenness_centrality(top_k=10)
graph.betweenness_centrality(normalized=True, sample_size=500)
graph.pagerank(top_k=10, damping_factor=0.85)
graph.degree_centrality(top_k=10)
graph.closeness_centrality(top_k=10)

# Alternative output formats
graph.pagerank(as_dict=True)      # → {1: 0.45, 2: 0.32, ...} (keyed by id)
graph.pagerank(to_df=True)        # → DataFrame with type, title, id, score columns

Community Detection

# Louvain modularity optimization (recommended)
result = graph.louvain_communities()
# {'communities': {0: [{type, title, id}, ...], 1: [...]},
#  'modularity': 0.45, 'num_communities': 2}

for comm_id, members in result['communities'].items():
    names = [m['title'] for m in members]
    print(f"Community {comm_id}: {names}")

# With edge weights and resolution tuning
result = graph.louvain_communities(weight_property='strength', resolution=1.5)

# Label propagation (faster, less precise)
result = graph.label_propagation(max_iterations=100)

Clustering

General-purpose clustering via Cypher CALL cluster(). Reads nodes from a preceding MATCH clause.

# Spatial DBSCAN — auto-detects lat/lon from set_spatial() config
result = graph.cypher("""
    MATCH (f:Field)
    CALL cluster({method: 'dbscan', eps: 50000, min_points: 2})
    YIELD node, cluster
    RETURN cluster, count(*) AS n, collect(node.name) AS fields
    ORDER BY n DESC
""")

# Property-based K-means — cluster on explicit numeric properties
result = graph.cypher("""
    MATCH (w:Wellbore)
    CALL cluster({
        properties: ['totalDepth', 'bottomHoleTemp'],
        method: 'kmeans', k: 5, normalize: true
    })
    YIELD node, cluster
    RETURN cluster, count(*) AS n
""")

Parameter

Type

Default

Notes

method

string

"dbscan"

"dbscan" or "kmeans"

properties

list

(none)

If omitted, uses spatial config

eps

float

0.5

DBSCAN neighborhood radius (meters for spatial, raw units for properties)

min_points

int

3

DBSCAN minimum neighbors for core point

k

int

5

K-means cluster count

max_iterations

int

100

K-means iteration limit

normalize

bool

false

Min-max scale features to [0,1] before clustering

Noise points (DBSCAN only) get cluster = -1. Filter with WHERE cluster >= 0.

Analytics

Statistics

price_stats = graph.select('Product').statistics('price')
unique_cats = graph.select('Product').unique_values(property='category', max_length=10)

# Group by a property — like SQL GROUP BY
graph.select('Person').count(group_by='city')
# → {'Oslo': 42, 'Bergen': 15, 'Trondheim': 8}

graph.select('Person').statistics('age', group_by='city')
# → {'Oslo': {'count': 42, 'mean': 35.2, 'std': 8.1, 'min': 22, 'max': 65, 'sum': 1478},
#    'Bergen': {'count': 15, ...}, ...}

Calculations

graph.select('Product').calculate(expression='price * 1.1', store_as='price_with_tax')

graph.select('User').traverse('PURCHASED').calculate(
    expression='sum(price * quantity)', store_as='total_spent'
)

graph.select('User').traverse('PURCHASED').count(store_as='product_count', group_by_parent=True)

Node Degrees

degrees = graph.select('Person').degrees()
# Returns: {'Alice': 5, 'Bob': 3, ...}