-
Notifications
You must be signed in to change notification settings - Fork 11
docs(algorithms): add algo.AStar and algo.CCH pages #558
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Qualify the optimality claim. The introduction says that A* returns an optimal path without a condition. The same page explains that an inadmissible Proposed wording-... this lets A* explore far fewer nodes than a plain Dijkstra search, while still returning an optimal path.
+... this lets A* explore far fewer nodes than a plain Dijkstra search, while returning an optimal path when `heuristicScale` is admissible.
-... and the search returns a sub-optimal path. Set `heuristicScale` ...
+... and the search may return a sub-optimal path. Set `heuristicScale` ...🤖 Prompt for AI Agents |
||
|
|
||
| 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: <node>, | ||
| targetNode: <node>, | ||
| relTypes: [<relationship_type>], | ||
| weightProp: <property>, | ||
| latitudeProperty: <property>, | ||
| longitudeProperty: <property>, | ||
| heuristicScale: <number>, // optional, default 1.0 | ||
| relDirection: "outgoing", // optional: "outgoing", "incoming", "both" | ||
| pathCount: <int> // 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 | | ||
|
|
||
| <Warning> | ||
| 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`. | ||
| </Warning> | ||
|
|
||
| <Note> | ||
| `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. | ||
| </Note> | ||
|
|
||
| ## 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 | ||
|
|
||
| <AccordionGroup> | ||
| <Accordion title="When should I use algo.AStar vs algo.SPpaths?"> | ||
| 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. | ||
| </Accordion> | ||
| <Accordion title="Why is my A* path not the shortest one?"> | ||
| 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)). | ||
| </Accordion> | ||
| <Accordion title="What happens if a node is missing its latitude or longitude?"> | ||
| 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. | ||
| </Accordion> | ||
| <Accordion title="Can I run many point-to-point queries efficiently on a static graph?"> | ||
| 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. | ||
| </Accordion> | ||
| </AccordionGroup> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
||
| <Note> | ||
| 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. | ||
| </Note> | ||
|
|
||
| ## 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: [<relationship_type>], | ||
| weightProp: <property>, | ||
| shortcutRelType: <relationship_type>, | ||
| rankProp: <property>, | ||
| middleProp: <property> | ||
| }) | ||
| 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 | | ||
|
|
||
| <Warning> | ||
| `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. | ||
| </Warning> | ||
|
|
||
| ## 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: <node>, | ||
| targetNode: <node>, | ||
| relTypes: [<relationship_type>], | ||
| shortcutRelType: <relationship_type>, | ||
| weightProp: <property>, | ||
| rankProp: <property>, | ||
| middleProp: <property> | ||
| }) | ||
| 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` | | ||
|
|
||
| <Note> | ||
| 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`. | ||
| </Note> | ||
|
|
||
| ### 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 | ||
|
|
||
| <AccordionGroup> | ||
| <Accordion title="When should I use CCH instead of algo.SPpaths or algo.AStar?"> | ||
| 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). | ||
| </Accordion> | ||
| <Accordion title="Does algo.CCH modify my graph?"> | ||
| Yes. It creates `shortcutRelType` edges and writes the `rankProp` and `middleProp` properties. `algo.CCH.query` is read-only and does not change the graph. | ||
| </Accordion> | ||
| <Accordion title="What happens if the graph changes after I build the hierarchy?"> | ||
| 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. | ||
| </Accordion> | ||
| <Accordion title="Can I keep more than one hierarchy on the same graph?"> | ||
| 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`. | ||
| </Accordion> | ||
| <Accordion title="Can multiple queries run at the same time?"> | ||
| Yes. `algo.CCH.query` is read-only and holds no shared state, so many queries can run concurrently against the same graph. | ||
| </Accordion> | ||
| </AccordionGroup> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add the terms reported by spellcheck.
The spellcheck job still fails because this list does not contain
optimalityorheuristicScale, both used inalgorithms/astar.mdx. Add the exact spellings before merge.Proposed additions
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Pipeline failures