Chapter 5: The GQL Query Language
AstraeaDB implements a query language compatible with the ISO GQL standard (released 2024). This chapter covers pattern matching, filtering, mutations, aggregation, and built-in functions—everything you need to read and write data using declarative queries.
The Python Query Interface
The primary way to execute GQL in AstraeaDB is through the query() method available
on every client library. The Python API looks like this:
result = client.query("GQL STRING")
Every query returns a dictionary with three keys:
{
"columns": ["a.name", "b.name"], // column headers from RETURN
"rows": [["Alice", "Bob"], ...], // result rows
"stats": {"nodes_created": 0, ...} // mutation statistics
}
5.1 Pattern Matching Deep Dive
Pattern matching is the heart of GQL. Instead of writing procedural code to walk a graph, you describe the shape of the subgraph you want, and the engine finds all matches.
Node Patterns
A node is represented by parentheses. You can optionally bind it to a variable, add a label, or specify properties:
| Pattern | Meaning |
|---|---|
(n) | Any node, bound to variable n |
(n:Person) | Any node with the label Person |
(n:Person {name: "Alice"}) | A Person node whose name property equals "Alice" |
(:Movie) | An anonymous Movie node (no variable binding) |
Edge Patterns
Edges connect nodes with arrows. The arrow direction indicates relationship direction:
| Pattern | Meaning |
|---|---|
-[:KNOWS]-> | Outgoing edge of type KNOWS |
<-[:KNOWS]- | Incoming edge of type KNOWS |
-[:KNOWS]- | Undirected (either direction) |
-[e:KNOWS]-> | Outgoing KNOWS edge bound to variable e |
-[e:KNOWS {since: 2020}]-> | Edge with a property constraint |
Combined Patterns
Combine node and edge patterns into full graph shapes:
MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name
This finds every pair of Person nodes connected by a KNOWS edge and returns both names.
Multi-Hop Patterns
Chain patterns to traverse multiple hops in a single query:
-- Find friends-of-friends MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person) RETURN a.name, c.name
Visual Pattern Matching
One of the most intuitive aspects of GQL is that the query looks like the shape it matches. Consider this ASCII representation of a movie database:
Complete Example with Tabs
from astraeadb import AstraeaClient with AstraeaClient() as client: # Find all actors and the movies they acted in result = client.query(""" MATCH (a:Person)-[:ACTED_IN]->(m:Movie) RETURN a.name, m.title """) for row in result["rows"]: print(f"{row[0]} acted in {row[1]}") # Multi-hop: friends of friends fof = client.query(""" MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person) RETURN a.name AS person, c.name AS friend_of_friend """) print(fof["columns"]) # ["person", "friend_of_friend"] print(fof["rows"]) # [["Alice", "Charlie"], ...]
library(astraeadb) client <- astraea_client() client$connect() # Find all actors and the movies they acted in result <- client$query(" MATCH (a:Person)-[:ACTED_IN]->(m:Movie) RETURN a.name, m.title ") for (i in seq_len(nrow(result$rows))) { cat(result$rows[i, 1], "acted in", result$rows[i, 2], "\n") } # Multi-hop: friends of friends fof <- client$query(" MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person) RETURN a.name AS person, c.name AS friend_of_friend ") print(fof$columns) # [1] "person" "friend_of_friend" print(fof$rows) # data.frame with results client$close()
package main import ( "context" "fmt" "github.com/AstraeaDB/AstraeaDB-Official" ) func main() { ctx := context.Background() client := astraeadb.NewClient( astraeadb.WithAddress("127.0.0.1", 7687), ) client.Connect(ctx) defer client.Close() // Find all actors and the movies they acted in result, _ := client.Query(ctx, ` MATCH (a:Person)-[:ACTED_IN]->(m:Movie) RETURN a.name, m.title `) for _, row := range result.Rows { fmt.Printf("%s acted in %s\n", row[0], row[1]) } // Multi-hop: friends of friends fof, _ := client.Query(ctx, ` MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person) RETURN a.name AS person, c.name AS friend_of_friend `) fmt.Println(fof.Columns) // [person friend_of_friend] fmt.Println(fof.Rows) // [[Alice Charlie] ...] }
import com.astraeadb.unified.UnifiedClient; import com.astraeadb.unified.QueryResult; public class PatternMatchExample { public static void main(String[] args) { try (var client = UnifiedClient.builder() .host("127.0.0.1").build()) { client.connect(); // Find all actors and the movies they acted in QueryResult result = client.query(""" MATCH (a:Person)-[:ACTED_IN]->(m:Movie) RETURN a.name, m.title """); for (var row : result.rows()) { System.out.printf("%s acted in %s%n", row.get(0), row.get(1)); } // Multi-hop: friends of friends QueryResult fof = client.query(""" MATCH (a:Person)-[:KNOWS]->(b)-[:KNOWS]->(c:Person) RETURN a.name AS person, c.name AS friend_of_friend """); System.out.println(fof.columns()); System.out.println(fof.rows()); } } }
5.2 Filtering with WHERE
The WHERE clause filters matched patterns based on property values. It appears after
MATCH and before RETURN.
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
= | Equal to | WHERE n.age = 30 |
<> | Not equal to | WHERE n.name <> "Bob" |
< | Less than | WHERE n.age < 25 |
> | Greater than | WHERE n.age > 25 |
<= | Less than or equal | WHERE n.age <= 30 |
>= | Greater than or equal | WHERE n.age >= 18 |
Boolean Operators
Combine conditions with AND, OR, and NOT:
-- Multiple conditions with AND MATCH (n:Person) WHERE n.age > 25 AND n.name <> "Bob" RETURN n.name, n.age
-- OR to broaden the filter MATCH (n:Person) WHERE n.city = "London" OR n.city = "Paris" RETURN n.name, n.city
-- NOT to negate a condition MATCH (n:Person) WHERE NOT n.age < 18 RETURN n.name, n.age
String Matching
Use the STARTS WITH, ENDS WITH, and CONTAINS operators for string filtering:
-- Find people whose name starts with "A" MATCH (n:Person) WHERE n.name STARTS WITH "A" RETURN n.name
-- Find movies containing "Matrix" in the title MATCH (m:Movie) WHERE m.title CONTAINS "Matrix" RETURN m.title, m.year
Complete Filtering Example
with AstraeaClient() as client: # Find adults in London who know someone over 30 result = client.query(""" MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.city = "London" AND a.age >= 18 AND b.age > 30 RETURN a.name, a.age, b.name, b.age """) for row in result["rows"]: print(f"{row[0]} (age {row[1]}) knows {row[2]} (age {row[3]})") # Example output: # columns: ["a.name", "a.age", "b.name", "b.age"] # rows: [["Alice", 30, "Charlie", 35], ...]
client <- astraea_client() client$connect() # Find adults in London who know someone over 30 result <- client$query(' MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.city = "London" AND a.age >= 18 AND b.age > 30 RETURN a.name, a.age, b.name, b.age ') print(result$rows) # a.name a.age b.name b.age # 1 Alice 30 Charlie 35 client$close()
ctx := context.Background() client := astraeadb.NewClient( astraeadb.WithAddress("127.0.0.1", 7687), ) client.Connect(ctx) defer client.Close() // Find adults in London who know someone over 30 result, _ := client.Query(ctx, ` MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.city = "London" AND a.age >= 18 AND b.age > 30 RETURN a.name, a.age, b.name, b.age `) for _, row := range result.Rows { fmt.Printf("%s (age %v) knows %s (age %v)\n", row[0], row[1], row[2], row[3]) }
try (var client = UnifiedClient.builder() .host("127.0.0.1").build()) { client.connect(); // Find adults in London who know someone over 30 QueryResult result = client.query(""" MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.city = "London" AND a.age >= 18 AND b.age > 30 RETURN a.name, a.age, b.name, b.age """); for (var row : result.rows()) { System.out.printf("%s (age %s) knows %s (age %s)%n", row.get(0), row.get(1), row.get(2), row.get(3)); } }
5.3 RETURN, ORDER BY, SKIP, LIMIT
The RETURN clause defines which columns appear in the result set. Combined with
ORDER BY, SKIP, and LIMIT, it gives you full control
over result shape and pagination.
Projections
Return specific properties rather than entire nodes:
MATCH (n:Person) RETURN n.name, n.age
Result:
columns: ["n.name", "n.age"] rows: ["Alice", 30] ["Bob", 28] ["Charlie", 35] ["Diana", 22]
Aliasing with AS
Rename columns in the output for clarity:
MATCH (n:Person) RETURN n.name AS person_name, n.age AS person_age
Result:
columns: ["person_name", "person_age"] rows: ["Alice", 30] ["Bob", 28] ...
Sorting with ORDER BY
Sort results by one or more expressions. Use ASC (default) or DESC:
MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC
Result:
rows: ["Charlie", 35] ["Alice", 30] ["Bob", 28] ["Diana", 22]
Pagination with SKIP and LIMIT
SKIP ignores the first N rows; LIMIT caps the total rows returned:
-- Page 1: first 2 results MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC LIMIT 2
rows: [["Charlie", 35], ["Alice", 30]]
-- Page 2: skip first 2, take next 2 MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC SKIP 2 LIMIT 2
rows: [["Bob", 28], ["Diana", 22]]
Combined Example
with AstraeaClient() as client: # Paginated, sorted query page_size = 5 page_num = 3 # 0-indexed result = client.query(f""" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN p.name AS actor, m.title AS movie, m.year AS year ORDER BY m.year DESC SKIP {page_num * page_size} LIMIT {page_size} """) print(result["columns"]) # ["actor", "movie", "year"] for row in result["rows"]: print(f" {row[0]:15} | {row[1]:25} | {row[2]}")
page_size <- 5 page_num <- 3 result <- client$query(sprintf(" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN p.name AS actor, m.title AS movie, m.year AS year ORDER BY m.year DESC SKIP %d LIMIT %d ", page_num * page_size, page_size)) print(result$rows) # actor movie year # 1 Alice The Matrix 1999
pageSize := 5 pageNum := 3 query := fmt.Sprintf(` MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN p.name AS actor, m.title AS movie, m.year AS year ORDER BY m.year DESC SKIP %d LIMIT %d `, pageNum*pageSize, pageSize) result, _ := client.Query(ctx, query) fmt.Println(result.Columns) // [actor movie year] for _, row := range result.Rows { fmt.Printf(" %-15s | %-25s | %v\n", row[0], row[1], row[2]) }
int pageSize = 5; int pageNum = 3; QueryResult result = client.query(String.format(""" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN p.name AS actor, m.title AS movie, m.year AS year ORDER BY m.year DESC SKIP %d LIMIT %d """, pageNum * pageSize, pageSize)); System.out.println(result.columns()); for (var row : result.rows()) { System.out.printf(" %-15s | %-25s | %s%n", row.get(0), row.get(1), row.get(2)); }
5.4 Aggregation Functions
AstraeaDB supports standard aggregation functions. When you use an aggregation function in RETURN,
all non-aggregated columns act as implicit GROUP BY keys—just like in SQL.
Available Functions
| Function | Description | Example |
|---|---|---|
count(n) | Number of matched items | RETURN count(n) |
sum(n.age) | Sum of numeric values | RETURN sum(n.age) |
avg(n.age) | Average of numeric values | RETURN avg(n.age) |
min(n.age) | Minimum value | RETURN min(n.age) |
max(n.age) | Maximum value | RETURN max(n.age) |
Implicit GROUP BY
When a RETURN clause mixes aggregated and non-aggregated expressions, the
non-aggregated expressions automatically become grouping keys. For example:
-- Count how many actors appeared in each movie MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN m.title, count(p) AS actor_count ORDER BY count(p) DESC
Result:
columns: ["m.title", "actor_count"] rows: ["The Matrix", 4] ["John Wick", 3] ["Speed", 2] ["Point Break", 2]
More Aggregation Examples
-- Average age of people in each city MATCH (n:Person) RETURN n.city, avg(n.age) AS avg_age, count(n) AS population ORDER BY avg_age DESC
-- Find the youngest and oldest person overall MATCH (n:Person) RETURN min(n.age) AS youngest, max(n.age) AS oldest, sum(n.age) AS total_age
Aggregation with Filtering
with AstraeaClient() as client: # Movies with at least 3 actors, sorted by cast size result = client.query(""" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN m.title AS movie, count(p) AS cast_size, avg(p.age) AS avg_age ORDER BY cast_size DESC """) print(f"{'Movie':25} {'Cast':6} {'Avg Age'}") print("-" * 42) for row in result["rows"]: print(f"{row[0]:25} {row[1]:6} {row[2]:.1f}") # Output: # Movie Cast Avg Age # ------------------------------------------ # The Matrix 4 32.5 # John Wick 3 38.0
result <- client$query(" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN m.title AS movie, count(p) AS cast_size, avg(p.age) AS avg_age ORDER BY cast_size DESC ") print(result$rows) # movie cast_size avg_age # 1 The Matrix 4 32.5 # 2 John Wick 3 38.0
result, _ := client.Query(ctx, ` MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN m.title AS movie, count(p) AS cast_size, avg(p.age) AS avg_age ORDER BY cast_size DESC `) fmt.Printf("%-25s %-6s %s\n", "Movie", "Cast", "Avg Age") for _, row := range result.Rows { fmt.Printf("%-25s %-6v %v\n", row[0], row[1], row[2]) }
QueryResult result = client.query(""" MATCH (p:Person)-[:ACTED_IN]->(m:Movie) RETURN m.title AS movie, count(p) AS cast_size, avg(p.age) AS avg_age ORDER BY cast_size DESC """); System.out.printf("%-25s %-6s %s%n", "Movie", "Cast", "Avg Age"); for (var row : result.rows()) { System.out.printf("%-25s %-6s %s%n", row.get(0), row.get(1), row.get(2)); }
5.5 CREATE and DELETE in GQL
GQL is not just for querying—it also supports data mutation. AstraeaDB's CREATE
and DELETE clauses let you build and tear down graph structures declaratively.
Creating Nodes
Create a new node with labels and properties:
CREATE (n:Person {name: "Dave", age: 35, city: "Berlin"})
The stats field in the response confirms the mutation:
{
"columns": [],
"rows": [],
"stats": {"nodes_created": 1, "edges_created": 0,
"nodes_deleted": 0, "edges_deleted": 0}
}
Creating Edges
MATCH to find the existing nodes first, then CREATE the relationship
between them.
-- First, match both endpoints MATCH (a:Person {name: "Alice"}), (d:Person {name: "Dave"}) CREATE (a)-[:KNOWS {since: 2020}]->(d)
Response stats:
{"stats": {"nodes_created": 0, "edges_created": 1, "nodes_deleted": 0, "edges_deleted": 0}}
Deleting Nodes and Edges
Use MATCH to find the element, then DELETE to remove it:
-- Delete a specific person MATCH (n:Person {name: "Dave"}) DELETE n
-- Delete a specific edge MATCH (a:Person {name: "Alice"})-[e:KNOWS]->(b:Person {name: "Bob"}) DELETE e
Response stats for node deletion:
{"stats": {"nodes_created": 0, "edges_created": 0, "nodes_deleted": 1, "edges_deleted": 0}}
Complete CREATE/DELETE Example
with AstraeaClient() as client: # Create a new person r1 = client.query('CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})') print(r1["stats"]) # {"nodes_created": 1, ...} # Create an edge between existing nodes r2 = client.query(""" MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"}) CREATE (a)-[:KNOWS {since: 2024}]->(e) """) print(r2["stats"]) # {"edges_created": 1, ...} # Verify the new connection r3 = client.query(""" MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name, b.city """) print(r3["rows"]) # [["Bob", "London"], ["Eve", "Tokyo"], ...] # Delete the person (clean up) r4 = client.query('MATCH (n:Person {name: "Eve"}) DELETE n') print(r4["stats"]) # {"nodes_deleted": 1, ...}
# Create a new person r1 <- client$query('CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})') print(r1$stats) # nodes_created: 1 # Create an edge between existing nodes r2 <- client$query(' MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"}) CREATE (a)-[:KNOWS {since: 2024}]->(e) ') print(r2$stats) # edges_created: 1 # Verify the new connection r3 <- client$query(' MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name, b.city ') print(r3$rows) # Delete the person r4 <- client$query('MATCH (n:Person {name: "Eve"}) DELETE n') print(r4$stats) # nodes_deleted: 1
// Create a new person r1, _ := client.Query(ctx, `CREATE (n:Person {name: "Eve", age: 27, city: "Tokyo"})`) fmt.Println(r1.Stats) // {NodesCreated:1 ...} // Create an edge between existing nodes r2, _ := client.Query(ctx, ` MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"}) CREATE (a)-[:KNOWS {since: 2024}]->(e) `) fmt.Println(r2.Stats) // {EdgesCreated:1 ...} // Verify the new connection r3, _ := client.Query(ctx, ` MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name, b.city `) fmt.Println(r3.Rows) // Delete the person r4, _ := client.Query(ctx, `MATCH (n:Person {name: "Eve"}) DELETE n`) fmt.Println(r4.Stats) // {NodesDeleted:1 ...}
// Create a new person QueryResult r1 = client.query( "CREATE (n:Person {name: \"Eve\", age: 27, city: \"Tokyo\"})"); System.out.println(r1.stats()); // {nodes_created=1, ...} // Create an edge between existing nodes QueryResult r2 = client.query(""" MATCH (a:Person {name: "Alice"}), (e:Person {name: "Eve"}) CREATE (a)-[:KNOWS {since: 2024}]->(e) """); System.out.println(r2.stats()); // {edges_created=1, ...} // Verify the new connection QueryResult r3 = client.query(""" MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name, b.city """); System.out.println(r3.rows()); // Delete the person QueryResult r4 = client.query( "MATCH (n:Person {name: \"Eve\"}) DELETE n"); System.out.println(r4.stats()); // {nodes_deleted=1, ...}
5.6 Built-in Functions
AstraeaDB provides several built-in functions for inspecting and transforming graph elements within GQL queries.
Identity and Metadata Functions
| Function | Description | Returns |
|---|---|---|
id(n) |
Internal numeric identifier for a node or edge | Integer |
labels(n) |
List of labels attached to a node | List of strings |
type(e) |
The relationship type of an edge | String |
Examples: id(), labels(), type()
-- Get the internal ID and labels of all Person nodes MATCH (n:Person) RETURN id(n), labels(n), n.name
columns: ["id(n)", "labels(n)", "n.name"] rows: [0, ["Person"], "Alice"] [1, ["Person"], "Bob"] [4, ["Person", "Actor"], "Charlie"]
-- Inspect edge types in a pattern MATCH (a:Person)-[e]->(b) RETURN a.name, type(e), b.name
columns: ["a.name", "type(e)", "b.name"] rows: ["Alice", "KNOWS", "Bob"] ["Alice", "ACTED_IN", "The Matrix"] ["Bob", "KNOWS", "Charlie"]
Type Conversion Functions
| Function | Description | Example |
|---|---|---|
toString(value) |
Converts a value to its string representation | toString(42) returns "42" |
toInteger(value) |
Converts a string to an integer | toInteger("42") returns 42 |
-- Convert age to string for concatenation MATCH (n:Person) RETURN n.name, toString(n.age) AS age_str
The DISTINCT Keyword
Use DISTINCT to eliminate duplicate rows from results:
-- Find unique cities where people live MATCH (n:Person) RETURN DISTINCT n.city
columns: ["n.city"] rows: ["London"] ["Paris"] ["Berlin"] ["Tokyo"]
-- Find unique relationship types in the graph MATCH ()-[e]->() RETURN DISTINCT type(e) AS relationship_type
columns: ["relationship_type"] rows: ["KNOWS"] ["ACTED_IN"] ["DIRECTED"]
Built-in Functions Example
with AstraeaClient() as client: # Explore the graph metadata result = client.query(""" MATCH (a:Person)-[e]->(b) RETURN id(a) AS from_id, labels(a) AS from_labels, type(e) AS rel_type, id(b) AS to_id, labels(b) AS to_labels """) for row in result["rows"]: print(f"Node {row[0]} {row[1]} --[{row[2]}]--> Node {row[3]} {row[4]}") # Find unique relationship types types = client.query(""" MATCH ()-[e]->() RETURN DISTINCT type(e) AS rel_type """) print("Relationship types:", [r[0] for r in types["rows"]]) # Relationship types: ["KNOWS", "ACTED_IN", "DIRECTED"]
# Explore the graph metadata result <- client$query(" MATCH (a:Person)-[e]->(b) RETURN id(a) AS from_id, labels(a) AS from_labels, type(e) AS rel_type, id(b) AS to_id, labels(b) AS to_labels ") print(result$rows) # Find unique relationship types types <- client$query(" MATCH ()-[e]->() RETURN DISTINCT type(e) AS rel_type ") print(types$rows) # rel_type # 1 KNOWS # 2 ACTED_IN # 3 DIRECTED
// Explore the graph metadata result, _ := client.Query(ctx, ` MATCH (a:Person)-[e]->(b) RETURN id(a) AS from_id, labels(a) AS from_labels, type(e) AS rel_type, id(b) AS to_id, labels(b) AS to_labels `) for _, row := range result.Rows { fmt.Printf("Node %v %v --[%v]--> Node %v %v\n", row[0], row[1], row[2], row[3], row[4]) } // Find unique relationship types types, _ := client.Query(ctx, ` MATCH ()-[e]->() RETURN DISTINCT type(e) AS rel_type `) fmt.Println("Relationship types:", types.Rows)
// Explore the graph metadata QueryResult result = client.query(""" MATCH (a:Person)-[e]->(b) RETURN id(a) AS from_id, labels(a) AS from_labels, type(e) AS rel_type, id(b) AS to_id, labels(b) AS to_labels """); for (var row : result.rows()) { System.out.printf("Node %s %s --[%s]--> Node %s %s%n", row.get(0), row.get(1), row.get(2), row.get(3), row.get(4)); } // Find unique relationship types QueryResult types = client.query(""" MATCH ()-[e]->() RETURN DISTINCT type(e) AS rel_type """); System.out.println("Relationship types: " + types.rows());
Chapter Summary
- Pattern Matching: Describe the shape you want with
MATCH—the engine finds all instances. - Filtering:
WHEREsupports comparisons, boolean logic, and string operators. - Projections:
RETURNwithASaliases,ORDER BY,SKIP, andLIMITfor clean, paginated output. - Aggregation:
count,sum,avg,min,maxwith implicit grouping. - Mutations:
CREATEfor nodes/edges,DELETEafterMATCH. Stats confirm every mutation. - Built-in Functions:
id(),labels(),type(), conversions, andDISTINCT.
In the next chapter, we move beyond declarative queries to explore the traversal algorithms built into AstraeaDB: BFS, DFS, shortest path, and neighbor expansion.