OGM (Object Graph Mapper)
Work with vertices and edges using convenient OGM helpers for graph database operations.
What is OGM?
OGM (Object Graph Mapper) is a pattern that allows you to work with graph databases using object-oriented concepts. Instead of thinking in terms of tables and rows, you work with vertices (nodes) and edges (relationships). arango-typed provides a powerful OGM layer on top of ArangoDB's native graph capabilities.
OGM helps you:
- Model Relationships: Easily define and manage relationships between entities
- Traverse Graphs: Navigate through connected data efficiently
- Find Paths: Discover connections and paths between entities
- Graph Algorithms: Perform complex graph operations like shortest path, centrality, etc.
- Type Safety: TypeScript support for graph operations
- Simplified API: High-level methods that abstract away complex AQL queries
Graph Concepts
Before diving into OGM, it's important to understand key graph concepts:
- Vertex (Node): An entity in the graph (e.g., a User, Post, or Product)
- Edge (Relationship): A connection between two vertices (e.g., "follows", "likes", "purchased")
- Graph: A collection of vertices and edges
- Direction: Edges can be directed (one-way) or undirected (bidirectional)
- Outbound: Edges going out from a vertex
- Inbound: Edges coming into a vertex
- Any: Both inbound and outbound edges
- Path: A sequence of vertices connected by edges
- Traversal: The process of visiting vertices by following edges
Creating a Graph
First, you need to create a graph in ArangoDB. A graph consists of vertex collections and edge collections.
Using ArangoDB Web Interface
You can create graphs using the ArangoDB web interface or programmatically:
Programmatically Creating a Graph
import { getDatabase } from 'arango-typed';
const db = getDatabase();
// Create a graph with vertex and edge collections
await db.createGraph('social', {
edgeDefinitions: [
{
collection: 'friends',
from: ['users'],
to: ['users']
},
{
collection: 'follows',
from: ['users'],
to: ['users']
}
]
});
Creating a GraphModel
A GraphModel extends the regular Model class with graph-specific methods. It provides an OGM interface for working with vertices and their relationships.
Basic GraphModel
import { getDatabase, graphModel, Schema } from 'arango-typed';
const db = getDatabase();
// Define schema for vertices
const UserSchema = new Schema({
name: String,
email: String,
age: Number
});
// Create GraphModel
const UserGraph = graphModel(db, 'social', 'users', UserSchema);
GraphModel Parameters
- database: The ArangoDB database instance
- graphName: Name of the graph (e.g., 'social')
- collectionName: Name of the vertex collection (e.g., 'users')
- schema: Schema definition for vertices
Working with Vertices
GraphModel extends Model, so you can use all regular model methods to work with vertices:
Creating Vertices
// Create a user vertex
const alice = await UserGraph.create({
name: 'Alice',
email: 'alice@example.com',
age: 30
});
const bob = await UserGraph.create({
name: 'Bob',
email: 'bob@example.com',
age: 25
});
const charlie = await UserGraph.create({
name: 'Charlie',
email: 'charlie@example.com',
age: 35
});
Finding Vertices
// Find by ID
const user = await UserGraph.findById('users/123');
// Find by query
const users = await UserGraph.find({ age: { $gte: 30 } }).all();
// Find one
const user = await UserGraph.findOne({ email: 'alice@example.com' });
Working with Edges (Relationships)
Edges represent relationships between vertices. You can create, query, and delete edges.
Creating Relationships
Use createRelationship to create an edge between two vertices:
// Alice follows Bob
await UserGraph.createRelationship(
'users/alice', // From vertex ID
'users/bob', // To vertex ID
'follows', // Edge collection name
{ // Edge data (optional)
since: new Date(),
status: 'active'
}
);
// Bob follows Alice (bidirectional relationship)
await UserGraph.createRelationship(
'users/bob',
'users/alice',
'follows',
{ since: new Date() }
);
// Alice and Bob are friends
await UserGraph.createRelationship(
'users/alice',
'users/bob',
'friends',
{ since: new Date('2020-01-01') }
);
Deleting Relationships
// Delete a specific relationship
await UserGraph.deleteRelationship('follows', {
_from: 'users/alice',
_to: 'users/bob'
});
// Delete all relationships from a vertex
await UserGraph.deleteRelationship('follows', {
_from: 'users/alice'
});
Traversing Relationships
Traversal is the process of visiting vertices by following edges. arango-typed provides several methods for traversing graphs.
Get Outbound Relationships
Get all vertices that a vertex points to (outgoing edges):
// Get all users that Alice follows
const following = await UserGraph.getOutbound('users/alice', 'follows');
following.forEach(user => {
console.log(`${user.name} is followed by Alice`);
});
// With filtering
const activeFollowing = await UserGraph.getOutbound('users/alice', 'follows', {
filter: { status: 'active' },
limit: 10
});
Get Inbound Relationships
Get all vertices that point to a vertex (incoming edges):
// Get all users that follow Alice
const followers = await UserGraph.getInbound('users/alice', 'follows');
followers.forEach(user => {
console.log(`${user.name} follows Alice`);
});
// Count followers
const followerCount = followers.length;
Get Connected Vertices
Get all vertices connected to a vertex (both inbound and outbound):
// Get all users connected to Alice (any direction)
const connected = await UserGraph.getConnected('users/alice', 'follows', {
direction: 'any',
limit: 50
});
// Get friends (bidirectional relationship)
const friends = await UserGraph.getConnected('users/alice', 'friends', {
direction: 'any'
});
Count Relationships
// Count outbound relationships
const followingCount = await UserGraph.countRelationships(
'users/alice',
'outbound'
);
// Count inbound relationships
const followerCount = await UserGraph.countRelationships(
'users/alice',
'inbound'
);
// Count all relationships
const totalConnections = await UserGraph.countRelationships(
'users/alice',
'any'
);
Graph Traversals
For more advanced traversals, use the GraphTraversal class:
Basic Traversal
import { GraphTraversal } from 'arango-typed';
const traversal = new GraphTraversal(
db,
'social', // Graph name
'users/alice', // Start vertex
{
direction: 'outbound',
minDepth: 1,
maxDepth: 2,
limit: 100
}
);
// Get all vertices
const vertices = await traversal.vertices();
// Get all edges
const edges = await traversal.edges();
// Get all paths
const paths = await traversal.paths();
Breadth-First Search (BFS)
// Traverse using BFS
const traversal = new GraphTraversal(db, 'social', 'users/alice')
.direction('outbound')
.depth(1, 3)
.bfs(true)
.limit(50);
const results = await traversal.execute();
Depth-First Search (DFS)
// Traverse using DFS (default)
const traversal = new GraphTraversal(db, 'social', 'users/alice')
.direction('outbound')
.depth(1, 5)
.uniqueVertices('path')
.limit(100);
const results = await traversal.execute();
Filtered Traversal
// Traverse with filters
const traversal = new GraphTraversal(db, 'social', 'users/alice')
.direction('outbound')
.depth(1, 2)
.filter('vertex.age >= 30') // Only users 30 or older
.limit(20);
const matureConnections = await traversal.vertices();
Traversal Options
- direction: 'outbound' | 'inbound' | 'any'
- minDepth: Minimum depth to traverse (default: 1)
- maxDepth: Maximum depth to traverse (default: 1)
- uniqueVertices: 'none' | 'global' | 'path' - How to handle duplicate vertices
- uniqueEdges: 'none' | 'global' | 'path' - How to handle duplicate edges
- bfs: Use breadth-first search (default: false, uses DFS)
- filter: AQL filter expression
- limit: Maximum number of results
Path Queries
Path queries help you find connections between vertices. arango-typed provides several path query methods.
Shortest Path
Find the shortest path between two vertices:
import { PathQueries } from 'arango-typed';
const pathQueries = new PathQueries(db, 'social');
// Find shortest path from Alice to Charlie
const path = await pathQueries.shortestPath(
'users/alice',
'users/charlie',
{
direction: 'any'
}
);
if (path) {
console.log('Path found:');
console.log('Vertices:', path.vertices);
console.log('Edges:', path.edges);
console.log('Distance:', path.distance);
} else {
console.log('No path found');
}
All Paths
Find all paths between two vertices:
// Find all paths from Alice to Charlie
const allPaths = await pathQueries.allPaths(
'users/alice',
'users/charlie',
{
minDepth: 1,
maxDepth: 5,
direction: 'any',
uniqueVertices: 'global'
}
);
console.log(`Found ${allPaths.length} paths`);
allPaths.forEach((path, index) => {
console.log(`Path ${index + 1}: ${path.vertices.join(' -> ')}`);
console.log(`Distance: ${path.distance}`);
});
K-Shortest Paths
Find the top K shortest paths:
// Find top 3 shortest paths
const topPaths = await pathQueries.kShortestPaths(
'users/alice',
'users/charlie',
3, // K = 3
{
direction: 'any'
}
);
topPaths.forEach((path, index) => {
console.log(`Path ${index + 1} (distance: ${path.distance}):`);
console.log(path.vertices.join(' -> '));
});
Path Exists
Check if a path exists between two vertices:
// Check if Alice can reach Charlie
const exists = await pathQueries.pathExists(
'users/alice',
'users/charlie',
10 // Max depth
);
if (exists) {
console.log('Path exists');
} else {
console.log('No path found');
}
Path Distance
Get the distance (number of edges) between two vertices:
// Get path distance
const distance = await pathQueries.pathDistance(
'users/alice',
'users/charlie'
);
if (distance !== null) {
console.log(`Distance: ${distance} edges`);
} else {
console.log('No path found');
}
Using GraphModel.getPath
GraphModel also provides a getPath method:
// Get path using GraphModel
const path = await UserGraph.getPath(
'users/alice',
'users/charlie',
{
maxDepth: 10,
direction: 'any',
edgeFilter: 'e.status == "active"' // Only active edges
}
);
console.log('Vertices:', path.vertices);
console.log('Edges:', path.edges);
Complete Example: Social Network
Here's a complete example of building a social network graph:
import { connect, getDatabase, graphModel, Schema } from 'arango-typed';
// Connect to database
await connect({
url: 'http://localhost:8529',
database: 'social_network',
username: 'root',
password: ''
});
const db = getDatabase();
// Create graph
try {
await db.createGraph('social', {
edgeDefinitions: [
{
collection: 'follows',
from: ['users'],
to: ['users']
},
{
collection: 'friends',
from: ['users'],
to: ['users']
}
]
});
} catch (error) {
// Graph might already exist
console.log('Graph already exists or error:', error);
}
// Define schemas
const UserSchema = new Schema({
name: String,
email: String,
age: Number,
city: String
});
// Create GraphModel
const UserGraph = graphModel(db, 'social', 'users', UserSchema);
// Create users
const alice = await UserGraph.create({
name: 'Alice',
email: 'alice@example.com',
age: 30,
city: 'New York'
});
const bob = await UserGraph.create({
name: 'Bob',
email: 'bob@example.com',
age: 25,
city: 'San Francisco'
});
const charlie = await UserGraph.create({
name: 'Charlie',
email: 'charlie@example.com',
age: 35,
city: 'New York'
});
const diana = await UserGraph.create({
name: 'Diana',
email: 'diana@example.com',
age: 28,
city: 'Los Angeles'
});
// Create relationships
// Alice follows Bob and Charlie
await UserGraph.createRelationship(alice._id, bob._id, 'follows', {
since: new Date('2023-01-01')
});
await UserGraph.createRelationship(alice._id, charlie._id, 'follows', {
since: new Date('2023-02-01')
});
// Bob follows Charlie
await UserGraph.createRelationship(bob._id, charlie._id, 'follows', {
since: new Date('2023-03-01')
});
// Alice and Bob are friends (bidirectional)
await UserGraph.createRelationship(alice._id, bob._id, 'friends', {
since: new Date('2020-01-01')
});
// Query relationships
// Who does Alice follow?
const aliceFollowing = await UserGraph.getOutbound(alice._id, 'follows');
console.log('Alice follows:', aliceFollowing.map(u => u.name));
// Who follows Alice?
const aliceFollowers = await UserGraph.getInbound(alice._id, 'follows');
console.log('Alice followers:', aliceFollowers.map(u => u.name));
// Who are Alice's friends?
const aliceFriends = await UserGraph.getConnected(alice._id, 'friends', {
direction: 'any'
});
console.log('Alice friends:', aliceFriends.map(u => u.name));
// Find path from Alice to Diana
const pathQueries = new PathQueries(db, 'social');
const path = await pathQueries.shortestPath(alice._id, diana._id);
if (path) {
console.log('Path found:', path.vertices);
} else {
console.log('No path found');
}
// Traverse Alice's network
const traversal = new GraphTraversal(db, 'social', alice._id)
.direction('outbound')
.depth(1, 2)
.limit(10);
const network = await traversal.vertices();
console.log('Alice network:', network.map(u => u.name));
Graph Algorithms
You can implement various graph algorithms using traversals and path queries:
Finding Mutual Connections
// Find mutual friends between Alice and Bob
const aliceFriends = await UserGraph.getConnected(alice._id, 'friends', {
direction: 'any'
});
const bobFriends = await UserGraph.getConnected(bob._id, 'friends', {
direction: 'any'
});
const aliceFriendIds = new Set(aliceFriends.map(f => f._id));
const mutualFriends = bobFriends.filter(f => aliceFriendIds.has(f._id));
console.log('Mutual friends:', mutualFriends.map(f => f.name));
Finding Common Followers
// Find users who follow both Alice and Bob
const aliceFollowers = await UserGraph.getInbound(alice._id, 'follows');
const bobFollowers = await UserGraph.getInbound(bob._id, 'follows');
const aliceFollowerIds = new Set(aliceFollowers.map(f => f._id));
const commonFollowers = bobFollowers.filter(f => aliceFollowerIds.has(f._id));
console.log('Common followers:', commonFollowers.map(f => f.name));
Degree Centrality
// Calculate degree centrality (number of connections)
async function degreeCentrality(userId: string) {
const outbound = await UserGraph.countRelationships(userId, 'outbound');
const inbound = await UserGraph.countRelationships(userId, 'inbound');
return outbound + inbound;
}
const aliceCentrality = await degreeCentrality(alice._id);
console.log(`Alice's degree centrality: ${aliceCentrality}`);
Best Practices
- Index Edge Collections: Create indexes on
_fromand_tofields for performance - Limit Traversal Depth: Always set reasonable maxDepth limits to avoid infinite loops
- Use Unique Vertices/Edges: Set appropriate uniqueness options to avoid redundant paths
- Filter Early: Use filters in traversals to reduce result sets
- Cache Results: Cache frequently accessed graph queries
- Batch Operations: When creating many relationships, batch them for better performance
- Monitor Performance: Use ArangoDB query profiler for slow graph queries
- Choose BFS vs DFS: Use BFS for finding shortest paths, DFS for exploring all paths
- Validate Graph Structure: Ensure graphs are properly defined before operations
- Handle Cycles: Be aware of cycles in graphs and use appropriate uniqueness settings
Performance Optimization
- Index Edge Collections: Index
_from,_to, and frequently queried edge properties - Limit Depth: Keep traversal depths reasonable (typically 1-5 for most use cases)
- Use Limits: Always set limits on traversals to prevent large result sets
- Filter at Database Level: Use AQL filters instead of filtering in application code
- Use Shortest Path: For path queries, use shortest path when possible instead of all paths
- Cache Graph Structure: Cache graph metadata and frequently accessed vertices
Common Use Cases
Social Networks
Model friendships, follows, likes, and other social connections:
// Friend recommendations based on mutual friends
const user = await UserGraph.findById('users/123');
const userFriends = await UserGraph.getConnected(user._id, 'friends', {
direction: 'any'
});
// Find friends of friends (excluding current friends)
const recommendations = new Set();
for (const friend of userFriends) {
const friendFriends = await UserGraph.getConnected(friend._id, 'friends', {
direction: 'any'
});
friendFriends.forEach(ff => {
if (ff._id !== user._id && !userFriends.find(f => f._id === ff._id)) {
recommendations.add(ff._id);
}
});
}
Recommendation Systems
Build recommendation engines based on user behavior and relationships:
// Product recommendations based on similar users
const user = await UserGraph.findById('users/123');
const similarUsers = await UserGraph.getConnected(user._id, 'similar_to', {
direction: 'any',
limit: 10
});
// Get products liked by similar users
const recommendations = [];
for (const similarUser of similarUsers) {
const likedProducts = await ProductGraph.getOutbound(similarUser._id, 'likes');
recommendations.push(...likedProducts);
}
Knowledge Graphs
Model complex relationships in knowledge bases:
// Find related concepts
const concept = await ConceptGraph.findById('concepts/ai');
const related = await ConceptGraph.getConnected(concept._id, 'related_to', {
direction: 'any',
depth: 2
});
Error Handling
Always handle errors when working with graphs:
try {
const path = await UserGraph.getPath('users/alice', 'users/bob');
if (path.vertices.length === 0) {
console.log('No path found');
} else {
console.log('Path found:', path.vertices);
}
} catch (error) {
console.error('Path query failed:', error);
// Handle error appropriately
}
Limitations and Considerations
- Graph Size: Very large graphs may require careful optimization
- Traversal Performance: Deep traversals can be slow on large graphs
- Memory Usage: Path queries on large graphs may consume significant memory
- Graph Definition: Graphs must be properly defined before use
- Edge Collections: Edge collections must be part of the graph definition