Use path results
Turn a path pattern into results your model can use: which endpoints are connected, how many paths join them, and the paths themselves. This guide uses a payments model to define which accounts can reach each other, then shows how to query the results after deployment. It also lists the result methods, selectors, and result types with links to their API reference pages.
- Prerequisites
- You know how to write a path pattern. See Create a path pattern.
- You have access to a Snowflake environment where you can deploy a model and query its output.
Use a path result
Section titled “Use a path result”A path pattern describes which paths match.
.exists() on the pattern tells you whether any path matches.
For anything else, a selector says which of the matching paths to keep, and a method on the selector says what to return about them.
You can define the result as a relationship in your model, deploy the model, and query the relationship like any other output.
In the following example, .exists() is used to record which accounts can reach each other through a path of up to four payments.
The model has five accounts.
Account A pays B and C, both of which pay D, and D pays E.
-
Define the model and the path result.
Consider the following model of accounts and transfers:
from relationalai.semantics import Integer, Model, Stringm = Model("Payments")Account = m.Concept("Account", identify_by={"name": String})Transfer = m.Concept("Transfer", identify_by={"id": Integer})Account.sent_to = m.Relationship(f"{Account:src} transferred (via {Transfer:transfer}) to {Account:dst}")10 collapsed lines# Base facts: A -> B -> D, A -> C -> D, D -> Etransfers = [(1, "A", "B"), (2, "B", "D"), (3, "A", "C"), (4, "C", "D"), (5, "D", "E")]for transfer_id, src, dst in transfers:m.define(Account.sent_to(Account.new(name=src),Transfer.new(id=transfer_id),Account.new(name=dst),))Account.reaches = m.Relationship(f"{Account:src} reaches {Account:dst}")initial_acct, final_acct = Account.ref(), Account.ref()reachable = m.path(initial_acct, Account.sent_to.repeat(1, 4), final_acct)m.where(reachable.exists()).define(Account.reaches(initial_acct, final_acct))reachablematches paths of one to foursent_tohops, frominitial_accttofinal_acct.reachable.exists()is a condition that holds for every(initial_acct, final_acct)pair joined by at least one matching path. It is computed without enumerating the paths.- The final rule records those pairs in
Account.reaches. After deployment, this relationship is theACCOUNT_REACHESoutput withSRCandDSTcolumns.
-
Query the reachable pairs.
After you have deployed the model, wait for the refresh to finish, then query the result. Choose a tab. Both tabs read the same deployed output.
- Path results are computed on the logic reasoner, not in SQL. The refresh creates or resumes the configured reasoner when it needs it, so no reasoner setting is required. See How logic reasoners are created.
- The first refresh after you add a path pattern can take longer while the reasoner starts. Later refreshes queue a job on it.
Put the query in a separate script that imports the model:
find_reachable_accounts.py from model import Account, msrc, dst = Account.ref(), Account.ref()m.where(Account.reaches(src, dst)).select(src.name.alias("src"), dst.name.alias("dst")).inspect()Output src dst0 A B1 A C2 A D3 A E4 B D5 B E6 C D7 C E8 D EReplace
MY_DATABASE.MY_MODELwith your deployment schema, then run:SELECT SRC, DSTFROM MY_DATABASE.MY_MODEL.ACCOUNT_REACHESORDER BY SRC, DST;Output SRC DSTA BA CA DA EB DB EC DC ED EAreaches every other account.BandCreachDandE, but not each other.Emakes no payments, so it reaches nothing.
Choose what to return
Section titled “Choose what to return”A path query has up to three parts: the pattern says which paths match, the selector says which matching paths to keep, and a method on the selector says what to return for the kept paths.
If you only need to know whether a path exists, call .exists() directly on the pattern; it needs no selector.
Selectors that keep a reduced set of paths per group also take .per(...), which says how to group the paths.
| Method | Keeps | Then call |
|---|---|---|
.exists() | Nothing: a condition that a matching path connects the endpoints | Use it in where(), or call .as_bool() on it. |
.any_shortest() | One shortest path per group | .per(initial_node, final_node).paths() |
.any() | One path per group | .per(initial_node, final_node).paths() |
.all_shortest() | Every shortest path per group | .per(initial_node, final_node).paths() or .per(initial_node, final_node).count() |
.all() | Every matching path | .paths() or .count() |
The only grouping currently supported is the pattern’s endpoint pair, written .per(initial_node, final_node) with the initial and final node references in that order.
Any other grouping raises PathSelectorPerError.
.all() keeps every path, so it has no .per(...).
The sections below use the payments model from Use a path result and the same reachable pattern.
Check which endpoints are connected
Section titled “Check which endpoints are connected”.exists() returns a condition: a matching path connects the pattern’s initial node to its final node.
It is the cheapest result, because it never lists paths, so prefer it whenever you only need to know whether or which endpoints are connected.
Use the condition as a filter in where(), as the walkthrough above does, or call .as_bool() on it to get a True or False value.
Which endpoints you select decides what the condition is checked for.
The following queries check it from each initial node to each final node, from each initial node to any final node, from any initial node to each final node, and from any initial node to any final node, respectively:
from model import final_acct, initial_acct, m, reachable
# Per pair: the connected (initial_acct, final_acct) pairs.m.where(reachable.exists()).select(initial_acct.name.alias("initial_acct"), final_acct.name.alias("final_acct")).inspect()
# Per initial node: whether each account reaches any account.m.select(initial_acct.name.alias("initial_acct"), reachable.exists().as_bool().alias("reaches_any")).inspect()
# Per final node: whether any account reaches each account.m.select(final_acct.name.alias("final_acct"), reachable.exists().as_bool().alias("reached_by_any")).inspect()
# Overall: whether any matching path exists at all.m.select(reachable.exists().as_bool().alias("any_path")).inspect() initial_acct final_acct0 A B1 A C...8 D E
initial_acct reaches_any0 A True1 B True2 C True3 D True4 E False
final_acct reached_by_any0 A False1 B True2 C True3 D True4 E True
any_path0 True- The per-pair query lists the nine connected pairs, the same ones
Account.reachesholds. Emakes no payments, so it reaches no account.Areceives none, so no account reaches it.- With neither endpoint selected, the condition collapses to one value: at least one matching path exists.
Keep one path per pair
Section titled “Keep one path per pair”.any_shortest() keeps one shortest path for each connected pair of endpoints.
.per(initial_node, final_node) is required and defines the pairs.
When a pair has several shortest paths, which one is kept is arbitrary.
The following example stores one path per pair:
from relationalai.semantics.std.path import PathTraversal
ShortestPath = m.Property(f"{Account:src} reaches {Account:dst} by {PathTraversal:path}")
p = reachable.any_shortest().per(initial_acct, final_acct).paths()m.where(p).define(ShortestPath(initial_acct, final_acct, p))pis onePathTraversalvalue per connected pair: a unique identifier for the kept path.ShortestPathis aProperty, because each pair determines exactly one kept path. It stores the pair together with its path, so later rules and queries can read the path’s hops. See Work withPathTraversalresults.
.any() has the same shape and keeps one matching path per pair, not necessarily a shortest one.
Today it returns the same path as .any_shortest(); a future version may return a possibly longer path, faster.
Count paths without enumerating them
Section titled “Count paths without enumerating them”.all().count() returns the number of matching paths, and .all_shortest().per(initial_node, final_node).count() returns the number of shortest paths per pair.
Both are computed without listing the paths, so they stay cheap even when the number of paths is exponential.
The count is an Aggregate.
Used bare, it is one global total.
Group it where you use it by applying .per(...) to the result, as with any other PyRel aggregate:
Account.num_shortest_paths = m.Property( f"{Account:src} reaches {Account:dst} by {Integer:n} shortest paths")
n = reachable.all_shortest().per(initial_acct, final_acct).count()m.where(c := n.per(initial_acct, final_acct)).define(Account.num_shortest_paths(initial_acct, final_acct, c))- The selector’s
.per(initial_acct, final_acct)decides which paths count as shortest: the shortest ones for each pair. - The result’s
.per(initial_acct, final_acct)decides how the count is grouped: one count per pair, sonum_shortest_pathsis aProperty.n.per(final_acct)would instead sum the counts for each final node. AreachesDby two shortest paths, throughBand throughC, so the count for pair(A, D)is 2.(A, E)also has a count of 2, throughBandDor throughCandD. Every other reachable pair has a count of 1.
- A pair with no matching path gets no count, not 0.
If you select the pair together with the count, the count shows as missing.
Use
| 0to get 0 instead. - Counting needs an unambiguous pattern: a pattern with only one way to match each path.
An ambiguous pattern, like
m.path(x.sent_to.repeat(0, 1), y.sent_to.repeat(0, 1)), raisesPathNonDeterminismError. - Every walk within the repeat bounds is counted, including walks around cycles.
Enumerate paths
Section titled “Enumerate paths”.all().paths() returns one PathTraversal per matching path, and .all_shortest().per(initial_node, final_node).paths() returns one per shortest path.
The following example stores every path:
AccountPath = m.Relationship(f"{Account:src} reaches {Account:dst} by {PathTraversal:path}")
p = reachable.all().paths()m.where(p).define(AccountPath(initial_acct, final_acct, p))Work with PathTraversal results
Section titled “Work with PathTraversal results”A PathTraversal is a value that identifies one path.
Read its details through its properties:
| Property | Fields | Meaning |
|---|---|---|
p.length | Integer | The number of hops in the path. |
p.nodes | index, concept | The entity at each position. Index 0 is the initial entity and index p.length is the final entity. |
p.relationships | index, relationship | The name of the relationship traversed at each hop, like "Account.sent_to". |
p.relationship_fields | index, field_index, field | The entity-typed fields of a relationship with more than two fields, by hop and field position. |
The relationship at hop i goes from the entity p.nodes(i) to the entity p.nodes(i+1).
p.nodes is typed as any entity, so cast it to a concept to read properties: Account(p.nodes).name.
These properties live in the model’s internal schema, not in the deployed public schema.
A relationship that stores PathTraversal values, like ShortestPath above, deploys as an output whose path column holds an opaque identifier.
To use the hops after deployment, either read them through PyRel or copy them into a relationship of your own.
Both work; pick the one that suits how you query.
Read the hops through PyRel. Query the stored paths from a script that imports the model:
from model import Account, ShortestPath, mfrom relationalai.semantics.std.path import PathTraversal
src, dst, p = Account.ref(), Account.ref(), PathTraversal.ref()m.where( ShortestPath(src, dst, p)).select( src.name.alias("src"), dst.name.alias("dst"), p.length, p.nodes["index"], Account(p.nodes).name.alias("acct_name"),).inspect() src dst length index acct_name0 A B 1 0 A1 A B 1 1 B2 A C 1 0 A3 A C 1 1 C4 A D 2 0 A5 A D 2 1 C6 A D 2 2 D...Copy the hops into your own relationship. Define a relationship with the fields you need, so they become columns of a deployed output:
PathHop = m.Property(f"{PathTraversal:path} at hop {Integer:index} visits {Account:account}")
p = reachable.any_shortest().per(initial_acct, final_acct).paths()m.where(p).define(PathHop(p, p.nodes["index"], Account(p.nodes)))After deployment, PATH_HOP has PATH, INDEX, and ACCOUNT columns, and ACCOUNT holds the account’s name.
- Cast
p.nodesto the concept you expect, as inAccount(p.nodes), when you copy it. Copyingp.nodes["concept"]directly stores the entity’s internal identifier instead of its key. - For a relationship with more than two fields,
p.relationship_fieldsholds the fields between the source and the destination. In the payments model,Transfer(p.relationship_fields).idgives the transfer used at each hop, withp.relationship_fields["index"]as the hop index.p.relationship_fields["field_index"]is1because the transfer occupies the position 1 field insent_to.
Keep path queries fast
Section titled “Keep path queries fast”- Ask for the least you need.
.exists(),.any_shortest(), and.count()all run in time polynomial in the size of your data..all().paths()and.all_shortest().per(...).paths()can be exponential. - Count with
.count(), not by counting enumerated paths.count(pattern.all().paths())is significantly less efficient thanpattern.all().count(). - Attach filters to the pattern. A
.where()on the pattern that constrains one node or one edge prunes paths during traversal. The same filter in an enclosingm.where(...)is applied after the paths are found. Naming an endpoint with a subconcept reference, likeFlagged.ref(), is the cheapest filter of all. - Watch for cycles. Paths are walks, so a cycle in the data multiplies the number of paths.
Use
.all().count()to check the size before enumerating, or check the structure with the graph library’sis_acyclic().
Available API
Section titled “Available API”The following tables list the path library’s public API. Each entry links to its API reference page.
Patterns
Section titled “Patterns”| Method | Description |
|---|---|
m.path(...) | Creates a PathPattern from nodes, edges, and sub-patterns. |
path(...) | Standalone form of m.path(...) that infers the active model. |
.where(...) | Attaches filters to the pattern. Call it before .repeat(). |
.repeat(...) | Repeats the whole pattern a fixed number of times or within a range. |
.exists() | A boolean condition representing existence of a matching path. Needs no selector. |
Chain.repeat(...) | Repeats a single edge inside a pattern. |
Selectors
Section titled “Selectors”| Method | Description |
|---|---|
.all() | Keeps every matching path. Returns an AllPaths selector with .paths() and .count() methods. |
.any() | Keeps one path per group; requires .per(...) to define the groups. Returns an AnyPath selector with .paths() method. |
.any_shortest() | Keeps one shortest path per group; requires .per(...) to define the groups. Returns an AnyShortestPath selector with .paths() method. |
.all_shortest() | Keeps every shortest path per group; requires .per(...) to define the groups. Returns an AllShortestPaths selector with .paths() and .count() methods. |
Results
Section titled “Results”| Name | Description |
|---|---|
PathTraversal | One enumerated path, with length, nodes, relationships, and relationship_fields properties. |