Unlike traditional relational databases, which represent data in tables, graph databases capture the rich relationships between entities as first-class citizens. They model data as nodes (entities) and edges (relationships), closely mirroring how information is connected and related in real-world systems.
Why Graph Databases?
Graph databases have a unique strength: they can efficiently traverse and query complex relationships. Think of social networks where you want to identify mutual friends, or recommendation systems where you’d like to find related products based on user behavior. Such operations, which could be cumbersome and inefficient in relational databases, become inherently natural in graph databases.
Use Cases:
- Social Networks: Graph databases excel at representing and querying social relations, making operations like “friends of friends” exceptionally fast.
- Recommendation Engines: Services like Netflix or Amazon use graph-like structures to provide users with product or movie recommendations based on user behavior and preferences.
- Fraud Detection: Financial institutions utilize graph databases to detect unusual patterns or connections that might indicate fraudulent activities.
- Supply Chain & Logistics: Understanding dependencies and relationships in supply routes, especially to identify vulnerabilities or optimize for efficiency.
- Knowledge Graphs: Entities and their interrelations, such as those used by Google to enhance its search results, are best represented as graphs.
- Bioinformatics: In genetics research, graph databases can model the intricate relationships and pathways of genes, proteins, and more.
Given this backdrop, there’s a growing interest in understanding graph databases and even building custom ones tailored for specific needs. In this article, we’ll use Rust, a language celebrated for its safety and performance, to construct a simple yet illustrative graph database. Whether you’re diving into this out of sheer curiosity or have a specific project in mind, this hands-on guide aims to provide a solid foundation.
Let’s Begin!
Step 1: Setting Up the Project
Initialize a new Rust project:
cargo new graph_db
cd graph_db
Step 2: Defining Our Data Structures
We’ll start by defining the primary entities: Nodes and Relationships.
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct Node {
id: u32,
label: String,
properties: HashMap<String, String>,
}
#[derive(Debug, Clone)]
struct Relationship {
id: u32,
label: String,
start_node: u32,
end_node: u32,
properties: HashMap<String, String>,
}
Each Node and Relationship has an ID, label, and a set of properties.
Step 3: Creating the GraphDatabase Structure
This will be the main database object holding nodes and relationships.
#[derive(Debug)]
struct GraphDatabase {
nodes: HashMap<u32, Node>,
relationships: HashMap<u32, Relationship>,
}
Step 4: Implementing Database Methods
impl GraphDatabase {
fn new() -> Self {
GraphDatabase {
nodes: HashMap::new(),
relationships: HashMap::new(),
}
}
fn add_node(&mut self, id: u32, label: String, properties: HashMap<String, String>) {
let node = Node { id, label, properties };
self.nodes.insert(id, node);
}
fn add_relationship(&mut self, id: u32, label: String, start_node: u32, end_node: u32, properties: HashMap<String, String>) {
let relationship = Relationship { id, label, start_node, end_node, properties };
self.relationships.insert(id, relationship);
}
// ... additional methods for querying and updates ...
}

Step 5: Building the CLI
A Command-Line Interface (CLI) allows us to interact with our database.
For this, you can parse user input using Rust’s std::io library and execute corresponding database commands.
Step 6: Coloring the CLI
To enhance the CLI, you can use the colored crate for colorful terminal output.
[dependencies]
colored = "2.0"
With this crate, you can easily color strings, e.g., "text".blue().
Step 7: The Main Loop and Initialization Screen
The main function will house our command loop and an initialization screen to greet the user.
use colored::*;
use std::thread;
use std::time::Duration;
// ... previous code ...
fn clear_screen() {
print!("\x1B[2J\x1B[H");
io::stdout().flush().unwrap();
}
fn main() {
println!("{}", "Welcome to Rust Graph".green().bold());
thread::sleep(Duration::from_secs(3));
clear_screen();
let mut db = GraphDatabase::new();
loop {
// Command loop logic
}
}
Writing Tests for the GraphDatabase
Let’s start by writing tests for the methods in our GraphDatabase.
1. Testing Node Creation
We want to ensure that after adding a node, we can retrieve it by its ID.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_and_get_node() {
let mut db = GraphDatabase::new();
let properties = HashMap::new(); // Initially, an empty property set.
db.add_node(1, "User".to_string(), properties.clone());
let node = db.get_node_by_id(1).unwrap();
assert_eq!(node.label, "User");
assert_eq!(node.properties, properties);
}
}
2. Testing Relationship Creation
We want to confirm that adding relationships works and that they link the correct nodes.
#[test]
fn test_add_and_get_relationship() {
let mut db = GraphDatabase::new();
let properties = HashMap::new();
db.add_node(1, "User".to_string(), properties.clone());
db.add_node(2, "Post".to_string(), properties.clone());
db.add_relationship(1, "WROTE".to_string(), 1, 2, properties.clone());
let relationship = db.relationships.get(&1).unwrap();
assert_eq!(relationship.start_node, 1);
assert_eq!(relationship.end_node, 2);
assert_eq!(relationship.label, "WROTE");
}
3. Testing Node Querying by Property
This test ensures that we can fetch nodes based on property values.
#[test]
fn test_query_node_by_property() {
let mut db = GraphDatabase::new();
let mut properties = HashMap::new();
properties.insert("username".to_string(), "alice".to_string());
db.add_node(1, "User".to_string(), properties);
let nodes = db.get_nodes_by_property("username", "alice");
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].id, 1);
}



