Graph Queries Tutorial
Graph Queries Tutorial
Difficulty: Intermediate
Prerequisites: Basic HeliosDB SQL knowledge
Module: src/graph/
HeliosDB includes a property-graph layer for modeling and traversing relationships. This tutorial covers the data model, API usage, traversal algorithms, and planned SQL integration.
1. Concepts
HeliosDB’s graph layer uses a property-graph model:
- Nodes are identified by a 64-bit unsigned integer (compatible with row IDs in the relational engine)
- Edges are directed, typed (labeled), and optionally weighted
- Adjacency lists are stored in concurrent DashMap structures for both outgoing and incoming edges
This design is ideal for social networks, knowledge graphs, dependency trees, and recommendation engines.
2. Creating a Graph
The graph store is created via the Rust API:
use heliosdb_lite::graph::{GraphStore, Edge};
// Create an in-memory graphlet graph = GraphStore::in_memory();3. Adding Nodes and Edges
Edges implicitly create nodes. Each edge has a source, destination, label, and optional weight:
// Unweighted edges (default weight = 1.0)graph.add_edge(Edge::new(1, 2, "FOLLOWS"));graph.add_edge(Edge::new(1, 3, "FOLLOWS"));graph.add_edge(Edge::new(2, 3, "KNOWS"));
// Weighted edgegraph.add_edge(Edge::with_weight(3, 4, "REPORTS_TO", 0.5));Removing Edges
// Remove all edges between nodes 1 and 2 (regardless of label)let removed = graph.remove_edges(1, 2);assert_eq!(removed, 1);Querying Adjacency
// Outgoing edges from node 1let out = graph.out_edges(1);// out = [Edge { from: 1, to: 3, label: "FOLLOWS", weight: 1.0 }]
// Incoming edges to node 3let inc = graph.in_edges(3);// inc contains edges from 1 and 2
// Degree querieslet out_deg = graph.out_degree(1); // 1 (after removing edge to 2)let in_deg = graph.in_degree(3); // 24. Traversal: BFS
Breadth-first search visits nodes layer by layer from a source node:
use heliosdb_lite::graph::traverse::bfs;
let result = bfs(&graph, 1, u32::MAX);
// result.order: nodes in BFS visit order// result.depth: HashMap<NodeId, u32> with hop distance from source
for node in &result.order { let d = result.depth[node]; println!("Node {} is {} hops from source", node, d);}Bounded BFS
Limit traversal depth to find nodes within N hops:
// Only visit nodes within 2 hopslet nearby = bfs(&graph, 1, 2);5. Traversal: Dijkstra Shortest Path
Find the shortest (minimum-weight) path between two nodes:
use heliosdb_lite::graph::traverse::dijkstra;
let result = dijkstra(&graph, 1);
// result.dist: HashMap<NodeId, f32> with total cost from source// result.prev: HashMap<NodeId, NodeId> predecessor map for path reconstruction
if let Some(&cost) = result.dist.get(&4) { println!("Shortest path cost from 1 to 4: {}", cost);}Reconstructing the Path
use heliosdb_lite::graph::traverse::shortest_path;
match shortest_path(&graph, 1, 4) { Ok(path) => { println!("Path: {:?}, cost: {}", path.nodes, path.cost); // e.g., Path: [1, 3, 4], cost: 1.5 } Err(e) => println!("No path: {}", e),}Dijkstra requires non-negative edge weights. A
NegativeWeighterror is returned if a negative weight is encountered.
6. Traversal: A* Search
A* extends Dijkstra with a heuristic function for faster goal-directed search:
use heliosdb_lite::graph::traverse::a_star;
// Heuristic: estimated cost from node to targetlet heuristic = |node: u64| -> f32 { match node { 3 => 0.5, // Node 3 is close to target _ => 1.0, // Default estimate }};
match a_star(&graph, 1, 4, heuristic) { Ok(path) => println!("A* path: {:?}, cost: {}", path.nodes, path.cost), Err(e) => println!("No path: {}", e),}The heuristic must be admissible (never overestimate) for A* to find optimal paths.
7. Planned SQL Functions
The graph module declares four SQL functions for future integration:
-- BFS traversal: returns reachable node IDs ordered by depthSELECT * FROM graph_bfs(1, 10);
-- Dijkstra shortest path between two nodesSELECT * FROM graph_shortest_path(1, 4);
-- A* search with a heuristic columnSELECT * FROM graph_a_star(1, 4);
-- Immediate out-neighbours of a nodeSELECT * FROM graph_neighbors(1);Note: These SQL functions are declared but not yet wired to the executor. The wiring is deferred until persistent EdgeStore backends (NativeBackend, RocksDB) are implemented. Currently, graph operations are available through the Rust API only.
8. Example: Social Network
use heliosdb_lite::graph::{GraphStore, Edge};use heliosdb_lite::graph::traverse::{bfs, shortest_path};
let g = GraphStore::in_memory();
// Build a social networkg.add_edge(Edge::new(1, 2, "FOLLOWS")); // Alice -> Bobg.add_edge(Edge::new(1, 3, "FOLLOWS")); // Alice -> Carolg.add_edge(Edge::new(2, 4, "FOLLOWS")); // Bob -> Daveg.add_edge(Edge::new(3, 4, "FOLLOWS")); // Carol -> Daveg.add_edge(Edge::new(4, 5, "FOLLOWS")); // Dave -> Eve
// Find all users within 2 hops of Alicelet nearby = bfs(&g, 1, 2);// nearby.order = [1, 2, 3, 4] (Eve is 3 hops away)
// Shortest path from Alice to Evelet path = shortest_path(&g, 1, 5).unwrap();// path.nodes = [1, 2, 4, 5] or [1, 3, 4, 5], cost = 3.09. Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
NoPath error | No directed path exists | Check edge direction; edges are one-way |
NegativeWeight error | Edge has weight < 0 | Dijkstra requires non-negative weights; use BFS for unweighted traversal |
| Node not in results | Node has no edges | Nodes only exist when they have at least one edge |
Next Steps
- Combine graph traversal with relational queries by using node IDs as foreign keys
- Use weighted edges for recommendation scoring
- See
tests/graph_basic_helix.rsfor comprehensive test examples