Programming Guide¶
This guide walks you through building applications with Uni using the Rust or Python API. Select your preferred language - all code examples will switch to match your choice.
Prerequisites¶
Before starting, ensure you have Uni installed for your language:
Opening a Database¶
The first step is opening or creating a database. Uni uses a builder pattern for configuration.
use uni::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
// Open or create a database (creates if doesn't exist)
let db = Uni::open("./my-graph").build().await?;
// Open existing database (fails if doesn't exist)
let db = Uni::open_existing("./my-graph").build().await?;
// Create new database (fails if already exists)
let db = Uni::create("./my-graph").build().await?;
// In-memory database (for testing)
let db = Uni::in_memory().build().await?;
Ok(())
}
import uni
# Open or create a database (creates if doesn't exist)
db = uni.DatabaseBuilder.open("./my-graph").build()
# Open existing database (fails if doesn't exist)
db = uni.DatabaseBuilder.open_existing("./my-graph").build()
# Create new database (fails if already exists)
db = uni.DatabaseBuilder.create("./my-graph").build()
# Temporary in-memory database
db = uni.DatabaseBuilder.temporary().build()
# Simple shorthand (open or create)
db = uni.Database("./my-graph")
Configuration Options¶
Configure cache size, parallelism, and other options:
Defining Schema¶
Before inserting data, define your graph schema with vertex labels, edge types, and properties.
Using the Schema Builder¶
db.schema()
// Define Person vertex type
.label("Person")
.property("name", DataType::String)
.property("age", DataType::Int32)
.property_nullable("email", DataType::String)
.index("name", IndexType::Scalar(ScalarType::Hash))
// Define Company vertex type
.label("Company")
.property("name", DataType::String)
.property("founded", DataType::Int32)
// Define WORKS_AT edge type (Person -> Company)
.edge_type("WORKS_AT", &["Person"], &["Company"])
.property("since", DataType::Int32)
.property_nullable("role", DataType::String)
// Define KNOWS edge type (Person -> Person)
.edge_type("KNOWS", &["Person"], &["Person"])
.apply()
.await?;
schema = db.schema()
# Define Person vertex type
schema = (
schema.label("Person")
.property("name", "string")
.property("age", "int")
.property_nullable("email", "string")
.index("name", "hash")
.done()
)
# Define Company vertex type
schema = (
schema.label("Company")
.property("name", "string")
.property("founded", "int")
.done()
)
# Define edge types
schema = (
schema.edge_type("WORKS_AT", ["Person"], ["Company"])
.property("since", "int")
.property_nullable("role", "string")
.done()
)
schema = (
schema.edge_type("KNOWS", ["Person"], ["Person"])
.done()
)
# Apply all schema changes
schema.apply()
Quick Schema Methods¶
For simple cases, use the direct methods:
// Create label
db.create_label("Person").await?;
// Add property (label, name, type)
db.add_property("Person", "name", DataType::String, false).await?; // required
db.add_property("Person", "email", DataType::String, true).await?; // nullable
// Create index
db.create_scalar_index("Person", "name", ScalarType::Hash).await?;
Basic Queries¶
Execute Cypher queries to read and write data.
Creating Data¶
// Create a single vertex
db.execute("CREATE (p:Person {name: 'Alice', age: 30})").await?;
// Create multiple vertices and an edge
db.execute(r#"
CREATE (alice:Person {name: 'Alice', age: 30})
CREATE (bob:Person {name: 'Bob', age: 25})
CREATE (alice)-[:KNOWS {since: 2020}]->(bob)
"#).await?;
// Create edge between existing vertices
db.execute(r#"
MATCH (a:Person {name: 'Alice'}), (c:Company {name: 'TechCorp'})
CREATE (a)-[:WORKS_AT {since: 2022, role: 'Engineer'}]->(c)
"#).await?;
# Create a single vertex
db.execute("CREATE (p:Person {name: 'Alice', age: 30})")
# Create multiple vertices and an edge
db.execute("""
CREATE (alice:Person {name: 'Alice', age: 30})
CREATE (bob:Person {name: 'Bob', age: 25})
CREATE (alice)-[:KNOWS {since: 2020}]->(bob)
""")
# Create edge between existing vertices
db.execute("""
MATCH (a:Person {name: 'Alice'}), (c:Company {name: 'TechCorp'})
CREATE (a)-[:WORKS_AT {since: 2022, role: 'Engineer'}]->(c)
""")
Reading Data¶
// Simple query
let results = db.query("MATCH (p:Person) RETURN p.name, p.age").await?;
for row in &results {
let name: String = row.get("p.name")?;
let age: i32 = row.get("p.age")?;
println!("{} is {} years old", name, age);
}
// Query with filtering and ordering
let results = db.query(r#"
MATCH (p:Person)
WHERE p.age >= 25
RETURN p.name AS name, p.age AS age
ORDER BY p.age DESC
LIMIT 10
"#).await?;
for row in &results {
println!("{}: {}", row.get::<String>("name")?, row.get::<i32>("age")?);
}
# Simple query
results = db.query("MATCH (p:Person) RETURN p.name AS name, p.age AS age")
for row in results:
print(f"{row['name']} is {row['age']} years old")
# Query with filtering and ordering
results = db.query("""
MATCH (p:Person)
WHERE p.age >= 25
RETURN p.name AS name, p.age AS age
ORDER BY p.age DESC
LIMIT 10
""")
for row in results:
print(f"{row['name']}: {row['age']}")
Parameterized Queries¶
Always use parameters for user-provided values to prevent injection attacks:
// Single parameter
let results = db.query_with("MATCH (p:Person) WHERE p.name = $name RETURN p")
.param("name", "Alice")
.fetch_all()
.await?;
// Multiple parameters
let results = db.query_with(r#"
MATCH (p:Person)
WHERE p.age >= $min_age AND p.age <= $max_age
RETURN p.name AS name, p.age AS age
"#)
.param("min_age", 20)
.param("max_age", 40)
.fetch_all()
.await?;
// Parameters from HashMap
let params = hashmap! {
"name" => "Alice".into(),
"company" => "TechCorp".into(),
};
let results = db.query_with(
"MATCH (p:Person {name: $name})-[:WORKS_AT]->(c:Company {name: $company}) RETURN p, c"
)
.params(params)
.fetch_all()
.await?;
# Single parameter
results = (
db.query_with("MATCH (p:Person) WHERE p.name = $name RETURN p.name AS name")
.param("name", "Alice")
.fetch_all()
)
# Multiple parameters
results = (
db.query_with("""
MATCH (p:Person)
WHERE p.age >= $min_age AND p.age <= $max_age
RETURN p.name AS name, p.age AS age
""")
.param("min_age", 20)
.param("max_age", 40)
.fetch_all()
)
# Parameters from dict
params = {"name": "Alice", "company": "TechCorp"}
results = (
db.query_with("""
MATCH (p:Person {name: $name})-[:WORKS_AT]->(c:Company {name: $company})
RETURN p.name AS person, c.name AS company
""")
.params(params)
.fetch_all()
)
Graph Traversals¶
Traverse relationships to explore connected data.
Basic Traversal¶
// Find all people that Alice knows
let results = db.query(r#"
MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(friend:Person)
RETURN friend.name AS name
"#).await?;
// Find friends of friends
let results = db.query(r#"
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*2]->(fof:Person)
WHERE fof.name <> 'Alice'
RETURN DISTINCT fof.name AS name
"#).await?;
// Variable-length paths (1 to 3 hops)
let results = db.query(r#"
MATCH path = (alice:Person {name: 'Alice'})-[:KNOWS*1..3]->(other:Person)
RETURN other.name AS name, length(path) AS distance
"#).await?;
# Find all people that Alice knows
results = db.query("""
MATCH (alice:Person {name: 'Alice'})-[:KNOWS]->(friend:Person)
RETURN friend.name AS name
""")
# Find friends of friends
results = db.query("""
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*2]->(fof:Person)
WHERE fof.name <> 'Alice'
RETURN DISTINCT fof.name AS name
""")
# Variable-length paths (1 to 3 hops)
results = db.query("""
MATCH path = (alice:Person {name: 'Alice'})-[:KNOWS*1..3]->(other:Person)
RETURN other.name AS name, length(path) AS distance
""")
Aggregations¶
// Count friends per person
let results = db.query(r#"
MATCH (p:Person)-[:KNOWS]->(friend:Person)
RETURN p.name AS person, COUNT(friend) AS friend_count
ORDER BY friend_count DESC
"#).await?;
// Average age by company
let results = db.query(r#"
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name AS company, AVG(p.age) AS avg_age, COUNT(p) AS employees
"#).await?;
# Count friends per person
results = db.query("""
MATCH (p:Person)-[:KNOWS]->(friend:Person)
RETURN p.name AS person, COUNT(friend) AS friend_count
ORDER BY friend_count DESC
""")
# Average age by company
results = db.query("""
MATCH (p:Person)-[:WORKS_AT]->(c:Company)
RETURN c.name AS company, AVG(p.age) AS avg_age, COUNT(p) AS employees
""")
Transactions¶
Group multiple operations into atomic transactions.
Explicit Transactions¶
// Begin transaction
let tx = db.begin().await?;
// Execute operations
tx.execute("CREATE (p:Person {name: 'Carol', age: 28})").await?;
tx.execute("CREATE (p:Person {name: 'Dave', age: 32})").await?;
tx.execute(r#"
MATCH (c:Person {name: 'Carol'}), (d:Person {name: 'Dave'})
CREATE (c)-[:KNOWS]->(d)
"#).await?;
// Commit (or rollback on error)
tx.commit().await?;
# Begin transaction
tx = db.begin()
try:
# Execute operations
tx.query("CREATE (p:Person {name: 'Carol', age: 28})")
tx.query("CREATE (p:Person {name: 'Dave', age: 32})")
tx.query("""
MATCH (c:Person {name: 'Carol'}), (d:Person {name: 'Dave'})
CREATE (c)-[:KNOWS]->(d)
""")
# Commit
tx.commit()
except Exception as e:
# Rollback on error
tx.rollback()
raise e
Transaction Closure (Rust)¶
// Auto-commit on success, auto-rollback on error
db.transaction(|tx| async move {
tx.execute("CREATE (a:Person {name: 'Eve', age: 26})").await?;
tx.execute("CREATE (b:Person {name: 'Frank', age: 29})").await?;
tx.execute(r#"
MATCH (e:Person {name: 'Eve'}), (f:Person {name: 'Frank'})
CREATE (e)-[:KNOWS]->(f)
"#).await?;
Ok(())
}).await?;
Vector Search¶
Store and search vector embeddings for semantic similarity.
Setting Up Vector Properties¶
// Add vector property to schema
db.schema()
.label("Document")
.property("title", DataType::String)
.property("content", DataType::String)
.vector("embedding", 384) // 384 dimensions
.index("embedding", IndexType::Vector(VectorIndexCfg {
algorithm: VectorAlgo::Hnsw { m: 16, ef_construction: 200 },
metric: VectorMetric::Cosine,
}))
.apply()
.await?;
Inserting Vectors¶
// Insert document with embedding
let embedding: Vec<f32> = compute_embedding("Machine learning fundamentals");
db.query_with(r#"
CREATE (d:Document {
title: $title,
content: $content,
embedding: $embedding
})
"#)
.param("title", "ML Basics")
.param("content", "Machine learning fundamentals...")
.param("embedding", embedding)
.fetch_all()
.await?;
# Insert document with embedding
embedding = compute_embedding("Machine learning fundamentals")
db.query_with("""
CREATE (d:Document {
title: $title,
content: $content,
embedding: $embedding
})
""").param("title", "ML Basics") \
.param("content", "Machine learning fundamentals...") \
.param("embedding", embedding) \
.fetch_all()
Searching Vectors¶
// Simple vector search
let query_vec = compute_embedding("deep learning neural networks");
let matches = db.vector_search("Document", "embedding", query_vec.clone(), 10).await?;
for m in &matches {
println!("VID: {}, Distance: {:.4}", m.vid, m.distance);
}
// Vector search with builder (filters and options)
let results = db.vector_search_with("Document", "embedding", query_vec)
.k(20)
.threshold(0.5) // Only results with distance < 0.5
.filter("node.category = 'tutorial'")
.fetch_nodes()
.await?;
for (node, distance) in results {
println!("{} ({:.4})", node.get::<String>("title")?, distance);
}
// Vector search via Cypher
let results = db.query_with(r#"
CALL db.idx.vector.query('Document', 'embedding', $vec, 10)
YIELD node, distance
RETURN node.title AS title, distance
ORDER BY distance
"#)
.param("vec", query_vec)
.fetch_all()
.await?;
# Simple vector search
query_vec = compute_embedding("deep learning neural networks")
matches = db.vector_search("Document", "embedding", query_vec, k=10)
for m in matches:
print(f"VID: {m.vid}, Distance: {m.distance:.4f}")
# Vector search with builder (filters and options)
results = (
db.vector_search_with("Document", "embedding", query_vec)
.k(20)
.threshold(0.5) # Only results with distance < 0.5
.search()
)
for m in results:
print(f"VID: {m.vid}, Distance: {m.distance:.4f}")
# Vector search via Cypher
results = db.query_with("""
CALL db.idx.vector.query('Document', 'embedding', $vec, 10)
YIELD node, distance
RETURN node.title AS title, distance
ORDER BY distance
""").param("vec", query_vec).fetch_all()
for row in results:
print(f"{row['title']}: {row['distance']:.4f}")
Bulk Loading¶
For large datasets, use the bulk writer for efficient loading.
// Create bulk writer with deferred indexing
let mut bulk = db.bulk_writer()
.defer_vector_indexes(true)
.defer_scalar_indexes(true)
.batch_size(50_000)
.on_progress(|p| {
println!("{:?}: {} rows processed", p.phase, p.rows_processed);
})
.build()?;
// Insert vertices in bulk
let people: Vec<HashMap<String, Value>> = (0..100_000)
.map(|i| hashmap! {
"name" => format!("Person-{}", i).into(),
"age" => (20 + i % 50).into(),
})
.collect();
let vids = bulk.insert_vertices("Person", people).await?;
// Insert edges in bulk (src_vid, dst_vid, properties)
let edges: Vec<EdgeData> = vids.windows(2)
.map(|pair| EdgeData {
src: pair[0],
dst: pair[1],
properties: hashmap! { "since" => 2024.into() },
})
.collect();
bulk.insert_edges("KNOWS", edges).await?;
// Commit and rebuild indexes
let stats = bulk.commit().await?;
println!(
"Loaded {} vertices, {} edges in {:?}",
stats.vertices_inserted, stats.edges_inserted, stats.duration
);
# Create bulk writer with configuration
writer = (
db.bulk_writer()
.batch_size(50_000)
.build()
)
# Insert vertices in bulk
people = [
{"name": f"Person-{i}", "age": 20 + i % 50}
for i in range(100_000)
]
vids = writer.insert_vertices("Person", people)
# Insert edges in bulk (src_vid, dst_vid, properties)
edges = [
(vids[i], vids[i + 1], {"since": 2024})
for i in range(len(vids) - 1)
]
writer.insert_edges("KNOWS", edges)
# Commit and rebuild indexes
stats = writer.commit()
print(f"Loaded {stats.vertices_inserted} vertices, {stats.edges_inserted} edges")
Sessions¶
Sessions provide scoped context for multi-tenant queries.
// Create session with tenant context
let session = db.session()
.set("tenant_id", "acme-corp")
.set("user_id", "user-123")
.build();
// All queries have access to $session.* variables
let results = session.query(r#"
MATCH (d:Document)
WHERE d.tenant_id = $session.tenant_id
RETURN d.title AS title
"#).await?;
// Query with additional parameters
let results = session.query_with(r#"
MATCH (d:Document)
WHERE d.tenant_id = $session.tenant_id
AND d.status = $status
RETURN d.title AS title
"#)
.param("status", "published")
.fetch_all()
.await?;
// Read session variable
let tenant: &str = session.get("tenant_id").unwrap().as_str().unwrap();
# Create session with tenant context
session = (
db.session()
.set("tenant_id", "acme-corp")
.set("user_id", "user-123")
.build()
)
# Execute queries with session context
results = session.query("""
MATCH (d:Document)
WHERE d.tenant_id = $session.tenant_id
RETURN d.title AS title
""")
# Read session variable
tenant = session.get("tenant_id")
print(f"Tenant: {tenant}")
EXPLAIN and PROFILE¶
Analyze query execution plans.
// Get query plan without executing
let plan = db.explain("MATCH (p:Person) WHERE p.age > 25 RETURN p.name").await?;
println!("Plan:\n{}", plan.plan_text);
println!("Estimated cost: {}", plan.estimated_cost);
// Execute with profiling
let (results, profile) = db.profile("MATCH (p:Person) WHERE p.age > 25 RETURN p.name").await?;
println!("Total time: {}ms", profile.total_time_ms);
println!("Rows scanned: {}", profile.rows_scanned);
println!("Peak memory: {} bytes", profile.peak_memory_bytes);
# Get query plan without executing
plan = db.explain("MATCH (p:Person) WHERE p.age > 25 RETURN p.name AS name")
print(f"Plan:\n{plan['plan_text']}")
print(f"Estimated cost: {plan['cost_estimates']}")
# Execute with profiling
results, profile = db.profile("MATCH (p:Person) WHERE p.age > 25 RETURN p.name AS name")
print(f"Total time: {profile['total_time_ms']}ms")
print(f"Peak memory: {profile['peak_memory_bytes']} bytes")
Snapshots¶
Access historical database states.
// Create a named snapshot
let snapshot_id = db.create_snapshot(Some("before_migration")).await?;
// List all snapshots
for snap in db.list_snapshots().await? {
println!("{}: {} ({})",
snap.id,
snap.name.as_deref().unwrap_or("-"),
snap.created_at
);
}
// Open read-only view at snapshot
let historical = db.at_snapshot(&snapshot_id).await?;
let old_count = historical.query("MATCH (n) RETURN count(n) AS c").await?;
println!("Count at snapshot: {}", old_count[0].get::<i64>("c")?);
// Restore to snapshot
db.restore_snapshot(&snapshot_id).await?;
# Create a named snapshot
snapshot_id = db.create_snapshot("before_migration")
# List all snapshots
for snap in db.list_snapshots():
print(f"{snap.snapshot_id}: {snap.name or '-'} ({snap.vertex_count} vertices)")
# Open read-only view at snapshot
historical = db.at_snapshot(snapshot_id)
old_results = historical.query("MATCH (n) RETURN count(n) AS c")
print(f"Count at snapshot: {old_results[0]['c']}")
# Restore to snapshot
db.restore_snapshot(snapshot_id)
Graph Algorithms¶
Run built-in graph algorithms.
PageRank¶
// Using the algorithm builder
let rankings = db.algo()
.pagerank()
.labels(&["Person"])
.edge_types(&["KNOWS"])
.damping(0.85)
.max_iterations(50)
.run()
.await?;
for (vid, score) in rankings.iter().take(10) {
println!("VID: {}, Score: {:.6}", vid, score);
}
// Via Cypher
let results = db.query(r#"
CALL algo.pageRank(['Person'], ['KNOWS'])
YIELD nodeId, score
RETURN nodeId, score
ORDER BY score DESC
LIMIT 10
"#).await?;
Weakly Connected Components¶
// Find connected components
let components = db.algo()
.wcc()
.labels(&["Person"])
.edge_types(&["KNOWS"])
.run()
.await?;
// Count component sizes
let mut sizes: HashMap<i64, usize> = HashMap::new();
for (_, component_id) in &components {
*sizes.entry(*component_id).or_default() += 1;
}
println!("Found {} components", sizes.len());
Community Detection¶
Error Handling¶
Handle errors appropriately in your application.
use uni::prelude::*;
match db.query("INVALID CYPHER").await {
Ok(results) => {
// Process results
}
Err(UniError::Parse { message, line, column, .. }) => {
eprintln!(
"Syntax error at {}:{}: {}",
line.unwrap_or(0),
column.unwrap_or(0),
message
);
}
Err(UniError::Query { message, .. }) => {
eprintln!("Query execution error: {}", message);
}
Err(UniError::Timeout { timeout_ms }) => {
eprintln!("Query timed out after {}ms", timeout_ms);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}
Complete Example: Social Network¶
Here's a complete example building a simple social network application.
use uni::prelude::*;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<()> {
// Create database
let db = Uni::open("./social-network")
.cache_size(512 * 1024 * 1024)
.build()
.await?;
// Define schema
db.schema()
.label("User")
.property("username", DataType::String)
.property("email", DataType::String)
.property("joined", DataType::Int32)
.index("username", IndexType::Scalar(ScalarType::Hash))
.label("Post")
.property("content", DataType::String)
.property("timestamp", DataType::Int64)
.edge_type("FOLLOWS", &["User"], &["User"])
.edge_type("POSTED", &["User"], &["Post"])
.edge_type("LIKES", &["User"], &["Post"])
.apply()
.await?;
// Create users
db.transaction(|tx| async move {
tx.execute("CREATE (u:User {username: 'alice', email: 'alice@example.com', joined: 2023})").await?;
tx.execute("CREATE (u:User {username: 'bob', email: 'bob@example.com', joined: 2023})").await?;
tx.execute("CREATE (u:User {username: 'carol', email: 'carol@example.com', joined: 2024})").await?;
// Create follow relationships
tx.execute(r#"
MATCH (a:User {username: 'alice'}), (b:User {username: 'bob'})
CREATE (a)-[:FOLLOWS]->(b)
"#).await?;
tx.execute(r#"
MATCH (b:User {username: 'bob'}), (c:User {username: 'carol'})
CREATE (b)-[:FOLLOWS]->(c)
"#).await?;
Ok(())
}).await?;
// Create posts
db.execute(r#"
MATCH (a:User {username: 'alice'})
CREATE (a)-[:POSTED]->(p:Post {content: 'Hello world!', timestamp: 1704067200000})
"#).await?;
// Query: Get user's feed (posts from people they follow)
let feed = db.query_with(r#"
MATCH (me:User {username: $username})-[:FOLLOWS]->(friend:User)-[:POSTED]->(post:Post)
RETURN friend.username AS author, post.content AS content, post.timestamp AS ts
ORDER BY post.timestamp DESC
LIMIT 20
"#)
.param("username", "alice")
.fetch_all()
.await?;
println!("Alice's Feed:");
for row in &feed {
println!(" @{}: {}",
row.get::<String>("author")?,
row.get::<String>("content")?
);
}
// Query: Suggest friends (friends of friends not already following)
let suggestions = db.query_with(r#"
MATCH (me:User {username: $username})-[:FOLLOWS*2]->(suggestion:User)
WHERE NOT (me)-[:FOLLOWS]->(suggestion)
AND suggestion.username <> $username
RETURN DISTINCT suggestion.username AS username, COUNT(*) AS mutual
ORDER BY mutual DESC
LIMIT 5
"#)
.param("username", "alice")
.fetch_all()
.await?;
println!("\nFriend Suggestions:");
for row in &suggestions {
println!(" @{} ({} mutual)",
row.get::<String>("username")?,
row.get::<i64>("mutual")?
);
}
// Run PageRank to find influential users
let influential = db.query(r#"
CALL algo.pageRank(['User'], ['FOLLOWS'])
YIELD nodeId, score
MATCH (u:User) WHERE id(u) = nodeId
RETURN u.username AS username, score
ORDER BY score DESC
LIMIT 5
"#).await?;
println!("\nMost Influential Users:");
for row in &influential {
println!(" @{}: {:.4}",
row.get::<String>("username")?,
row.get::<f64>("score")?
);
}
Ok(())
}
import uni
def main():
# Create database
db = uni.DatabaseBuilder.open("./social-network") \
.cache_size(512 * 1024 * 1024) \
.build()
# Define schema
db.create_label("User")
db.add_property("User", "username", "string", False)
db.add_property("User", "email", "string", False)
db.add_property("User", "joined", "int", False)
db.create_scalar_index("User", "username", "hash")
db.create_label("Post")
db.add_property("Post", "content", "string", False)
db.add_property("Post", "timestamp", "int", False)
db.create_edge_type("FOLLOWS", ["User"], ["User"])
db.create_edge_type("POSTED", ["User"], ["Post"])
db.create_edge_type("LIKES", ["User"], ["Post"])
# Create users in transaction
tx = db.begin()
try:
tx.query("CREATE (u:User {username: 'alice', email: 'alice@example.com', joined: 2023})")
tx.query("CREATE (u:User {username: 'bob', email: 'bob@example.com', joined: 2023})")
tx.query("CREATE (u:User {username: 'carol', email: 'carol@example.com', joined: 2024})")
# Create follow relationships
tx.query("""
MATCH (a:User {username: 'alice'}), (b:User {username: 'bob'})
CREATE (a)-[:FOLLOWS]->(b)
""")
tx.query("""
MATCH (b:User {username: 'bob'}), (c:User {username: 'carol'})
CREATE (b)-[:FOLLOWS]->(c)
""")
tx.commit()
except Exception as e:
tx.rollback()
raise e
# Create posts
db.execute("""
MATCH (a:User {username: 'alice'})
CREATE (a)-[:POSTED]->(p:Post {content: 'Hello world!', timestamp: 1704067200000})
""")
# Query: Get user's feed
feed = db.query_with("""
MATCH (me:User {username: $username})-[:FOLLOWS]->(friend:User)-[:POSTED]->(post:Post)
RETURN friend.username AS author, post.content AS content, post.timestamp AS ts
ORDER BY post.timestamp DESC
LIMIT 20
""").param("username", "alice").fetch_all()
print("Alice's Feed:")
for row in feed:
print(f" @{row['author']}: {row['content']}")
# Query: Suggest friends
suggestions = db.query_with("""
MATCH (me:User {username: $username})-[:FOLLOWS*2]->(suggestion:User)
WHERE NOT (me)-[:FOLLOWS]->(suggestion)
AND suggestion.username <> $username
RETURN DISTINCT suggestion.username AS username, COUNT(*) AS mutual
ORDER BY mutual DESC
LIMIT 5
""").param("username", "alice").fetch_all()
print("\nFriend Suggestions:")
for row in suggestions:
print(f" @{row['username']} ({row['mutual']} mutual)")
# Run PageRank
influential = db.query("""
CALL algo.pageRank(['User'], ['FOLLOWS'])
YIELD nodeId, score
MATCH (u:User) WHERE id(u) = nodeId
RETURN u.username AS username, score
ORDER BY score DESC
LIMIT 5
""")
print("\nMost Influential Users:")
for row in influential:
print(f" @{row['username']}: {row['score']:.4f}")
if __name__ == "__main__":
main()
Next Steps¶
You now have the foundation to build applications with Uni. Continue learning:
| Topic | Description |
|---|---|
| Cypher Querying | Complete query language reference |
| Vector Search | Semantic search patterns |
| Schema Design | Best practices for graph modeling |
| Performance Tuning | Optimization techniques |
| Rust API Reference | Complete Rust API docs |
| Python API Reference | Complete Python API docs |