Create a graph
Build graphs from concepts and relationships in your semantic model so you can run graph algorithms and reuse their results in your model. This guide covers creating directed, undirected, and weighted graphs, defining nodes and edges, and handling multiple edges between nodes.
- Prerequisites
- PyRel is installed. See Install and run PyRel for instructions.
- You understand concepts, relationships, and model definitions. See Build a semantic model.
Understand graph basics
Section titled “Understand graph basics”A graph is a way to represent a set of things and the connections between them. The things are called nodes. The connections are called edges.
This sounds a bit like what you already do with concepts and relationships in a semantic model, and it is! The main difference is that a graph views nodes and edges as part of a single structure that you can use to run graph algorithms.
The Graph class is the container for a graph.
- Nodes: The set of things that count as nodes for this question.
In PyRel graph reasoning, nodes are instances of
graph.Node. - Edges: The set of directed or undirected connections between nodes.
Edges are instances of
graph.Edge. - Optional edge weights: A non-negative numeric value per edge.
- Graph algorithms: Methods that compute properties of the graph structure, like connectivity, centrality, similarity, and communities.
Graphs in PyRel are:
- Labeled property graphs: Nodes and edges can have properties like
id,name, oramount. - Simple graphs: There can only be one edge in a given direction between any two nodes.
If your data has multiple distinct edges between the same nodes, use the
aggregatorargument to combine them for graph algorithms.
The most common workflow is to use the concepts and relationships you already have in your model to define a graph’s structure and then run algorithms on that structure to derive new facts that you can join back into your model for analysis.
The examples below define graph structure using small inline datasets. They belong in your model definition, not in a query script. To compute graph results, declare algorithm outputs in the model before you deploy it. Query the deployed outputs from a separate script after the refresh completes.
Understand the deployed representation
Section titled “Understand the deployed representation”A Python Graph object does not become a separate graph database or a single graph object in Snowflake.
Its node and edge concepts and relationships become regular model outputs: tables, views, or dynamic tables.
- Nodes: With
node_concept=Account, the graph reuses theAccountconcept and its deployed output. Withoutnode_concept, PyRel creates a node concept with a generated name, such asgraph1_Node. - Edges: By default, PyRel creates an edge concept with a generated name, such as
graph1_Edge. Its output includes source and destination columns, and a weight column for weighted edges. Withedge_concept=Transaction, the graph reusesTransactionand the endpoint and weight relationships you supply.
To find a particular graph’s outputs, start with its constructor in your model definition.
If it supplies node_concept or edge_concept, look for outputs named after those concepts.
Otherwise, match the generated node and edge names by their shared number, such as GRAPH1_NODE and GRAPH1_EDGE with default Snowflake naming.
The number is assigned when the Graph object is constructed, so a Python variable name such as payment_graph is not the deployed object name.
Adding or reordering graph constructors can change generated names.
If several graphs reuse the same node concept, use their edge concepts and endpoint relationships to distinguish them.
Use Monitor a deployed model to inspect the actual output objects. Internal graph helper relationships belong in the internal schema, not in application queries.
Choose a graph type
Section titled “Choose a graph type”When you create a Graph object, you must choose the type of graph you want to create.
Graphs can be:
- Directed: Each edge has an arrow from
srctodst. For example, a payment edge might point from payer → payee. - Undirected: Edges do not have arrows. You can treat a link between two nodes as going both ways.
- Weighted: Each edge has a number called a weight. For example, that weight might be payment amount or interaction frequency.
- Unweighted: Edges do not have weights. The graph only cares whether a connection exists.
Which type of graph you create is determined by the directed and weighted arguments you are required to pass when you construct the Graph object.
Use this table to help you pick which type of to create:
| What to use | When to use it |
|---|---|
Graph(m, directed=True, weighted=False) | Use when direction matters, but weights would mislead, like follower → followed relationships. |
Graph(m, directed=False, weighted=False) | Use when links are mutual and only link existence matters, like friendship or collaboration. |
Graph(m, directed=False, weighted=True) | Use when links are mutual and weights matter, like co-authorship with number of joint papers as weight. |
Graph(m, directed=True, weighted=True) | Use when direction and weights matter, like payer → payee payments with amount as weight. |
- In weighted graphs, every edge must have a weight. Graph algorithms assume weights are non-negative.
Create an undirected graph
Section titled “Create an undirected graph”Create an undirected graph by setting directed=False when you construct the Graph object:
from relationalai.semantics import Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("UndirectedGraph")
# Declare the model schema.Account = m.Concept("Account", identify_by={"id": Integer})Person = m.Concept("Person", identify_by={"id": Integer})Account.owner = m.Relationship(f"{Account} is owned by {Person}")Person.account = Account.owner.alt(f"{Person} owns {Account}")
# Define base facts.13 collapsed lines
people_data = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"},])m.define(Person.new(people_data.to_schema()))
account_data = m.data([ {"id": 101, "owner_id": 1}, {"id": 102, "owner_id": 2}, {"id": 103, "owner_id": 2},])account_owner = Person.lookup(id=account_data.owner_id)m.define(Account.new(id=account_data.id, owner=account_owner))
# Create an undirected, unweighted graph.graph = Graph(m, directed=False, weighted=False)
# Define the edges of the graph.m.define(graph.Edge.new(src=Person, dst=Person.account))Graph(m, directed=False, weighted=False)creates an undirected, unweighted graph.graph.Edge.new(src=Person, dst=Person.account)derives edges from thePerson.accountrelationship. Each edge connects aPersonnode to theAccountnode they own.
- Defining edges from a relationship like
Person.accountis a common pattern when your model already has a relationship that maps directly to the connections you want in your graph. - For other patterns for edge definition, see Define nodes with
node_concept.
Create a directed graph
Section titled “Create a directed graph”To create a directed graph, set directed=True when you construct the Graph object:
from relationalai.semantics import Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("DirectedGraph")
# Declare the model schema.Person = m.Concept("Person", identify_by={"id": Integer})Account = m.Concept("Account", identify_by={"id": Integer})Account.owner = m.Property(f"{Account} owned by {Person}")Transaction = m.Concept("Transaction", identify_by={"id": Integer})Transaction.payer = m.Property(f"{Transaction} received from {Account:payer}")Transaction.payee = m.Property(f"{Transaction} payed to {Account:payee}")
# Define base facts.22 collapsed lines
person_data = m.data([ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"},])m.define(Person.new(person_data.to_schema()))
account_data = m.data([ {"id": 1, "owner_id": 1}, {"id": 2, "owner_id": 2}, {"id": 3, "owner_id": 2},])account_owner = Person.lookup(id=account_data.owner_id)m.define(Account.new(id=account_data.id, owner=account_owner))
transaction_data = m.data([ {"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0}, {"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0}, {"id": 103, "payer_id": 2, "payee_id": 1, "amount": 150.0},])txn_payer = Account.lookup(id=transaction_data.payer_id)txn_payee = Account.lookup(id=transaction_data.payee_id)m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee))
# Create a directed, unweighted graph.graph = Graph(m, directed=True, weighted=False)
# Define the edges of the graph.m.define( graph.Edge.new(src=Transaction.payer, dst=Transaction.payee), graph.Edge.new(src=Account.owner, dst=Account),)Graph(m, directed=True, weighted=False)creates a directed, unweighted graph.- A directed graph makes sense here because payments have a clear direction from payer to payee.
graph.Edge.new(src=Transaction.payer, dst=Transaction.payee)derives edges from theTransactionconcept, connecting payer accounts to payee accounts.
Create a weighted graph
Section titled “Create a weighted graph”Use a weighted graph when edge magnitude should influence results, like payments with amount as edge weight:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("WeightedGraph")
# Declare the model schema.Person = m.Concept("Person", identify_by={"id": Integer})Account = m.Concept("Account", identify_by={"id": Integer})Account.owner = m.Property(f"{Account} owned by {Person}")Transaction = m.Concept("Transaction", identify_by={"id": Integer})Transaction.payer = m.Property(f"{Transaction} received from {Account:payer}")Transaction.payee = m.Property(f"{Transaction} payed to {Account:payee}")Transaction.amount = m.Property(f"{Transaction} has amount {Float:amount}")
# Define base facts.person_data = m.data([22 collapsed lines
{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"},])m.define(Person.new(person_data.to_schema()))
account_data = m.data([ {"id": 1, "owner_id": 1}, {"id": 2, "owner_id": 2}, {"id": 3, "owner_id": 2},])account_owner = Person.lookup(id=account_data.owner_id)m.define(Account.new(id=account_data.id, owner=account_owner))
transaction_data = m.data([ {"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0}, {"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0}, {"id": 103, "payer_id": 2, "payee_id": 3, "amount": 150.0},])txn_payer = Account.lookup(id=transaction_data.payer_id)txn_payee = Account.lookup(id=transaction_data.payee_id)m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph.graph = Graph(m, directed=True, weighted=True)
# Define the edges of the graph.m.define( graph.Edge.new( src=Transaction.payer, dst=Transaction.payee, weight=Transaction.amount ),)Graph(m, directed=True, weighted=True)enables weight-aware algorithms.- Each
Edge.new(...)call includes aweight.
- In weighted graphs, every edge must have a weight.
- Weights must be finite, non-negative floating point numbers.
- Use
floats.float()to convert from other numeric types if needed. - Graph algorithms assume weights are non-negative and finite.
Define nodes with node_concept
Section titled “Define nodes with node_concept”To include every entity instance of a concept as a node, pass the Concept object to the Graph constructor’s node_concept argument.
Then define edges between those nodes as usual.
In the following example, every Account is a node, but you only create edges for transaction with a high enough value:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("AccountsAsNodes")
# Declare the model schema.Account = m.Concept("Account", identify_by={"id": Integer})Transaction = m.Concept("Transaction", identify_by={"id": Integer})Transaction.payer = m.Property(f"{Transaction} payer is {Account:payer}")Transaction.payee = m.Property(f"{Transaction} payee is {Account:payee}")Transaction.amount = m.Property(f"{Transaction} amount is {Float:amount}")
# Define base facts14 collapsed lines
account_data = m.data([ {"id": 1}, {"id": 2}, {"id": 3},])m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([ {"id": 101, "payer_id": 1, "payee_id": 2, "amount": 50.0}, {"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},])txn_payer = Account.lookup(id=transaction_data.payer_id)txn_payee = Account.lookup(id=transaction_data.payee_id)m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Created a directed graph with nodes from the Account concept.graph = Graph(m, directed=True, weighted=False, node_concept=Account)
# Define edges for transactions with amount >= 100.0.( m.define(graph.Edge.new(src=Transaction.payer, dst=Transaction.payee)) .where(Transaction.amount >= 100.0))
assert graph.Node is Accountnode_concept=Accountmakes everyAccountinstance a node in the graph..where(Transaction.amount >= 100.0)filters whichTransactioninstances become edges.Accountentities with no high value transactions still appear in the graph as isolated nodes.
graph.Nodeis the suppliedAccountconcept, not a separate concept. Edge endpoints must be instances of that concept.graph.Node.new(...)defines additional instances ofAccountusing its identifying properties.
Define edges with edge_concept
Section titled “Define edges with edge_concept”Use this approach when your model already represents each interaction as its own entity (for example, each Transaction is an entity).
This is especially useful when edges have their own properties (like amount) and you want those properties available for weighting.
To define edges from an existing concept, set edge_concept=<YourEdgeConcept> when you construct the Graph.
You also provide relationships that map each edge entity to its endpoints.
In the following example, every Account is a node, and every Transaction is a distinct directed edge whose weight is the transaction amount:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("TransactionsAsEdges")
# Declare model schema.Account = m.Concept("Account", identify_by={"id": Integer})Transaction = m.Concept("Transaction", identify_by={"id": Integer})Transaction.payer = m.Relationship(f"{Transaction} payer is {Account}")Transaction.payee = m.Relationship(f"{Transaction} payee is {Account}")Transaction.amount = m.Property(f"{Transaction} amount is {Float:amount}")
# Define base facts.14 collapsed lines
account_data = m.data([ {"id": 1}, {"id": 2}, {"id": 3},])m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([ {"id": 101, "payer_id": 1, "payee_id": 2, "amount": 50.0}, {"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},])txn_payer = Account.lookup(id=transaction_data.payer_id)txn_payee = Account.lookup(id=transaction_data.payee_id)m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph with edges from the `Transaction` concept.graph = Graph( m, directed=True, weighted=True, node_concept=Account, edge_concept=Transaction, edge_src_relationship=Transaction.payer, edge_dst_relationship=Transaction.payee, edge_weight_relationship=Transaction.amount,)edge_concept=Transactionmakes everyTransactioninstance a distinct graph edge.edge_src_relationshipandedge_dst_relationshipmap each transaction edge to itssrcanddstproperties.edge_weight_relationship=Transaction.amountuses the transactionamountas the edge weight.
- You must pass all three of
edge_concept,edge_src_relationship, andedge_dst_relationshiptogether. edge_weight_relationshipis also required when the graph is weighted.- With
edge_concept, you do not need to define edges separately withgraph.Edge.new(...)because the graph automatically treats every instance of that concept as an edge. graph.Edgeis the supplied edge concept. To add a connection, define another instance of that concept and its endpoint relationships.- You must pass
node_conceptwhen you useedge_concept.
Define nodes explicitly with Node.new()
Section titled “Define nodes explicitly with Node.new()”You can use Node.new() to explicitly define nodes in a graph that do not come from a concept in your model.
This pattern is not common but could be useful for adding special nodes that represent entities outside of your model.
In the following example, three nodes are defined explicitly with Node.new() and connected by two edges:
from relationalai.semantics import Float, Integer, Model, Stringfrom relationalai.semantics.reasoners.graph import Graph
m = Model("ExplicitNodesAndEdges")
# Create an undirected, unweighted graph.graph = Graph(m, directed=False, weighted=False)Node, Edge = graph.Node, graph.Edge
m.define( # Define nodes for the graph n1 := Node.new(id=Integer(1)), n2 := Node.new(id=Integer(2)), n3 := Node.new(id=Integer(3)), # Define edges for the graph Edge.new(src=n1, dst=n2), Edge.new(src=n2, dst=n3),)Nodeis a concept, soNode.new()creates instances of typeNode.- Since
Nodehas no identifying properties, every argument you pass toNode.new()is used to create an identity for that node.
Handle multi-edges with aggregator
Section titled “Handle multi-edges with aggregator”A multi-edge occurs when two distinct edges share the same src and dst nodes but have different identities.
The fragments below reuse m, n1, and n2 from the preceding example.
There are two main ways this can happen:
-
In a directed graph, multiple edges between the same
srcanddstnodes are created with additional properties that differentiate them. This is most common in a weighted graph where there are multiple interactions between the same entities with different weights, like multiple transactions between the same payer and payee with different amounts:graph = Graph(m, directed=True, weighted=True)m.define(graph.Edge.new(src=n1, dst=n2, weight=10.0),graph.Edge.new(src=n1, dst=n2, weight=2.5)) -
In an undirected graph, multiple edges between the same two nodes can occur when edges are defined for both directions between the same pair of nodes:
graph = Graph(m, directed=False, weighted=False)m.define(graph.Edge.new(src=n1, dst=n2),graph.Edge.new(src=n2, dst=n1),)
Graph algorithms use a simple graph with one connection per node pair.
With the default aggregator=None, PyRel records a model validation error when graph methods consume multi-edges.
Do not rely on that error to stop computation.
If repeated connections are intentional, such as multiple transactions between the same payer and payee, explicitly collapse them into a single edge.
To collapse multi-edges, use the aggregator argument when you construct the Graph object:
from relationalai.semantics import Float, Integer, Modelfrom relationalai.semantics.reasoners.graph import Graph
m = Model("MultiEdgeGraph")
# Declare the model schema.Account = m.Concept("Account", identify_by={"id": Integer})Transaction = m.Concept("Transaction", identify_by={"id": Integer})Transaction.payer = m.Property(f"{Transaction} payed to {Account:payee}")Transaction.payee = m.Property(f"{Transaction} received from {Account:payer}")Transaction.amount = m.Property(f"{Transaction} has amount {Float:amount}")
# Define base facts.15 collapsed lines
account_data = m.data([ {"id": 1}, {"id": 2}, {"id": 3},])m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([ {"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0}, {"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0}, {"id": 103, "payer_id": 2, "payee_id": 1, "amount": 150.0},])txn_payer = Account.lookup(id=transaction_data.payer_id)txn_payee = Account.lookup(id=transaction_data.payee_id)m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph that collapses multi-edges by summing weights.graph = Graph(m, directed=True, weighted=True, aggregator="sum")
# Define the edges of the graph.m.define(graph.Edge.new(src=Transaction.payer, dst=Transaction.payee, weight=Transaction.amount))aggregator="sum"collapses multi-edges by summing their weights.- Currently,
"sum"is the only supported aggregator. - You can use
aggregator="sum"to collapse multi-edges in an unweighted graph as well.
Avoid common pitfalls
Section titled “Avoid common pitfalls”Use this table when deployed graph results are missing nodes, edges, or weights:
| Symptom | Likely cause | What to do |
|---|---|---|
num_edges is 0 | No edges were defined. | Make sure you call define() for Edge.new() declarations. If your edge definition is conditional, ensure the conditions are not filtering out all edges. If you used edge_concept, make sure instances of the concept have been defined. |
num_nodes is 0 | No nodes were defined. | If nodes were implicitly defined by edges, ensure your edge definitions do not filter out all nodes. If you used node_concept, make sure instances of the concept have been defined. |
| Weights are not showing up in edges. | You created an unweighted graph. | Make sure you set weighted=True when you construct the Graph object. |
For a deployed model, apply corrections in the model definition, redeploy, and check that the refresh completes before querying again. See Deploy a model for the deployment workflow.