Supply Chain Management with Uni (Rust)¶
This notebook demonstrates how to model a supply chain graph to perform BOM (Bill of Materials) explosion and cost rollup using Uni's native Rust API.
In [ ]:
Copied!
:dep uni-db = { path = "../../../crates/uni" }
:dep tokio = { version = "1", features = ["full"] }
:dep serde_json = "1"
:dep uni-db = { path = "../../../crates/uni" }
:dep tokio = { version = "1", features = ["full"] }
:dep serde_json = "1"
In [ ]:
Copied!
use uni::{Uni, DataType, IndexType, ScalarType, VectorMetric, VectorAlgo, VectorIndexCfg};
use std::collections::HashMap;
use serde_json::json;
// Helper macro to run async code in evcxr
macro_rules! run {
($e:expr) => {
tokio::runtime::Runtime::new().unwrap().block_on($e)
};
}
use uni::{Uni, DataType, IndexType, ScalarType, VectorMetric, VectorAlgo, VectorIndexCfg};
use std::collections::HashMap;
use serde_json::json;
// Helper macro to run async code in evcxr
macro_rules! run {
($e:expr) => {
tokio::runtime::Runtime::new().unwrap().block_on($e)
};
}
In [ ]:
Copied!
let db_path = "./supply_chain_db";
// Clean up any existing database
if std::path::Path::new(db_path).exists() {
std::fs::remove_dir_all(db_path).unwrap();
}
let db = run!(Uni::open(db_path).build()).unwrap();
println!("Opened database at {}", db_path);
let db_path = "./supply_chain_db";
// Clean up any existing database
if std::path::Path::new(db_path).exists() {
std::fs::remove_dir_all(db_path).unwrap();
}
let db = run!(Uni::open(db_path).build()).unwrap();
println!("Opened database at {}", db_path);
1. Define Schema¶
We define Parts, Suppliers, and Products, along with relationships for Assembly and Supply.
In [ ]:
Copied!
run!(async {
db.schema()
.label("Part")
.property("sku", DataType::String)
.property("cost", DataType::Float64)
.index("sku", IndexType::Scalar(ScalarType::BTree))
.label("Supplier")
.label("Product")
.property("name", DataType::String)
.property("price", DataType::Float64)
.edge_type("ASSEMBLED_FROM", &["Product", "Part"], &["Part"])
.edge_type("SUPPLIED_BY", &["Part"], &["Supplier"])
.apply()
.await
}).unwrap();
println!("Schema created successfully");
run!(async {
db.schema()
.label("Part")
.property("sku", DataType::String)
.property("cost", DataType::Float64)
.index("sku", IndexType::Scalar(ScalarType::BTree))
.label("Supplier")
.label("Product")
.property("name", DataType::String)
.property("price", DataType::Float64)
.edge_type("ASSEMBLED_FROM", &["Product", "Part"], &["Part"])
.edge_type("SUPPLIED_BY", &["Part"], &["Supplier"])
.apply()
.await
}).unwrap();
println!("Schema created successfully");
2. Ingest Data¶
We insert parts and products using bulk insertion for performance.
In [ ]:
Copied!
// Insert Parts
let part_props = vec![
HashMap::from([
("sku".to_string(), json!("RES-10K")),
("cost".to_string(), json!(0.05)),
]),
HashMap::from([
("sku".to_string(), json!("MB-X1")),
("cost".to_string(), json!(50.0)),
]),
HashMap::from([
("sku".to_string(), json!("SCR-OLED")),
("cost".to_string(), json!(30.0)),
]),
];
let part_vids = run!(db.bulk_insert_vertices("Part", part_props)).unwrap();
let (p1, p2, p3) = (part_vids[0], part_vids[1], part_vids[2]);
println!("Inserted parts: {:?}", part_vids);
// Insert Product
let prod_props = vec![
HashMap::from([
("name".to_string(), json!("Smartphone X")),
("price".to_string(), json!(500.0)),
]),
];
let phone_vids = run!(db.bulk_insert_vertices("Product", prod_props)).unwrap();
let phone = phone_vids[0];
println!("Inserted product: {:?}", phone);
// Create assembly relationships
// Phone is assembled from MB-X1 and SCR-OLED
// MB-X1 is assembled from RES-10K
run!(db.bulk_insert_edges("ASSEMBLED_FROM", vec![
(phone, p2, HashMap::new()), // phone <- MB-X1
(phone, p3, HashMap::new()), // phone <- SCR-OLED
(p2, p1, HashMap::new()), // MB-X1 <- RES-10K
])).unwrap();
run!(db.flush()).unwrap();
println!("Data ingested and flushed");
// Insert Parts
let part_props = vec![
HashMap::from([
("sku".to_string(), json!("RES-10K")),
("cost".to_string(), json!(0.05)),
]),
HashMap::from([
("sku".to_string(), json!("MB-X1")),
("cost".to_string(), json!(50.0)),
]),
HashMap::from([
("sku".to_string(), json!("SCR-OLED")),
("cost".to_string(), json!(30.0)),
]),
];
let part_vids = run!(db.bulk_insert_vertices("Part", part_props)).unwrap();
let (p1, p2, p3) = (part_vids[0], part_vids[1], part_vids[2]);
println!("Inserted parts: {:?}", part_vids);
// Insert Product
let prod_props = vec![
HashMap::from([
("name".to_string(), json!("Smartphone X")),
("price".to_string(), json!(500.0)),
]),
];
let phone_vids = run!(db.bulk_insert_vertices("Product", prod_props)).unwrap();
let phone = phone_vids[0];
println!("Inserted product: {:?}", phone);
// Create assembly relationships
// Phone is assembled from MB-X1 and SCR-OLED
// MB-X1 is assembled from RES-10K
run!(db.bulk_insert_edges("ASSEMBLED_FROM", vec![
(phone, p2, HashMap::new()), // phone <- MB-X1
(phone, p3, HashMap::new()), // phone <- SCR-OLED
(p2, p1, HashMap::new()), // MB-X1 <- RES-10K
])).unwrap();
run!(db.flush()).unwrap();
println!("Data ingested and flushed");
3. BOM Explosion¶
Find all products that contain a specific defective part (RES-10K), traversing up the assembly hierarchy.
In [ ]:
Copied!
// First, warm up adjacency cache
run!(db.query("MATCH (a:Part)-[:ASSEMBLED_FROM]->(b:Part) RETURN a.sku")).unwrap();
// BOM explosion query - find products affected by defective part
let query = r#"
MATCH (defective:Part {sku: 'RES-10K'})
MATCH (product:Product)-[:ASSEMBLED_FROM*1..5]->(defective)
RETURN product.name as name, product.price as price
"#;
let results = run!(db.query(query)).unwrap();
println!("Products affected by defective part:");
for row in results.rows {
println!(" {:?}", row);
}
// First, warm up adjacency cache
run!(db.query("MATCH (a:Part)-[:ASSEMBLED_FROM]->(b:Part) RETURN a.sku")).unwrap();
// BOM explosion query - find products affected by defective part
let query = r#"
MATCH (defective:Part {sku: 'RES-10K'})
MATCH (product:Product)-[:ASSEMBLED_FROM*1..5]->(defective)
RETURN product.name as name, product.price as price
"#;
let results = run!(db.query(query)).unwrap();
println!("Products affected by defective part:");
for row in results.rows {
println!(" {:?}", row);
}
4. Cost Rollup¶
Calculate the total cost of parts for a product by traversing down the assembly tree.
In [ ]:
Copied!
let query_cost = r#"
MATCH (p:Product {name: 'Smartphone X'})
MATCH (p)-[:ASSEMBLED_FROM*1..5]->(part:Part)
RETURN SUM(part.cost) AS total_bom_cost
"#;
let results = run!(db.query(query_cost)).unwrap();
println!("Total BOM Cost: {:?}", results.rows[0]);
let query_cost = r#"
MATCH (p:Product {name: 'Smartphone X'})
MATCH (p)-[:ASSEMBLED_FROM*1..5]->(part:Part)
RETURN SUM(part.cost) AS total_bom_cost
"#;
let results = run!(db.query(query_cost)).unwrap();
println!("Total BOM Cost: {:?}", results.rows[0]);