graph kit — lightweight typed directed multigraph + pattern queries
In-memory, typed directed multigraph with powerful and performant Cypher-inspired pattern queries
In-memory, typed directed multigraph with:
- Typed nodes (e.g.,
Person
,Team
,Project
,Resource
) - Typed edges (e.g.,
WORKS_FOR
,MANAGES
,ASSIGNED_TO
,DEPENDS_ON
) - Multiple relationships between the same nodes
- Advanced Cypher queries with WHERE clauses, logical operators, and variable-length paths
- Complete path results with Neo4j-style edge information
- Graph algorithms for analysis (shortest path, connected components, topological sort, reachability)
Table of Contents
- 1. Quick Preview
- 2. Complete Usage Examples
- 3. Graph Algorithms
- 4. Generic Traversal Utilities
- 5. Pattern Query Examples
- 6. Mini-Cypher Reference
- 7. Comparison with Cypher
- 8. Design and performance
- 9. JSON Serialization
- 10. Examples index
- License
1. Quick Preview
Interactive graph algorithms demo showing centrality analysis in the Flutter app.
2. Complete Usage Examples
This section provides copy-paste ready examples demonstrating all major query methods with a sample graph. Each example can be run as a standalone Dart script.
2.1 Setup: Sample Graph
import 'package:graph_kit/graph_kit.dart';
void main() {
// Create graph and add sample data
final graph = Graph<Node>();
// Add people
graph.addNode(Node(id: 'alice', type: 'Person', label: 'Alice Cooper'));
graph.addNode(Node(id: 'bob', type: 'Person', label: 'Bob Wilson'));
graph.addNode(Node(id: 'charlie', type: 'Person', label: 'Charlie Davis'));
// Add teams
graph.addNode(Node(id: 'engineering', type: 'Team', label: 'Engineering'));
graph.addNode(Node(id: 'design', type: 'Team', label: 'Design Team'));
graph.addNode(Node(id: 'marketing', type: 'Team', label: 'Marketing'));
// Add projects
graph.addNode(Node(id: 'web_app', type: 'Project', label: 'Web Application'));
graph.addNode(Node(id: 'mobile_app', type: 'Project', label: 'Mobile App'));
graph.addNode(Node(id: 'campaign', type: 'Project', label: 'Ad Campaign'));
// Add relationships
graph.addEdge('alice', 'WORKS_FOR', 'engineering');
graph.addEdge('bob', 'WORKS_FOR', 'engineering');
graph.addEdge('charlie', 'MANAGES', 'engineering');
graph.addEdge('charlie', 'MANAGES', 'design');
graph.addEdge('charlie', 'MANAGES', 'marketing');
graph.addEdge('engineering', 'ASSIGNED_TO', 'web_app');
graph.addEdge('engineering', 'ASSIGNED_TO', 'mobile_app');
graph.addEdge('design', 'ASSIGNED_TO', 'mobile_app');
graph.addEdge('marketing', 'ASSIGNED_TO', 'campaign');
graph.addEdge('alice', 'LEADS', 'web_app');
final query = PatternQuery(graph);
// Run examples below...
}
2.2 Basic Queries - Get Single Type
// Get all people, query.match returns Map<String, Set<String>>
final people = query.match('person:Person');
print(people); // {person: {alice, bob, charlie}}
// Get all teams, query.match returns Map<String, Set<String>>
final teams = query.match('team:Team');
print(teams); // {team: {engineering, design, marketing}}
// Get all projects, query.match returns Map<String, Set<String>>
final projects = query.match('project:Project');
print(projects); // {project: {web_app, mobile_app, campaign}}
2.3 Relationship Queries - Get Connected Nodes
// Find who works for teams, query.match returns Map<String, Set<String>>
final workers = query.match('person:Person-[:WORKS_FOR]->team:Team');
print(workers); // {person: {alice, bob}, team: {engineering}}
// Find who manages teams, query.match returns Map<String, Set<String>>
final managers = query.match('person:Person-[:MANAGES]->team:Team');
print(managers); // {person: {charlie}, team: {engineering, design, marketing}}
// Find team assignments to projects, query.match returns Map<String, Set<String>>
final assignments = query.match('team:Team-[:ASSIGNED_TO]->project:Project');
print(assignments); // {team: {engineering, design, marketing}, project: {web_app, mobile_app, campaign}}
2.4 Queries from Specific Starting Points
// What does Alice work on? query.match with startId returns Map<String, Set<String>>
final aliceWork = query.match(
'person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project',
startId: 'alice'
);
print(aliceWork); // {person: {alice}, team: {engineering}, project: {web_app, mobile_app}}
// What does Charlie manage? query.match with startId returns Map<String, Set<String>>
final charlieManages = query.match(
'person-[:MANAGES]->team',
startId: 'charlie'
);
print(charlieManages); // {person: {charlie}, team: {engineering, design, marketing}}
// Who works on the web app project? query.match with startId returns Map<String, Set<String>>
final webAppTeam = query.match(
'project<-[:ASSIGNED_TO]-team<-[:WORKS_FOR]-person',
startId: 'web_app'
);
print(webAppTeam); // {project: {web_app}, team: {engineering}, person: {alice, bob}}
2.5 Row-wise Results - Preserve Path Relationships
// Get specific person-team-project combinations, query.matchRows returns List<Map<String, String>>
final rows = query.matchRows('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project');
print(rows);
// [
// {person: alice, team: engineering, project: web_app},
// {person: alice, team: engineering, project: mobile_app},
// {person: bob, team: engineering, project: web_app},
// {person: bob, team: engineering, project: mobile_app}
// ]
// Access individual path data
print(rows.first); // {person: alice, team: engineering, project: web_app}
print(rows.first['person']); // alice
print(rows.first['team']); // engineering
2.6 Complete Path Results with Edge Information
// Get complete path information, query.matchPaths returns List<PathMatch>
final paths = query.matchPaths('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project');
print(paths.length); // 4
// Print all paths with edges
for (final path in paths) {
print(path.nodes); // {person: alice, team: engineering, project: web_app}
for (final edge in path.edges) {
print(' ${edge.from} -[:${edge.type}]-> ${edge.to}'); // alice -[:WORKS_FOR]-> engineering
}
}
2.7 Multiple Pattern Queries
// Get all of Alice's connections, query.matchMany returns Map<String, Set<String>>
final aliceConnections = query.matchMany([
'person-[:WORKS_FOR]->team',
'person-[:LEADS]->project'
], startId: 'alice');
print(aliceConnections); // {person: {alice}, team: {engineering}, project: {web_app}}
// Combine multiple relationship types, query.matchMany returns Map<String, Set<String>>
final allConnections = query.matchMany([
'person:Person-[:WORKS_FOR]->team:Team',
'person:Person-[:MANAGES]->team:Team',
'person:Person-[:LEADS]->project:Project'
]);
print(allConnections); // {person: {alice, bob, charlie}, team: {engineering, design, marketing}, project: {web_app}}
2.8 WHERE Clause Filtering
// Add people with properties for filtering examples
graph.addNode(Node(
id: 'alice',
type: 'Person',
label: 'Alice Cooper',
properties: {'age': 28, 'department': 'Engineering', 'salary': 85000}
));
graph.addNode(Node(
id: 'bob',
type: 'Person',
label: 'Bob Wilson',
properties: {'age': 35, 'department': 'Engineering', 'salary': 95000}
));
// Filter by age - query.matchRows returns List<Map<String, String>>
final seniors = query.matchRows('MATCH person:Person WHERE person.age > 30');
print(seniors); // [{person: bob}]
// Filter by department - query.matchRows returns List<Map<String, String>>
final engineers = query.matchRows('MATCH person:Person WHERE person.department = "Engineering"');
print(engineers); // [{person: alice}, {person: bob}]
// Combine conditions with AND - query.matchRows returns List<Map<String, String>>
final seniorEngineers = query.matchRows('MATCH person:Person WHERE person.age > 30 AND person.department = "Engineering"');
print(seniorEngineers); // [{person: bob}]
// Use OR conditions - query.matchRows returns List<Map<String, String>>
final youngOrWellPaid = query.matchRows('MATCH person:Person WHERE person.age < 30 OR person.salary > 90000');
print(youngOrWellPaid); // [{person: alice}, {person: bob}]
// Complex filtering with relationships - query.matchRows returns List<Map<String, String>>
final seniorWorkers = query.matchRows('MATCH person:Person-[:WORKS_FOR]->team:Team WHERE person.age > 30');
print(seniorWorkers); // [{person: bob, team: engineering}]
2.9 Utility Methods
// Find by type, query.findByType returns Set<String>
final allPeople = query.findByType('Person');
print(allPeople); // {alice, bob, charlie}
// Find by exact label, query.findByLabelEquals returns Set<String>
final aliceIds = query.findByLabelEquals('Alice Cooper');
print(aliceIds); // {alice}
// Find by label substring, query.findByLabelContains returns Set<String>
final bobUsers = query.findByLabelContains('bob');
print(bobUsers); // {bob}
// Direct edge traversal, query.outFrom returns Set<String>
final aliceTeams = query.outFrom('alice', 'WORKS_FOR');
print(aliceTeams); // {engineering}
// Reverse edge traversal, query.inTo returns Set<String>
final engineeringWorkers = query.inTo('engineering', 'WORKS_FOR');
print(engineeringWorkers); // {alice, bob}
2.10 Summary of Query Methods
Method | Returns | Use Case |
---|---|---|
match() |
Map<String, Set<String>> |
Get grouped node IDs by variable |
matchMany() |
Map<String, Set<String>> |
Combine multiple patterns |
matchRows() |
List<Map<String, String>> |
Preserve path relationships |
matchPaths() |
List<PathMatch> |
Complete path + edge information |
findByType() |
Set<String> |
All nodes of specific type |
findByLabelEquals() |
Set<String> |
Nodes by exact label match |
findByLabelContains() |
Set<String> |
Nodes by label substring |
outFrom() /inTo() |
Set<String> |
Direct edge traversal |
3. Graph Algorithms
Graph Kit includes efficient implementations of common graph algorithms for analysis and pathfinding:
Available Algorithms
- Shortest Path - Find optimal routes between nodes using BFS (counts hops)
- Path Enumeration - Find all possible routes between nodes within hop limits
- Connected Components - Identify groups of interconnected nodes
- Reachability Analysis - Discover all nodes reachable from a starting point
- Topological Sort - Order nodes by dependencies (useful for build systems, task scheduling)
- Centrality Analysis - Identify important nodes (betweenness and closeness centrality)
Shortest path visualization highlighting the optimal route between selected nodes.
Quick Example
import 'package:graph_kit/graph_kit.dart';
// Create a dependency graph
final graph = Graph<Node>();
graph.addNode(Node(id: 'core', type: 'Package', label: 'Core'));
graph.addNode(Node(id: 'utils', type: 'Package', label: 'Utils'));
graph.addNode(Node(id: 'app', type: 'Package', label: 'App'));
// Add dependencies (app depends on utils, utils depends on core)
graph.addEdge('utils', 'DEPENDS_ON', 'core');
graph.addEdge('app', 'DEPENDS_ON', 'utils');
// Use graph algorithms
final algorithms = GraphAlgorithms(graph);
// Find shortest path (counts hops)
final path = algorithms.shortestPath('app', 'core');
print('Path: ${path.path}'); // [app, utils, core]
print('Distance: ${path.distance}'); // 2
// Find all possible paths
final allPaths = enumeratePaths(graph, 'app', 'core', maxHops: 4);
print('Routes: ${allPaths.paths.length}'); // All alternative routes
// Get build order (topological sort)
final buildOrder = algorithms.topologicalSort();
print('Build order: $buildOrder'); // [core, utils, app]
// Find connected components
final components = algorithms.connectedComponents();
print('Components: $components'); // [{core, utils, app}]
// Check reachability
final reachable = algorithms.reachableFrom('app');
print('App can reach: $reachable'); // {app, utils, core}
// Check what can reach a target
final reachableBy = algorithms.reachableBy('core');
print('Can reach core: $reachableBy'); // {app, utils, core}
// Check all connected nodes (bidirectional)
final reachableAll = algorithms.reachableAll('utils');
print('Connected to utils: $reachableAll'); // {app, utils, core}
// Find critical bridge nodes
final betweenness = algorithms.betweennessCentrality();
print('Bridge nodes: ${betweenness.entries.where((e) => e.value > 0.3).map((e) => e.key)}');
// Find communication hubs
final closeness = algorithms.closenessCentrality();
print('Most central: ${closeness.entries.reduce((a, b) => a.value > b.value ? a : b).key}');
Visual Demo
Run the interactive Flutter demo to see graph algorithms in action:
cd example
flutter run
- Switch to "Graph Algorithms" tab
- Click nodes to see shortest paths
- View connected components with color coding
- See topological sort with dependency levels
- Explore reachability analysis
Command Line Demo
For a focused algorithms demonstration:
dart run bin/algorithms_demo.dart
This shows practical examples of:
- Package dependency analysis
- Build order optimization
- Component isolation detection
- Shortest path finding
4. Generic Traversal Utilities
For BFS-style subgraph exploration around nodes within hop limits. Unlike pattern queries that follow specific paths, this explores neighborhoods in all directions using specified edge types.
// Using the same graph setup from section 2...
// Add this to your existing code:
// Explore everything within 2 hops from Alice, expandSubgraph returns SubgraphResult
final aliceSubgraph = expandSubgraph(
graph,
seeds: {'alice'},
edgeTypesRightward: {'WORKS_FOR', 'MANAGES', 'ASSIGNED_TO', 'LEADS'},
forwardHops: 2,
backwardHops: 0,
);
print(aliceSubgraph.nodes); // {alice, engineering, web_app, mobile_app}
print(aliceSubgraph.edges.length); // 4
// Explore everything connected to engineering team, expandSubgraph returns SubgraphResult
final engineeringSubgraph = expandSubgraph(
graph,
seeds: {'engineering'},
edgeTypesRightward: {'ASSIGNED_TO'},
edgeTypesLeftward: {'WORKS_FOR', 'MANAGES'},
forwardHops: 1,
backwardHops: 1,
);
print(engineeringSubgraph.nodes); // {engineering, web_app, mobile_app, alice, bob, charlie}
print(engineeringSubgraph.edges.length); // 5
// Find everyone within 2 hops of projects, expandSubgraph returns SubgraphResult
final projectEcosystem = expandSubgraph(
graph,
seeds: {'web_app', 'mobile_app', 'campaign'},
edgeTypesRightward: {'WORKS_FOR', 'MANAGES'},
edgeTypesLeftward: {'ASSIGNED_TO', 'LEADS'},
forwardHops: 0,
backwardHops: 2,
);
print(projectEcosystem.nodes); // {web_app, mobile_app, campaign, engineering, alice, design, marketing}
print(projectEcosystem.edges.length); // 7
When to use vs Pattern Queries
Use Case | Pattern Queries | expandSubgraph |
---|---|---|
Specific paths | person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project |
No |
Neighborhood exploration | No | Yes - "Everything around Alice" |
Impact analysis | No | Yes - "What's affected by this change?" |
Subgraph extraction | No | Yes - For visualization/analysis |
Known relationships | Yes - Clear path patterns | No |
Unknown structure | No | Yes - Explore what's connected |
5. Pattern Query Examples
- Simple patterns:
"user:User"
→{alice, bob, charlie}
- Forward patterns:
"user-[:MEMBER_OF]->group"
- Backward patterns:
"resource<-[:CAN_ACCESS]-group<-[:MEMBER_OF]-user"
→{alice, bob}
- Label filtering:
"user:User{label~Admin}"
→{bob}
- Multiple edge types:
"person-[:WORKS_FOR|VOLUNTEERS_AT]->org"
→ matches ANY of the specified relationship types - Mixed directions:
"person1-[:WORKS_FOR]->team<-[:MANAGES]-manager"
→ finds common connections and shared relationships - Variable-length paths:
"manager-[:MANAGES*1..3]->subordinate"
→ finds direct and indirect reports
6. Mini-Cypher Reference
GraphKit uses a simplified version of Cypher - the query language used by Neo4j (the most popular graph database). Think of it like SQL for graphs.
What is Cypher?
Cypher is a language designed to describe patterns in graphs. Instead of writing complex code to traverse relationships, you draw the path with text:
- SQL:
SELECT * FROM users WHERE department = 'engineering'
- Cypher:
person:Person-[:WORKS_FOR]->team:Team{label=Engineering}
What is "Mini-Cypher"?
GraphKit supports a subset of Cypher - the most useful parts without the complexity:
Supported: Basic patterns, node types, relationships, label filters, variable-length paths, WHERE clauses with logical operators, parentheses Not supported: Aggregations, complex subqueries
This gives you the power of graph queries without learning the full Cypher language.
How to Build Pattern Queries
Think of pattern queries like giving directions. Instead of "turn left at the store", you're saying "follow this relationship to that type of thing".
Step 1: Start with What You Want to Find
When you want to find all people in your company, you write:
query.match('person:Person')
This breaks down into:
person
= What you want to call them in your results (like a nickname):
= "that are of type"Person
= The actual type of thing you're looking for
Think of it like: "Find me all things of type Person, and I'll call them 'person' in my results"
Step 2: Connect Things with Arrows
Now say you want to know "who works where". You connect person to team:
query.match('person:Person-[:WORKS_FOR]->team:Team')
Reading left to right:
person:Person
= "Start with a person"-[
= "who has a connection":WORKS_FOR
= "of type WORKS_FOR"]->
= "that points to"team:Team
= "a team"
Like saying: "Show me people who have a WORKS_FOR arrow pointing to teams"
Step 3: The Arrow Direction Matters!
Right arrow ->
means "going out from":
person-[:WORKS_FOR]->team // Person points to team (person works FOR the team)
Left arrow <-
means "coming in to":
team<-[:WORKS_FOR]-person // Person points to team (team is worked for BY person)
Same relationship, different starting point!
Step 4: Chain Multiple Steps
Want to follow a longer path? Just keep adding arrows:
person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project
This means:
- Start with a person
- Follow their WORKS_FOR connection to a team
- Follow that team's ASSIGNED_TO connection to a project
Like following a trail: person → team → project
Step 5: Filter by Name
Want to find a specific person? Add their name in curly braces:
person:Person{label=Alice Cooper} // Find exactly "Alice Cooper"
person:Person{label~alice} // Find anyone with "alice" in their name
The ~
means "contains" (like a fuzzy search)
Quick Examples to Try
// Simple: Find all people
query.match('person:Person')
// Returns: {person: {alice, bob, charlie}}
// Connection: Who works where?
query.match('person-[:WORKS_FOR]->team')
// Returns: {person: {alice, bob}, team: {engineering}}
// Chain: Follow a path through the graph
query.match('person-[:WORKS_FOR]->team-[:ASSIGNED_TO]->project')
// Returns: {person: {alice, bob}, team: {engineering}, project: {web_app, mobile_app}}
// Backwards: What teams work on this project?
query.match('project<-[:ASSIGNED_TO]-team', startId: 'web_app')
// Returns: {project: {web_app}, team: {engineering}}
// Filter: Find specific person
query.match('person:Person{label~Alice}')
// Returns: {person: {alice}}
Remember: The names you pick (person
, team
, etc.) become the keys in your results!
Variable-Length Paths
Variable-length paths let you find connections across multiple hops without specifying the exact number of steps:
// Use the unified PatternQuery (includes all advanced features)
final query = PatternQuery(graph);
// Find all direct and indirect reports (1-3 management levels)
query.match('manager-[:MANAGES*1..3]->subordinate')
// Find anyone at least 2 levels down the hierarchy
query.match('manager-[:MANAGES*2..]->subordinate')
// Find dependencies up to 4 steps away
query.match('component-[:DEPENDS_ON*..4]->dependency')
// Find all reachable dependencies (unlimited hops)
query.match('component-[:DEPENDS_ON*]->dependency')
Variable-length syntax:
[:TYPE*]
- Unlimited hops[:TYPE*1..3]
- Between 1 and 3 hops[:TYPE*2..]
- 2 or more hops[:TYPE*..4]
- Up to 4 hops[:TYPE*2]
- Exactly 2 hops
Note: Variable-length paths are fully supported in the unified PatternQuery
implementation.
6.1 Advanced WHERE Clauses and Complex Filtering
For sophisticated filtering beyond basic patterns, GraphKit supports full WHERE clause syntax with logical operators and parentheses.
Complete Cypher Query Language Guide
The comprehensive guide covers:
- Complex logical expressions with parentheses:
(A AND B) OR (C AND D)
- Multiple comparison operators:
>
,<
,>=
,<=
,=
,!=
- Real-world query examples for HR, project management, and organizational analysis
- Property filtering best practices and performance tips
- Error handling and troubleshooting
Quick examples:
// Complex filtering with parentheses
MATCH person:Person WHERE (person.age > 40 AND person.salary > 100000) OR person.department = "Management"
// Multi-hop with filtering
MATCH person:Person-[:WORKS_FOR]->team:Team-[:WORKS_ON]->project:Project WHERE person.salary > 80000 AND project.status = "active"
Try the Interactive WHERE Demo:
cd example
flutter run -t lib/where_demo.dart
The demo includes sample queries, real-time query execution, and a comprehensive dataset for testing complex WHERE clauses.
7. Comparison with Cypher
Feature | Real Cypher | graph_kit |
---|---|---|
Mixed directions | Yes | Yes |
Variable length paths | Yes | Yes |
Multiple edge types | [:TYPE1|TYPE2] |
Yes |
Multiple patterns | pattern1, pattern2 |
No |
Optional matches | Yes | Via matchMany |
WHERE clauses | Yes | Yes |
Logical operators | Yes | Yes (AND, OR) |
Parentheses | Yes | Yes |
8. Design and performance
- Traversal from a known ID (
startId
) is fast:- Each hop uses adjacency maps; cost is proportional to the edges visited.
- Seeding by type (
alias:Type
) does a one-time node scan to find initial seeds.- For small/medium graphs, this is effectively instant; indexing can be added later if needed.
matchMany([...])
mirrors "multiple MATCH/OPTIONAL MATCH" lines in Cypher by running several independent chains from the same start and unioning results.
9. JSON Serialization
Save and load graphs to/from JSON for persistence and data exchange:
import 'dart:io';
import 'package:graph_kit/graph_kit.dart';
// Build your graph
final graph = Graph<Node>();
graph.addNode(Node(id: 'alice', type: 'User', label: 'Alice',
properties: {'email': 'alice@example.com', 'active': true}));
graph.addNode(Node(id: 'team1', type: 'Team', label: 'Engineering'));
graph.addEdge('alice', 'MEMBER_OF', 'team1');
// Serialize to JSON
final json = graph.toJson();
final jsonString = graph.toJsonString(pretty: true);
// Save to file
await File('graph.json').writeAsString(jsonString);
// Load from file
final loadedJson = await File('graph.json').readAsString();
final restoredGraph = GraphSerializer.fromJsonString(loadedJson, Node.fromJson);
// Graph is fully restored - queries work immediately
final query = PatternQuery(restoredGraph);
final members = query.match('team<-[:MEMBER_OF]-user', startId: 'team1');
print(members['user']); // {alice}
10. Examples index
Dart CLI Examples
bin/showcase.dart
– comprehensive graph demo with multiple query examplesbin/access_control.dart
– access control patterns with users, groups, and resourcesbin/project_dependencies.dart
– project dependency analysis and traversalbin/social_network.dart
– social network relationships and friend recommendationsbin/serialization_demo.dart
– JSON serialization and persistencebin/algorithms_demo.dart
– graph algorithms demonstration with dependency analysis
Run any example:
dart run bin/showcase.dart
dart run bin/access_control.dart
Flutter Example App
Interactive graph visualization with pattern queries:
cd example
# Main demo - visual graph with pattern queries
flutter run
# WHERE clause demo - interactive Cypher query testing
flutter run -t lib/where_demo.dart
# Features:
# - Visual graph with nodes and edges
# - Live pattern query execution
# - WHERE clause testing with sample data
# - Path highlighting and visualization
# - Example queries with one-click execution
License
See LICENSE
.
Libraries
- graph_kit
- A lightweight, in-memory graph library with pattern-based queries and efficient traversal.