Working with Graphs
The RelationalAI (RAI) Python API provides a Graph
class for creating graphs from entities and relationships in a model.
In this guide, you’ll learn how to create Graph
objects and run graph algorithms.
Graphs
Section titled “Graphs”Graphs are comprised of:
- Nodes, which represent entities from a RAI model.
- Edges, which represent relationships between entities.
Before you can create a graph, you first need to define entities and relationships in the model.
In a sense, models are a kind of graph. Entities are like nodes, and relationships, such as properties, are like edges. What a graph represents, then, is a subset — sometimes called a projection — of the model’s entities and relationships that can be visualized and analyzed using graph algorithms.
Note that the graphs created with the Graph
class are not labeled property graphs and do not retain the structure of the model.
Graphs are homogenous, meaning they have only one type of node and one type of edge.
They are primarily used for running graph algorithms on a subset of the model’s entities and relationships.
For the examples in this section, we’ll use the following model:
import relationalai as rai
model = rai.Model("MyModel")Person = model.Type("Person")Product = model.Type("Product")
with model.rule(): # Define some Product entities. flashlight = Product.add(description="Flashlight") batteries = Product.add(description="Batteries") toothpaste = Product.add(description="Toothpaste")
# Define some Person entities. alice = Person.add(name="Alice") bob = Person.add(name="Bob") carol = Person.add(name="Carol")
# Define a multi-valued friends property that connect Person entities. alice.friends.extend([bob, carol]) bob.friends.add(alice) carol.friends.add(alice)
# Define a multi-valued purchases property that connect Person and Product entities. alice.purchases.extend([flashlight, batteries]) bob.purchases.add(batteries)
This model has three Product
entities and three Person
entities:
- Alice, who is friends with Bob and Carol, has purchased a flashlight and batteries.
- Bob, who is friends with Alice, has purchased batteries.
- Carol, who is friends with Alice, has not purchased any products.
The friends
and purchases
properties are multi-valued and represent relationships between entities:
friends
is a symmetric relationships betweenPerson
entities. If Person A is friends with Person B, then Person B is friends with Person A.purchases
is an asymmetric relationship betweenPerson
andProduct
entities. A person may purchase a product, but a product does not purchase a person.
We’ll use this model to create two different graph objects, one for each relationship type.
Create a Directed Graph
Section titled “Create a Directed Graph”A directed graph is a graph where edges have a direction. That is, an edge from node A to node B is not the same as an edge from node B to node A.
Directed graphs model asymmetric relationships, such as the purchases
relationship between Person
and Product
entities in the preceding model described above.
The following snippet creates a directed graph from the model:
from relationalai.std.graphs import Graph
# Create a graph object from the model.purchases_graph = Graph(model)
# Add nodes to the graph.purchases_graph.Node.extend(Person)purchases_graph.Node.extend(Product)
# Add edges to the graph.purchases_graph.Edge.extend(Person.purchases)
# Visualize the graph.purchases_graph.visualize()
Let’s break down the code, step by step:
-
First, you must import the
Graph
class from therelationalai.std.graphs
module. -
Then, you instantiate a
Graph
object from themodel
. By default, theGraph
constructor creates a directed graph. If you prefer to be explicit, you may set theundirected
parameter toFalse
:purchases_graph = Graph(model, undirected=False) -
You add nodes to the graph using the
purchase_graph.Node
type. Here,.extend()
is used to add allPerson
and allProduct
entities to the graph. -
You add edges to the graph using the
purchase_graph.Edge
class. UnlikeNode
, theEdge
class is not aType
. However, it does have an.extend()
method that you can use to add edges to the graph based on a property in the model. -
Finally, you can visualize the graph using the
.visualize()
method. In a Jupyter notebook, this method will render an interactive visualization of the graph below the cell. In non-interactive environments, the method will open a new browser tab with the visualization.For this graph, the visualization looks like this:
There are two isolated nodes in the graph. These are nodes that are not connected to any other nodes by edges. In this case, the isolated nodes represent the toothbrush, since no one has purchased it, and Carol, since she has not purchased any products.
Because the graph is directed, the edges have arrows indicating the direction of the relationship. Note that the nodes are not labeled in the visualization. See the Graph Visualization guide for details on customizing the visualization.
Create an Undirected Graph
Section titled “Create an Undirected Graph”An undirected graph is a graph where edges do not have a direction. An edge from node A to node B is the same as an edge from node B to node A.
Undirected graphs model symmetric relationships, such as the friends
relationship between Person
entities in the preceding model described above.
The following snippet creates an undirected graph from the model:
from relationalai.std.graphs import Graph
# Create a graph object from the model. Set the `undirected` parameter to `True`# to create an undirected graph.friends_graph = Graph(model, undirected=True)
# Add nodes to the graph.friends_graph.Node.extend(Person)
# Add edges to the graph.friends_graph.Edge.extend(Person.friends)
# Visualize the graph.friends_graph.visualize()
The code is similar to the directed graph example, except that the undirected
parameter is set to True
when creating the Graph
object.
Here’s the visualization of the undirected graph:
Edges in undirected graphs do not have arrows, indicating that the relationships are symmetric. See the Graph Visualization guide for details on customizing the visualization.
Limitations
Section titled “Limitations”-
Multigraphs are not supported. That is, a graph cannot have multiple edges between the same pair of nodes. Note that in a directed graph an edge from node A to node B is distinct from an edge from node B to node A, so this limitation only applies to edges in the same direction.
-
Node weights are not supported. Although weighted graphs are supported, the weights are associated with edges, not nodes. You may set properties on nodes that represent weights, but RAI graph algorithms do not make use of node weights.
A graph’s node set is an instance of the Node
class, which is a subclass of Type
.
To illustrate how to work with nodes, we’ll use the following model:
import relationalai as rai
model = rai.Model("MyModel")Person = model.Type("Person")Product = model.Type("Product")
with model.rule(): # Define some Product entities. flashlight = Product.add(description="Flashlight") batteries = Product.add(description="Batteries") toothpaste = Product.add(description="Toothpaste")
# Define some Person entities. alice = Person.add(name="Alice") bob = Person.add(name="Bob") carol = Person.add(name="Carol")
# Define a multi-valued purchases property that connect Person and Product entities. alice.purchases.extend([flashlight, batteries]) bob.purchases.add(toothbrush)
Add Nodes to a Graph
Section titled “Add Nodes to a Graph”There are three ways to add nodes to a graph:
-
Add all entities of a
Type
to the graph using theNode.extend()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add all Person entities as nodes in the graph.purchases_graph.Node.extend(Person)# Add all Product entities as nodes in the graph.purchases_graph.Node.extend(Product) -
Add specific entities to the graph using the
Node.add()
method inside of amodel.rule()
block:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add specific Person entities as nodes in the graph.with model.rule():purchases_graph.Node.add(Person(name="Alice"))# Add specific Product entities as nodes in the graph.with model.rule():purchases_graph.Node.add(Product(description="Flashlight"))purchases_graph.Node.add(Product(description="Batteries")) -
Automatically add nodes when edges are added to the graph:
from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add all Person entities as nodes in the graph.purchases_graph.Node.extend(Person)# Add edges to the graph from the Person.purchases property. Products that# people have purchased are automatically added to the graph as nodes.purchases_graph.Edge.extend(Person.purchases)In the example above, each
Person
entity is added to the graph. Then edges are created from thePerson.purchases
property. This automatically adds the flashlight and batteries products to the graph’s node set. The toothbrush product is not added to the node set because it is not connected to anyPerson
entities by thepurchases
property.
Remove Nodes from a Graph
Section titled “Remove Nodes from a Graph”There is no way to directly remove nodes from a graph. You declare graphs and their nodes in your Python code. A graph is not materialized until it is queried or visualized.
To remove nodes from a graph, you must redeclare the graph without the nodes you want to remove. You could do this by either:
-
Editing the code that adds nodes to the graph to exclude the nodes you want to remove.
-
Creating a new graph object and add only the nodes you want to keep:
# Create a copy of the purchases_graph without the node for Alice.purchases_graph_without_alice = Graph(model)with model.rule():# Get all Person entities that are nodes in the purchases_graph.person = Person(purchases_graph.Node)# Exclude Alice.person.name != "Alice"# Add all remaining Person entities as nodes in the new graph.purchases_graph_without_alice.Node.add(person)with model.rule():# Get all edges in the purchases_graph.edge = purchases_graph.Edge()# Exclude edges that connect to Alice.alice = Person(name="Alice")edge.from_ != aliceedge.to != alice# Add all remaining edges to the new graph.purchases_graph_without_alice.Edge.add(from_=edge.from_, to=edge.to)
Set Properties on Nodes
Section titled “Set Properties on Nodes”Nodes support both single-valued and multi-valued properties. There are four ways to set properties on nodes:
-
Pass single-valued properties to keyword arguments of the
Node.extend()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add all Person entities as nodes in the graph. Set the label property of# each node to the Person's name.purchases_graph.Node.extend(Person, label=Person.name) -
Pass single-valued properties to keyword arguments of the
Node.add()
method inside of amodel.rule()
block:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add specific Person entities as nodes in the graph. Set the label property# of each node to the Person's name.with model.rule():alice = Person(name="Alice")purchases_graph.Node.add(alice, label=alice.name) -
Set single-valued properties on node instances using the
.set()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Set the label property of each node to the Person's name.with model.rule():person = Person()node = purchases_graph.Node.add(person)node.set(label=person.name) -
Set multi-valued properties on nodes:
from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add edges to the graph from the Person.purchases property.purchases_graph.Edge.extend(Person.purchases)# Set a neighbors property of each node.with model.rule():node = purchases_graph.Node()neighbor = purchases_graph.Edge(from_=node).tonode.neighbors.add(neighbor)
Query Properties of Nodes
Section titled “Query Properties of Nodes”Properties set on nodes can be queried by getting an Instance
of a graph’s Node
type and selecting the properties you want:
from relationalai.std.graphs import Graph
purchases_graph = Graph(model)purchases_graph.Node.extend(Person, label=Person.name)purchases_graph.Edge.extend(Person.purchases)
with model.query() as select: # Get an instance of the graph.Node() type. node = purchases_graph.Node() # Select the label property of each node. response = select(node.label)
# Display the results.print(response.results)# label# 0 Alice# 1 Bob# 2 Carol
Nodes cannot directly access properties of the entities they represent.
For instance, although each node in the graph is a Person
entity, you cannot access the Person
entity’s name
property directly from the node:
with model.query() as select: node = purchases_graph.Node() # The following raises an UninitializedProperty warning. response = select(node.name)
# --- Uninitialized property -----------------------------------------------------
# The property graph224_name has never been set or added to and so will always# cause the rule or query to fail.
# 1 | with model.query() as select:# 2 | node = purchases_graph.Node()# 3 | response = select(node.name)
# --------------------------------------------------------------------------------
To access the name
property, you may either:
-
Set the
name
property on the node when you add it to the graph:purchases_graph.Node.extend(Person, name=Person.name)# Now the following query works:with model.query() as select:node = purchases_graph.Node()# The following raises an UninitializedProperty warning.response = select(node.name)print(response.results)# name# 0 Alice# 1 Bob# 2 CarolSee Set Properties on Nodes for details.
-
Get an
Instance
of thePerson
type, filter it for people who are nodes in the graph, and select thename
property:with model.query() as select:# Get people who are also nodes in the graph.person = Person(purchases_graph.Node)# Select the name property of each person.response = select(person.name)print(response.results)# name# 0 Alice# 1 Bob# 2 CarolThis approach is more flexible than setting properties on nodes because it allows you to access any property of the
Person
entity, not just those set on the node.Note that
person = Person(purchase_graph.Node)
is equivalent to:person = Person()purchase_graph.Node(person)See Filtering Objects by Type for more information.
A graph’s edge set is an instance of the Edge
class.
Edge
is analogous to a Type
in that it represents a set of things – namely the edges of the graph.
However, Edge
is not a subclass of Type
because edges are not entities in the model.
Instances of edges are represented by the EdgeInstance
class, which is analogous to an Instance
of a Type
.
EdgeInstance
objects have two special properties named from_
and to
that represent the nodes at either end of the edge.
To illustrate how to work with edges, we’ll use the following model:
import relationalai as rai
model = rai.Model("MyModel")Person = model.Type("Person")Product = model.Type("Product")
with model.rule(): # Define some Product entities. flashlight = Product.add(description="Flashlight") batteries = Product.add(description="Batteries") toothpaste = Product.add(description="Toothpaste")
# Define some Person entities. alice = Person.add(name="Alice") bob = Person.add(name="Bob") carol = Person.add(name="Carol")
# Define a multi-valued friends property that connect Person entities. alice.friends.extend([bob, carol]) bob.friends.add(alice) carol.friends.add(alice)
# Define a multi-valued purchases property that connect Person and Product entities. alice.purchases.extend([flashlight, batteries]) bob.purchases.add(toothbrush)
Add Edges to a Graph
Section titled “Add Edges to a Graph”There are two ways to add edges to a graph:
-
Add all edges defined by a property to the graph using the
Edge.extend()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add edges to the graph from the Person.purchases property. Note that entities# that are connected by the purchases property are automatically added to the# nodes of the graph.purchases_graph.Edge.extend(Person.purchases) -
Add specific edges to the graph using the
Edge.add()
method inside of amodel.rule()
block:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add specific edges to the graph. Connect Alice to the Flashlight and Batteries# products. Note that Alice, Flashlight, and Batteries are automatically added to# the graph's node set.with model.rule():alice = Person(name="Alice")purchases_graph.Edge.add(from_=alice, to=Product(description="Flashlight"))purchases_graph.Edge.add(from_=alice, to=Product(description="Batteries"))This method allows you to add edges that are not defined by a property in the model. For example, you could add edges from an entity that connects to multiple other entities, such as a
Purchase
entity that connects aPerson
entity to aProduct
entity:# Define a Purchase type.Purchase = model.Type("Purchase")# Add some Purchase entities to the model.with model.rule():Purchase.add(by=Person(name="Alice"), product=Product(description="Flashlight"), quantity=1)Purchase.add(by=Person(name="Bob"), product=Product(description="Batteries"), quantity=2)# Add edges to the graph from the Purchase entities.with model.rule():purchase = Purchase()purchases_graph.Edge.add(from=purchase.by, to=purchase.product)Note that calling
.add()
multiple times with the samefrom_
andto
arguments does not create multiple edges between the same pair of nodes, since multigraphs are not supported. Only one edge is created between the nodes and no error is raised.
Remove Edges from a Graph
Section titled “Remove Edges from a Graph”There is no way to directly remove edges from a graph. You declare graphs and their edges in your Python code. A graph is not materialized until it is queried or visualized.
To remove edges from a graph, you must redeclare the graph without the edges you want to remove. You could do this by either:
-
Editing the code that adds edges to the graph to exclude the edges you want to remove.
-
Creating a new graph object and add only the edges you want to keep:
# Create a copy of the purchases_graph without the edges involving Batteries.purchases_graph_without_edges = Graph(model)# Add all of the nodes from the purchases_graph to the new graph.purchases_graph_without_edges.Node.extend(purchases_graph.Node)with model.rule():# Get all edges in the purchases_graph.edge = purchases_graph.Edge()# Exclude edges that connect to Flashlight.batteries = Product(description="Flashlight")edge.from_ != batteriesedge.to != batteries# Add all remaining edges to the new graph.purchases_graph_without_edges.Edge.add(from_=edge.from_, to=edge.to)
Set Properties on Edges
Section titled “Set Properties on Edges”Edges support only single-valued properties. There are three ways to set properties on edges:
-
Pass properties to keyword arguments of the
Edge.extend()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add edges to the graph from the Person.purchases property. Set the label# property of each edge to the string "purchased."purchases_graph.Edge.extend(Person.purchases, label="purchased") -
Pass properties to keyword arguments of the
Edge.add()
method inside of amodel.rule()
block:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add specific edges to the graph. Connect Alice to Flashlight and set the# label to the string "purchased."with model.rule():alice = Person(name="Alice")purchases_graph.Edge.add(from_=alice, to=Product(description="Flashlight"), label="purchased")Avoid calling
.add()
multiple times with the samefrom_
andto
arguments. Although it is possible to do so without error, the behavior is undefined:with model.rule():alice = Person(name="Alice")flashlight = Product(description="flashlight")purchases_graph.Edge.add(from_=alice, to=flashlight, label="purchased")# The following does not create a new edge between Alice and the flashlight,# and the label property of the existing edge is not guaranteed to be updated.purchases_graph.Edge.add(from_=alice, to=flashlight, label="bought")Since multigraphs are not supported, only one edge is created between the nodes. Moreover, the property’s value is indeterminate. Edge properties, like
label
, behave like single-valued properties of other entities in the model. Two values have been declared, but only one will be used and there’s no guarantee it will always be the first or the last value. -
Set properties on edge instances using the
EdgeInstance.set()
method:from relationalai.std.graphs import Graph# Create a graph object from the model.purchases_graph = Graph(model)# Add edges from the Person.purchases property to the graph.purchases_graph.Edge.extend(Person.purchases)# Set the label property of each edge to the string "purchased."with model.rule():edge = purchases_graph.Edge()edge.set(label="purchased")
Query Properties of Edges
Section titled “Query Properties of Edges”Properties set on edges can be queried by getting an EdgeInstance
from a graph’s Edge
set and selecting the properties you want:
from relationalai.std.graphs import Graph
purchases_graph = Graph(model)purchases_graph.Edge.extend(Person.purchases)
# Get the entities at either end of the edge.with model.query() as select: edge = purchases_graph.Edge() response = select(edge.from_.name, edge.to.name)
print(response.results)# name name2# 0 Alice Flashlight# 1 Alice Batteries# 2 Bob Batteries
Every edge instance has from_
and to
properties that return Instance
objects representing the nodes at either end of the edge.
Properties of the nodes can be accessed directly from the Instance
objects.
In a directed graph, like the purchase_graph
in the preceding example, from_
is the source node and to
is the target node.
In an undirected graph, from_
and to
are not interchangeable.
The direction of the edge is determined by the order in which the nodes are added to the edge:
friends_graph = Graph(model, undirected=True)friends_graph.Edge.extend(Person.friends)
with model.query() as select: edge = friends_graph.Edge() edge.from_ == Person(name="Alice") response = select(edge.from_.name, edge.to.name)
print(response.results)# name name2# 0 Alice Bob# 1 Alice Carol
with model.query() as select: edge = friends_graph.Edge() edge.to == Person(name="Alice") response = select(edge.from_.name, edge.to.name)
print(response.results)# Empty DataFrame# Columns: []# Index: []
The Person.friends
property connects Alice to Bob and Carol.
Even though the graph is undirected, Alice is considered the source node and Bob and Carol are considered target nodes.
You can ensure that from_
and to
are interchangeable by adding a rule that makes the Person.friends
relationship symmetric:
with model.rule(): # Make the Person.friends relationship symmetric. person = Person() friend = person.friends friend.friends.add(person)
with model.query() as select: edge = friends_graph.Edge() response = select(edge.from_.name, edge.to.name)
print(response.results)# name name2# 0 Alice Bob# 1 Alice Carol# 2 Bob Alice# 3 Carol Alice
Weighted Graphs
Section titled “Weighted Graphs”Edges in a graph may be weighted, meaning they have a numerical value associated with them.
To create a weighted graph, you must set the weighted
parameter to True
when creating the Graph
object and set the weight
property on the edges:
import relationalai as raifrom relationalai.std.graphs import Graph
model = rai.Model("MyModel")Person = model.Type("Person")Friendship = model.Type("Friendship")
# Add some Person entities and Friendship relationships to the model.with model.rule(): alice = Person.add(name="Alice") bob = Person.add(name="Bob") carol = Person.add(name="Carol") Friendship.add(person1=alice, person2=bob, years_known=2) Friendship.add(person1=alice, person2=carol, years_known=5)
# Create a weighted graph object from the model.friends_graph = Graph(model, undirected=True, weighted=True)
# Add edges to the graph from the Friendship type. The weight of each edge is# set to the years_known property of the Friendship relationship.with model.rule(): friendship = Friendship() friends_graph.Edge.add( from_=friendship.person1, to=friendship.person2, weight=friendship.years_known )
# Show the edges and their weights.with model.query() as select: edge = friends_graph.Edge() response = select(edge.from_.name, edge.to.name, edge.weight)
print(response.results)# name name2 v# 0 Alice Bob 2# 1 Alice Carol 5
Both directed and undirected graphs may be weighted.
Any edges that are not explicitly given a weight
are assigned a default weight of 1.0
.
Just like other single-valued properties, you should avoid setting an edges’ weight
property multiple times.
For instance, the following is invalid:
with model.rule(): friendship = Friendship() edge = friends_graph.Edge.add( from_=friendship.person1, to=friendship.person2, weight=friendship.years_known ) # The following does not update the weight of the edge. It creates an edge with # an indeterminate weight value. edge.set(weight=friendship.years_known + 1)
# Querying the model will result in an error:with model.query(): edge = friends_graph.Edge() response = select(edge.from_.name, edge.to.name, edge.weight)
# --- Integrity constraint violation ---------------------------------------------## The provided weight relation must not contain multiple-weight edges.## 17 | friends_graph = Graph(model, undirected=True, weighted=True)## --------------------------------------------------------------------------------
In some cases, however, you may want to update the edge weights in a graph. To do so, you must create a new graph with the same nodes and edges but with different weights:
# Create a new graph.new_graph = Graph(model)
# Add nodes from friends_graph to the new graph.new_graph.Node.extend(friends_graph.Node)
# Add edges from friends_graph to the new graph with updated weights.with model.rule(): edge = friends_graph.Edge() new_graph.Edge.add( from_=edge.from_, to=edge.to, weight=edge.weight + 1 )