Fraud Detection with Uni (Rust)¶
Detecting money laundering rings (3-cycles) and shared device anomalies 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 = "./fraud_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 = "./fraud_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. Schema¶
In [ ]:
Copied!
run!(async {
db.schema()
.label("User")
.property("name", DataType::String)
.property("email", DataType::String)
.property_nullable("risk_score", DataType::Float32)
.label("Device")
.property("device_id", DataType::String)
.edge_type("SENT_MONEY", &["User"], &["User"])
.property("amount", DataType::Float64)
.edge_type("USED_DEVICE", &["User"], &["Device"])
.apply()
.await
}).unwrap();
println!("Fraud detection schema created");
run!(async {
db.schema()
.label("User")
.property("name", DataType::String)
.property("email", DataType::String)
.property_nullable("risk_score", DataType::Float32)
.label("Device")
.property("device_id", DataType::String)
.edge_type("SENT_MONEY", &["User"], &["User"])
.property("amount", DataType::Float64)
.edge_type("USED_DEVICE", &["User"], &["Device"])
.apply()
.await
}).unwrap();
println!("Fraud detection schema created");
2. Ingestion¶
5 named users, 3 devices, a money ring, and suspicious cross-device links.
In [ ]:
Copied!
// 5 users: 3 in a ring, 2 high-risk fraudsters
let users = vec![
HashMap::from([("name".to_string(), json!("Alice")), ("email".to_string(), json!("alice@example.com")), ("risk_score".to_string(), json!(0.10))]),
HashMap::from([("name".to_string(), json!("Bob")), ("email".to_string(), json!("bob@example.com")), ("risk_score".to_string(), json!(0.15))]),
HashMap::from([("name".to_string(), json!("Carlos")), ("email".to_string(), json!("carlos@example.com")), ("risk_score".to_string(), json!(0.20))]),
HashMap::from([("name".to_string(), json!("Dana")), ("email".to_string(), json!("dana@example.com")), ("risk_score".to_string(), json!(0.92))]),
HashMap::from([("name".to_string(), json!("Eve")), ("email".to_string(), json!("eve@example.com")), ("risk_score".to_string(), json!(0.88))]),
];
let user_vids = run!(db.bulk_insert_vertices("User", users)).unwrap();
let (alice, bob, carlos, dana, eve) =
(user_vids[0], user_vids[1], user_vids[2], user_vids[3], user_vids[4]);
// 3 devices
let devices = vec![
HashMap::from([("device_id".to_string(), json!("device_A"))]),
HashMap::from([("device_id".to_string(), json!("device_B"))]),
HashMap::from([("device_id".to_string(), json!("device_C"))]),
];
let device_vids = run!(db.bulk_insert_vertices("Device", devices)).unwrap();
let (device_a, device_b, device_c) = (device_vids[0], device_vids[1], device_vids[2]);
// Money ring: Alice -> Bob -> Carlos -> Alice
run!(db.bulk_insert_edges("SENT_MONEY", vec![
(alice, bob, HashMap::from([("amount".to_string(), json!(9500.0))])),
(bob, carlos, HashMap::from([("amount".to_string(), json!(9000.0))])),
(carlos, alice, HashMap::from([("amount".to_string(), json!(8750.0))])),
(dana, eve, HashMap::from([("amount".to_string(), json!(15000.0))])), // Suspicious
])).unwrap();
// Device sharing: Alice+Dana on device_A, Bob+Eve on device_B, Carlos alone on device_C
run!(db.bulk_insert_edges("USED_DEVICE", vec![
(alice, device_a, HashMap::new()),
(dana, device_a, HashMap::new()),
(bob, device_b, HashMap::new()),
(eve, device_b, HashMap::new()),
(carlos, device_c, HashMap::new()),
])).unwrap();
run!(db.flush()).unwrap();
println!("Fraud data ingested");
// 5 users: 3 in a ring, 2 high-risk fraudsters
let users = vec![
HashMap::from([("name".to_string(), json!("Alice")), ("email".to_string(), json!("alice@example.com")), ("risk_score".to_string(), json!(0.10))]),
HashMap::from([("name".to_string(), json!("Bob")), ("email".to_string(), json!("bob@example.com")), ("risk_score".to_string(), json!(0.15))]),
HashMap::from([("name".to_string(), json!("Carlos")), ("email".to_string(), json!("carlos@example.com")), ("risk_score".to_string(), json!(0.20))]),
HashMap::from([("name".to_string(), json!("Dana")), ("email".to_string(), json!("dana@example.com")), ("risk_score".to_string(), json!(0.92))]),
HashMap::from([("name".to_string(), json!("Eve")), ("email".to_string(), json!("eve@example.com")), ("risk_score".to_string(), json!(0.88))]),
];
let user_vids = run!(db.bulk_insert_vertices("User", users)).unwrap();
let (alice, bob, carlos, dana, eve) =
(user_vids[0], user_vids[1], user_vids[2], user_vids[3], user_vids[4]);
// 3 devices
let devices = vec![
HashMap::from([("device_id".to_string(), json!("device_A"))]),
HashMap::from([("device_id".to_string(), json!("device_B"))]),
HashMap::from([("device_id".to_string(), json!("device_C"))]),
];
let device_vids = run!(db.bulk_insert_vertices("Device", devices)).unwrap();
let (device_a, device_b, device_c) = (device_vids[0], device_vids[1], device_vids[2]);
// Money ring: Alice -> Bob -> Carlos -> Alice
run!(db.bulk_insert_edges("SENT_MONEY", vec![
(alice, bob, HashMap::from([("amount".to_string(), json!(9500.0))])),
(bob, carlos, HashMap::from([("amount".to_string(), json!(9000.0))])),
(carlos, alice, HashMap::from([("amount".to_string(), json!(8750.0))])),
(dana, eve, HashMap::from([("amount".to_string(), json!(15000.0))])), // Suspicious
])).unwrap();
// Device sharing: Alice+Dana on device_A, Bob+Eve on device_B, Carlos alone on device_C
run!(db.bulk_insert_edges("USED_DEVICE", vec![
(alice, device_a, HashMap::new()),
(dana, device_a, HashMap::new()),
(bob, device_b, HashMap::new()),
(eve, device_b, HashMap::new()),
(carlos, device_c, HashMap::new()),
])).unwrap();
run!(db.flush()).unwrap();
println!("Fraud data ingested");
3. Ring Detection¶
Find 3-cycles in the money transfer graph. Deduplication: a._vid < b._vid AND a._vid < c._vid.
In [ ]:
Copied!
let query_ring = r#"
MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
COUNT(*) AS rings
"#;
let results = run!(db.query(query_ring)).unwrap();
println!("Money laundering rings detected:");
for row in &results.rows {
println!(" {:?}", row);
}
assert!(results.rows.len() == 1, "Expected 1 ring, got {}", results.rows.len());
let query_ring = r#"
MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
COUNT(*) AS rings
"#;
let results = run!(db.query(query_ring)).unwrap();
println!("Money laundering rings detected:");
for row in &results.rows {
println!(" {:?}", row);
}
assert!(results.rows.len() == 1, "Expected 1 ring, got {}", results.rows.len());
4. Ring with Transfer Amounts¶
Same pattern, but also retrieve edge properties to show total cycled money.
In [ ]:
Copied!
let query_amounts = r#"
MATCH (a:User)-[r1:SENT_MONEY]->(b:User)-[r2:SENT_MONEY]->(c:User)-[r3:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
r1.amount AS leg1, r2.amount AS leg2, r3.amount AS leg3,
r1.amount + r2.amount + r3.amount AS total_cycled
"#;
let results = run!(db.query(query_amounts)).unwrap();
println!("Ring with transfer amounts:");
for row in &results.rows {
println!(" {:?}", row);
}
let query_amounts = r#"
MATCH (a:User)-[r1:SENT_MONEY]->(b:User)-[r2:SENT_MONEY]->(c:User)-[r3:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
RETURN a.name AS user_a, b.name AS user_b, c.name AS user_c,
r1.amount AS leg1, r2.amount AS leg2, r3.amount AS leg3,
r1.amount + r2.amount + r3.amount AS total_cycled
"#;
let results = run!(db.query(query_amounts)).unwrap();
println!("Ring with transfer amounts:");
for row in &results.rows {
println!(" {:?}", row);
}
5. Shared Device Risk¶
Find users who share a device with a high-risk user (risk > 0.8). Carlos should NOT appear.
In [ ]:
Copied!
let query_shared = r#"
MATCH (u:User)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
WHERE fraudster.risk_score > 0.8 AND u._vid <> fraudster._vid
RETURN u.name AS user, d.device_id AS device, fraudster.name AS flagged_contact
ORDER BY user
"#;
let results = run!(db.query(query_shared)).unwrap();
println!("Users sharing device with high-risk account:");
for row in &results.rows {
println!(" {:?}", row);
}
// Carlos should not appear - he only uses device_C alone
println!("Note: Carlos uses device_C alone and should not appear above");
let query_shared = r#"
MATCH (u:User)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
WHERE fraudster.risk_score > 0.8 AND u._vid <> fraudster._vid
RETURN u.name AS user, d.device_id AS device, fraudster.name AS flagged_contact
ORDER BY user
"#;
let results = run!(db.query(query_shared)).unwrap();
println!("Users sharing device with high-risk account:");
for row in &results.rows {
println!(" {:?}", row);
}
// Carlos should not appear - he only uses device_C alone
println!("Note: Carlos uses device_C alone and should not appear above");
6. Combined Alert: Ring + Device Sharing¶
Users appearing in BOTH a money ring AND sharing a device with a fraudster.
In [ ]:
Copied!
let query_combined = r#"
MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
MATCH (a)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
WHERE fraudster.risk_score > 0.8
RETURN DISTINCT a.name AS high_priority_user
"#;
let results = run!(db.query(query_combined)).unwrap();
println!("HIGH PRIORITY targets (ring + device-sharing):");
for row in &results.rows {
println!(" {:?}", row);
}
// Alice is in the ring AND shares device_A with Dana (high risk)
assert!(!results.rows.is_empty(), "Expected at least one combined alert");
let query_combined = r#"
MATCH (a:User)-[:SENT_MONEY]->(b:User)-[:SENT_MONEY]->(c:User)-[:SENT_MONEY]->(a)
WHERE a._vid < b._vid AND a._vid < c._vid
MATCH (a)-[:USED_DEVICE]->(d:Device)<-[:USED_DEVICE]-(fraudster:User)
WHERE fraudster.risk_score > 0.8
RETURN DISTINCT a.name AS high_priority_user
"#;
let results = run!(db.query(query_combined)).unwrap();
println!("HIGH PRIORITY targets (ring + device-sharing):");
for row in &results.rows {
println!(" {:?}", row);
}
// Alice is in the ring AND shares device_A with Dana (high risk)
assert!(!results.rows.is_empty(), "Expected at least one combined alert");