diff --git a/.wordlist.txt b/.wordlist.txt index fd87086b..585b03bd 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -947,4 +947,13 @@ RSS OOM Embeddable reranking -Swappable \ No newline at end of file +Swappable +AStar +CCH +Dijkstra +haversine +middleProp +preprocess +preprocesses +rankProp +shortcutRelType \ No newline at end of file diff --git a/algorithms/astar.mdx b/algorithms/astar.mdx new file mode 100644 index 00000000..57499975 --- /dev/null +++ b/algorithms/astar.mdx @@ -0,0 +1,154 @@ +--- +title: "algo.AStar" +description: "Find the shortest path between two nodes using A* search guided by a geographic (haversine) heuristic." +--- + +The `algo.AStar` procedure finds the shortest path between a **source** and a **target** node using the [A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). + +Like [algo.SPpaths](/algorithms/sppath), it minimizes a numeric edge property (`weightProp`), but it is guided by a geographic heuristic — the straight-line (great-circle) distance from each node to the target. On spatial graphs such as road networks this lets A* explore far fewer nodes than a plain Dijkstra search, while still returning an optimal path. + +Every node must carry latitude and longitude properties; A* uses them to compute the haversine distance to the target. + +## Syntax + +```cypher +CALL algo.AStar({ + sourceNode: , + targetNode: , + relTypes: [], + weightProp: , + latitudeProperty: , + longitudeProperty: , + heuristicScale: , // optional, default 1.0 + relDirection: "outgoing", // optional: "outgoing", "incoming", "both" + pathCount: // optional, default 1 +}) +YIELD path, pathWeight +``` + +## Parameters + +| Name | Type | Description | +|---------------------|---------|-------------------------------------------------------------------------------------------------| +| `sourceNode` | Node | Starting node | +| `targetNode` | Node | Destination node | +| `relTypes` | Array | List of relationship types to follow | +| `weightProp` | String | Edge property to minimize along the path (e.g., `length`, `time`) | +| `latitudeProperty` | String | Node property holding the latitude (decimal degrees) | +| `longitudeProperty` | String | Node property holding the longitude (decimal degrees) | +| `heuristicScale` | Number | Optional. Converts the haversine heuristic (meters) into `weightProp` units. Default `1.0`. | +| `relDirection` | String | Optional. Traversal direction: `outgoing` (default), `incoming`, or `both` | +| `pathCount` | Integer | Optional. Number of paths to return (default `1`; must be `≥ 1`) | + +## Returns + +| Name | Type | Description | +|--------------|--------|-------------------------------------------------| +| `path` | Path | Discovered path from source to target | +| `pathWeight` | Float | Sum of `weightProp` across the path | + +## The `heuristicScale` parameter + +The A* heuristic is the **haversine distance in meters** between a node and the target. For the search to return an optimal path, the heuristic must never overestimate the remaining cost — it must be a *lower bound* on the true remaining `weightProp` (this is the *admissibility* requirement of A*). + +When `weightProp` is **not** a distance in meters, the raw meters heuristic is on the wrong scale and can dwarf the actual edge weights, causing A* to behave greedily and return a **sub-optimal** path. `heuristicScale` multiplies the heuristic to bring it into `weightProp` units. Set it to a lower bound on the weight accrued per meter of straight-line progress: + +| `weightProp` represents | Suggested `heuristicScale` | Reasoning | +|-------------------------|---------------------------------------|--------------------------------------------------------| +| Distance in meters | `1.0` (the default) | Heuristic is already in the right units | +| Distance in kilometers | `0.001` | 1 meter = 0.001 km | +| Travel time in seconds | `1 / max_speed_m_per_s` | Fastest possible time to cover one meter | +| Travel time in hours | `1 / max_speed_m_per_hour` | e.g. `1/120000` for a 120 km/h maximum speed | + + + Leaving `heuristicScale` at its default `1.0` while `weightProp` is *not* a + distance in meters (for example a drive-time weight) makes the heuristic + inadmissible, so A* may return a **sub-optimal path**. Always scale the + heuristic to the units of `weightProp`. + + + + `heuristicScale` must be a non-negative number. A smaller value is always + safe (the search stays optimal but explores more nodes); a value larger than + the true minimum weight-per-meter trades optimality for speed. + + +## Examples + +Consider a small road network where each junction stores its coordinates and each road stores its `length` (meters) and `time` (seconds): + +```cypher +CREATE + (a:Junction {name:'A', lat:40.7128, lon:-74.0060}), + (b:Junction {name:'B', lat:40.7300, lon:-73.9950}), + (c:Junction {name:'C', lat:40.7580, lon:-73.9855}), + (a)-[:ROAD {length:3200, time:240}]->(b), + (b)-[:ROAD {length:4100, time:300}]->(c), + (a)-[:ROAD {length:8000, time:360}]->(c) +``` + +### Example: shortest route by distance + +`length` is in meters, so the default `heuristicScale` of `1.0` is correct: + +```cypher +MATCH (a:Junction {name:'A'}), (c:Junction {name:'C'}) +CALL algo.AStar({ + sourceNode: a, + targetNode: c, + relTypes: ['ROAD'], + weightProp: 'length', + latitudeProperty: 'lat', + longitudeProperty: 'lon' +}) +YIELD path, pathWeight +RETURN pathWeight, [n IN nodes(path) | n.name] AS route +``` + +#### Expected Result: +| pathWeight | route | +|------------|---------------| +| `7300` | [A, B, C] | + +### Example: fastest route by travel time + +Here `weightProp` is `time` in **seconds**, so the heuristic must be scaled by `1 / max_speed`. Assuming a maximum speed of 30 m/s, use `heuristicScale: 0.0333`: + +```cypher +MATCH (a:Junction {name:'A'}), (c:Junction {name:'C'}) +CALL algo.AStar({ + sourceNode: a, + targetNode: c, + relTypes: ['ROAD'], + weightProp: 'time', + latitudeProperty: 'lat', + longitudeProperty: 'lon', + heuristicScale: 0.0333 +}) +YIELD path, pathWeight +RETURN pathWeight, [n IN nodes(path) | n.name] AS route +``` + +#### Expected Result: +| pathWeight | route | +|------------|------------| +| `360` | [A, C] | + +--- + +## Frequently Asked Questions + + + + Use **algo.AStar** for point-to-point queries on **spatial** graphs where nodes have coordinates (road networks, maps): the geographic heuristic prunes the search and is usually faster than Dijkstra. Use **[algo.SPpaths](/algorithms/sppath)** when nodes have no coordinates, or when you need cost constraints (`costProp`/`maxCost`) or all shortest paths. + + + Almost always because `heuristicScale` doesn't match the units of `weightProp`. The heuristic is in meters; if `weightProp` is a travel time (or any non-meter unit) and the scale is left at the default `1.0`, the heuristic overestimates the remaining cost and the search returns a sub-optimal path. Set `heuristicScale` to a lower bound on the weight per meter (see [the heuristicScale section](#the-heuristicscale-parameter)). + + + A* needs coordinates on every node it visits to compute the heuristic. Ensure `latitudeProperty` and `longitudeProperty` are present and numeric on all nodes reachable during the search. + + + For a graph that rarely changes and is queried many times, consider preprocessing it with **[algo.CCH](/algorithms/cch)** (Customizable Contraction Hierarchies), which answers repeated point-to-point queries even faster than A* after a one-time build. + + diff --git a/algorithms/cch.mdx b/algorithms/cch.mdx new file mode 100644 index 00000000..fccbe1cd --- /dev/null +++ b/algorithms/cch.mdx @@ -0,0 +1,183 @@ +--- +title: "algo.CCH" +description: "Preprocess a weighted graph into a Customizable Contraction Hierarchy for fast, repeated point-to-point shortest-path queries." +--- + +**Customizable Contraction Hierarchies (CCH)** trade a one-time preprocessing step for very fast point-to-point shortest-path queries. On a large, mostly-static weighted graph — a road network being the canonical example — a CCH answers repeated shortest-path queries far faster than running [algo.SPpaths](/algorithms/sppath) or [algo.AStar](/algorithms/astar) from scratch each time. + +CCH is exposed as two procedures that are used together: + +1. **`algo.CCH`** — builds the hierarchy **once**, materializing it into the graph as *shortcut* edges plus a *rank* property on every node. This is a write operation. +2. **`algo.CCH.query`** — answers a point-to-point query using the materialized hierarchy. This is a read operation you run as many times as you like. + +Because the hierarchy lives entirely in the graph (as edges and properties), nothing is kept in server memory between calls. + + + CCH shines when the graph topology is stable and you issue many queries + against it. For a handful of one-off queries, or when the graph changes + frequently, [algo.SPpaths](/algorithms/sppath) or + [algo.AStar](/algorithms/astar) are simpler and avoid the build cost. + + +## Building the hierarchy — `algo.CCH` + +`algo.CCH` preprocesses the sub-graph induced by `relTypes` for the metric `weightProp`, and commits the result to the graph: + +- every improving shortcut becomes a `shortcutRelType` edge carrying its weight under `weightProp` and its middle node under `middleProp`; +- every node receives its elimination rank under `rankProp`. + +### Syntax + +```cypher +CALL algo.CCH({ + relTypes: [], + weightProp: , + shortcutRelType: , + rankProp: , + middleProp: +}) +YIELD shortcutsCreated +``` + +### Parameters + +| Name | Type | Description | +|-------------------|--------|---------------------------------------------------------------------------------------------------| +| `relTypes` | Array | Relationship types that form the graph to preprocess | +| `weightProp` | String | Edge property to minimize (the metric, e.g. `length` or `time`) | +| `shortcutRelType` | String | Relationship type used for the shortcut edges the build creates | +| `rankProp` | String | Node property the build writes each node's elimination rank into | +| `middleProp` | String | Edge property on each shortcut holding its middle node's id (used to unpack shortcuts into a path) | + +All five keys are required. + +### Returns + +| Name | Type | Description | +|--------------------|---------|--------------------------------------| +| `shortcutsCreated` | Integer | Number of shortcut edges materialized | + + + `algo.CCH` **modifies the graph**: it creates `shortcutRelType` edges and + writes the `rankProp` and `middleProp` properties. Choose names that do not + clash with your existing schema, and re-run `algo.CCH` after any change to the + graph's topology or to the `weightProp` values — the materialized hierarchy is + only valid for the graph it was built on. + + +## Querying — `algo.CCH.query` + +`algo.CCH.query` runs a rank-aware bidirectional search over the original edges **and** the shortcut edges, then unpacks the result back into a path made of original edges. + +### Syntax + +```cypher +CALL algo.CCH.query({ + sourceNode: , + targetNode: , + relTypes: [], + shortcutRelType: , + weightProp: , + rankProp: , + middleProp: +}) +YIELD pathWeight, path +``` + +### Parameters + +| Name | Type | Description | +|-------------------|--------|-------------------------------------------------------------------------| +| `sourceNode` | Node | Starting node | +| `targetNode` | Node | Destination node | +| `relTypes` | Array | The original relationship types (same as used to build) | +| `shortcutRelType` | String | The shortcut relationship type created by `algo.CCH` | +| `weightProp` | String | The metric property (same as used to build) | +| `rankProp` | String | The node rank property written by `algo.CCH` | +| `middleProp` | String | The shortcut middle-node property written by `algo.CCH` | + + + The `relTypes`, `weightProp`, `shortcutRelType`, `rankProp` and `middleProp` + values passed to `algo.CCH.query` must match the ones used to build the + hierarchy with `algo.CCH`. + + +### Returns + +| Name | Type | Description | +|--------------|-------|------------------------------------------------| +| `pathWeight` | Float | Total `weightProp` of the shortest path | +| `path` | Path | Shortest path from source to target, expanded into original edges | + +If the target is unreachable from the source, the procedure returns no rows. + +## Example + +Build the hierarchy once, then query it. Here `ROAD` edges carry a `w` weight, shortcuts use the `SHORTCUT` type, ranks are stored under `rank`, and shortcut middles under `mid`: + +```cypher +// 1. a small road graph +CREATE + (a:Junction {name:'A'}), (b:Junction {name:'B'}), (c:Junction {name:'C'}), + (a)-[:ROAD {w:10}]->(b), (b)-[:ROAD {w:10}]->(a), + (a)-[:ROAD {w:1}]->(c), (c)-[:ROAD {w:1}]->(a), + (c)-[:ROAD {w:1}]->(b), (b)-[:ROAD {w:1}]->(c) +``` + +```cypher +// 2. build the hierarchy (run once) +CALL algo.CCH({ + relTypes: ['ROAD'], + weightProp: 'w', + shortcutRelType: 'SHORTCUT', + rankProp: 'rank', + middleProp: 'mid' +}) +YIELD shortcutsCreated +RETURN shortcutsCreated +``` + +```cypher +// 3. query it (run many times) +MATCH (a:Junction {name:'A'}), (b:Junction {name:'B'}) +CALL algo.CCH.query({ + sourceNode: a, + targetNode: b, + relTypes: ['ROAD'], + shortcutRelType: 'SHORTCUT', + weightProp: 'w', + rankProp: 'rank', + middleProp: 'mid' +}) +YIELD pathWeight, path +RETURN pathWeight, [n IN nodes(path) | n.name] AS route +``` + +#### Expected Result: +| pathWeight | route | +|------------|------------| +| `2` | [A, C, B] | + +The direct `A → B` road costs `10`, but the shortest route `A → C → B` costs `2`; the query returns that path, expanded back into the original `ROAD` edges. + +--- + +## Frequently Asked Questions + + + + Use CCH when the graph is large and mostly static, and you run **many** point-to-point queries against it — the one-time build is amortized over fast repeated queries. For occasional queries, or a graph that changes often, prefer [algo.SPpaths](/algorithms/sppath) or [algo.AStar](/algorithms/astar). + + + Yes. It creates `shortcutRelType` edges and writes the `rankProp` and `middleProp` properties. `algo.CCH.query` is read-only and does not change the graph. + + + The materialized hierarchy is only valid for the graph it was built on. After changing the topology or the `weightProp` values, re-run `algo.CCH` to rebuild it. + + + Yes. Build each one with distinct `shortcutRelType`, `rankProp` and `middleProp` names — for example one hierarchy optimizing distance and another optimizing travel time — and pass the matching names to `algo.CCH.query`. + + + Yes. `algo.CCH.query` is read-only and holds no shared state, so many queries can run concurrently against the same graph. + + diff --git a/algorithms/index.mdx b/algorithms/index.mdx index b0d2abcf..47cf8245 100644 --- a/algorithms/index.mdx +++ b/algorithms/index.mdx @@ -25,6 +25,12 @@ This overview summarizes the available algorithms and links to their individual - **[SPpath](/algorithms/sppath)** Computes the shortest paths between a source and one or more destination nodes. +- **[A* Search](/algorithms/astar)** + Finds the shortest path between two nodes using A* search guided by a geographic (haversine) heuristic — fast on spatial graphs where nodes have coordinates. + +- **[CCH](/algorithms/cch)** + Preprocesses a weighted graph into a Customizable Contraction Hierarchy for fast, repeated point-to-point shortest-path queries. + - **[SSpath](/algorithms/sspath)** Enumerates all paths from a single source node to other nodes, based on constraints like edge filters and depth. diff --git a/docs.json b/docs.json index 7c970f92..50d719dc 100644 --- a/docs.json +++ b/docs.json @@ -89,6 +89,8 @@ "algorithms/cdlp", "algorithms/pagerank", "algorithms/sppath", + "algorithms/astar", + "algorithms/cch", "algorithms/sspath", "algorithms/wcc", "algorithms/harmonic-centrality",