PathPattern
PathPattern( *args: Ref | Concept | Chain | PathPattern | Relationship | Union, model=None)A reusable specification of a pattern that can match paths through a knowledge graph.
Construct a PathPattern with Model.path
(or its standalone convenience form path), optionally refine it with
PathPattern.where, repeat it with PathPattern.repeat, or both
— in that order, since a repeat is a unification boundary — then either call
PathPattern.exists to check for the existence of any matching paths,
or call a selector (PathPattern.all, PathPattern.any,
PathPattern.any_shortest) followed by .paths() to enumerate paths
or .count() to count paths.
Positional arguments are classified by type:
- Edges — a
Chainor aRelationship. - Nodes — a
Conceptor aRef. - Unions of nodes: a
Unionwhose members are all aReforConcept, potentially nested in otherUnions. - Nested sub-paths — a
PathPattern.
Path pattern arguments are joined via path-join semantics: the last field of an
argument unifies with the first slot of the next argument. For example, if A,
B, and C are nodes and r1 and r2 are edges, then
m.path(A, r1, B)implies thatA == r1[0]andr1[-1] == B,m.path(A, B, C)implies thatA == B == C, andm.path(r1, r2)implies thatr1[-1] == r2[0].
All node identifiers before the first edge of a path pattern explicitly match the initial node of the path. Similarly, all node identifiers after the final edge match the final node of the matched path. These are called endpoint identifiers, and if a path pattern lacks explicit endpoint identifiers, the endpoint(s) are implicit, since all matched paths start from an initial node and end at a final node.
Only endpoint identifiers may be referenced outside the path pattern, e.g.,
m.where(m.path(initial_node, Person.follows.repeat(1, 3), final_node).all().paths()).select(initial_node, final_node).
All other node identifiers are referred to as interior nodes and are not yet
accessible outside the path pattern. There are two exceptions to the above rule - the
following are endpoints, but CANNOT be referenced outside of the pattern:
- Endpoints that are inside the scope of a
repeat: neitherm.where(m.path(A, Person.follows).repeat(2).all().paths()).select(A)norm.where(m.path(m.path(A, Person.follows).repeat(2)).all().paths()).select(A)are allowed. - Endpoints that are inside a Union:
m.where(m.path(m.union(A, B), C, Person.follows).all().paths()).select(A)is not allowed, but selectingCis allowed.
All occurrences of the same Concept within a
single m.path(...) refer to the same variable, just like elsewhere in PyRel. Reusing a Concept or Ref
at more than one node position is rejected with an error. For example,
m.path(Person.follows, Person.works_with) is invalid because the concept Person
appears in two node positions, and x = Person.ref(); m.path(x.follows, x.works_with)
is invalid because the reference x appears in two node positions. If the intent
is to keep node positions independent, use distinct Refs:
x, y = Person.ref(), Person.ref(); m.path(x.follows, y.works_with).
Important: The following two repeat forms have different semantics:
m.path(C.r.repeat(N))— repeats the edgeronly. Note that only the first node must be of typeC; intermediate nodes are not constrained to typeC.m.path(C.r).repeat(N)— repeats the patternC.r: every intermediate node must be of typeC.
Parameters
(*argsRef|Concept|Chain|PathPattern|Relationship|Union, default:()) - One or more arguments defining the path pattern to be matched. Each argument may be a node (ConceptorRef), an edge (ChainorRelationship), or a nested sub-path (PathPattern).
(modelModel, default:None) - TheModelthis path pattern belongs to. When omitted, the active model is resolved automatically, which errors if multiple models are defined in this Python process.
Examples
Person = m.Concept("Person")source, destination = Person.ref(), Person.ref()m.path(Person.relationship)m.path(source.relationship)m.path(source.relationship, destination)m.path(source.relationship.repeat(1, 10))m.path(source.relationship.repeat(1, 10), destination)m.path(Person.relationship).repeat(3)m.path(Person.follows, path(Employee.follows).repeat(min=3, max=5))Notes
Most users should not instantiate this class directly. Prefer
Model.path (or its standalone
convenience form path).
Methods
.where()
PathPattern.where(*args: Expression | Match | Not) -> PathPatternAttach filters and constraints to this path pattern.
We currently support only local filters, which are those that reference a single node or the endpoints of a single edge. They are routed onto that node or hop and are applied during traversal, pruning the search space. Non-local filters (also called cross-hop filters) are those that are not local to a single node or a single edge’s endpoints, and are currently unsupported except for those that reference only the path’s endpoints. That special case is allowed, but the filter is applied as a post-filter after all matching paths are enumerated and does not prune the traversal.
Filters defined here (called attached filters) differ from those placed in an
enclosing Model.where (called enclosing
filters) in two primary ways:
- because only the path’s endpoints unify with the enclosing scope, attached filters can reference interior nodes, whereas enclosing filters cannot.
- attached filters are applied during traversal, which more efficiently prunes the traversal space, whereas enclosing filters are applied after enumeration.
Important: If this pattern is repeated, the filter must be attached before
PathPattern.repeat, not after. A repeat is a unification boundary, so
path(...).where(...).repeat(...) is the only spelling that attaches a filter to
a repeated pattern; the reverse order raises.
Parameters:
(*argsExpression|Match|Not, default:()) - Zero or more of the following constraints on a single node, a single edge’s endpoints, or the pattern’s endpoints only: a booleanExpression, aMatch(a | b), or aNot(not_(...)).
Returns:
PathPattern- This pattern (self), to allow chaining.
Raises:
relationalai.util.error.RAIException-PathFilterAfterRepeatErrorifPathPattern.repeathas already been applied to this pattern. Attach the filter before the repeat instead.
Examples:
# Constrain the initial node.m.path(x, Person.follows.repeat(1, 3)).where(x.name == "Alice")# Constrain an interior node's concept.m.path(x.follows, y.follows).where(Criminal(y))# Constrain the endpoints of a single edge.m.path(x.follows, y.follows).where(x.age < y.age)# A cross-hop filter acting only on endpoints (applied after enumeration).m.path(x.follows.repeat(1, 10), y).where(x.age < y.age)
# PathInternalError: a cross-hop filter that references an interior node ``z``.m.path(x.follows, y.follows, z.follows).where(x.age < z.age)
# PathFilterAfterRepeatError: `.where` after `.repeat` on the same pattern.m.path(Person.follows).repeat(2).where(Person.age >= 18)# Instead, attach the filter before the repeat.m.path(Person.follows).where(Person.age >= 18).repeat(2).repeat()
PathPattern.repeat( min_or_exact: int | None = None, max: int | None = None, *, min: int | None = None) -> PathPatternMatch the specified number of repetitions of this pattern.
Repeats the whole path pattern: For example, given a concept Person with
property follows, m.path(Person.follows).repeat(3) repeats the pattern
Person.follows three times with independent Person instances, effectively
m.path(Person.ref().follows, Person.ref().follows, Person.ref().follows).
Note that the whole pattern is repeated, including its start: For example, in
m.path(Criminal.follows).repeat(2), the start Criminal repeats together
with the edge follows, effectively m.path(Criminal.ref().follows, Criminal.ref().follows)
— so every intermediate node must be of type Criminal. See
Chain.repeat for how this differs from a repeated
chain.
Important: Note that no node identifier may be used both inside and outside a
repeated pattern, due to ambiguity of which repetition the identifier refers to.
For example, the following are disallowed due to Criminal appearing both inside
and outside the repeated pattern:
>>> m.where(
… p := m.path(Criminal.follows).repeat(2,5).all().paths(),
… Criminal.age > 18
… ).select(p, p.nodes[“index”], Person(p.nodes).name)
>>> m.where( … p := m.path(Criminal.follows).repeat(2,5).all().paths(), … ).select(Criminal)
>>> repeated_pattern = m.path(Criminal.follows).repeat(2,5) >>> m.where( … p := m.path(repeated_pattern).where(Criminal.age > 18).all().paths() … ).select(p, p.nodes[“index”], Person(p.nodes).name)
Referencing a node identifier in the where attached to the repeated pattern is allowed and refers to every repetition (so this example matches chains of Criminals who are all above age 18): >>> m.where( … p := m.path(Criminal.follows).where(Criminal.age > 18).repeat(2,5) … ).select(p, p.nodes[“index”], Person(p.nodes).name)
You may reference the initial endpoint of a repeated pattern’s first repetition or the final endpoint of its last repetition by introducing a node identifier before or after the repeated pattern, respectively: >>> A, B = Criminal.ref(), Criminal.ref() >>> m.where( … p := m.path(A, m.path(Criminal.follows).repeat(2,5), B).all().paths() … ).select(A, B)
An explicit finite upper bound is required; unbounded repeats are not currently supported.
The upper bound must be at least 1. A repeat that permits only zero repetitions matches no occurrences, so it adds nothing to the path; write the path without the repeated element instead. A zero minimum paired with a non-zero maximum is fine, and admits the zero-repetition case alongside the others.
Nesting repeats is not supported and will raise a PathAPINotImplemented.
A PathPattern.where may not be attached after a repeat; attach it before, where
it applies to every repetition. The reverse order raises a
PathFilterAfterRepeatError.
A pattern may be repeated at most once. A second repeat would repeat an
already-repeated pattern — a nested repeat, which is not supported — so it
raises a PathRepeatAppliedTwiceError. Write a single repeat with the
bounds you want.
Parameters:
(min_or_exactint, default:None) - When it is the only argument, the exact number of repetitions. Otherwise the lower bound (paired withmax).
(maxint, default:None) - The inclusive upper bound on the number of repetitions.
(minint, default:None) - Keyword-only alternative to the positional lower bound. Cannot be combined with a positionalmin_or_exact.
Returns:
PathPattern- This pattern (self), to allow chaining.
Raises:
relationalai.util.error.RAIException-PathRepeatAppliedTwiceErrorif this pattern has already been repeated.ValueError- If no argument is given; if a bound is negative; ifmin > max; if the maximum is zero (however spelled); if only one ofmin/maxis supplied; or if both a positional min and themin=keyword are given.
Examples:
pattern.repeat(3) # exactly 3 times -> (3, 3)pattern.repeat(1, 20) # between 1 and 20 -> (1, 20)pattern.repeat(min=1, max=20) # keyword form -> (1, 20)pattern.repeat(0, 3) # zero to 3 times -> (0, 3)pattern.repeat(0) # ValueError: a count of at least 1 is requiredpattern.repeat(0, 0) # ValueError: a max of at least 1 is requiredpattern.repeat(max=20) # ValueError: an explicit min is requiredpattern.repeat(min=1) # ValueError: an explicit max is requiredpattern.repeat(2).repeat(3) # PathRepeatAppliedTwiceError: at most one repeat.all()
PathPattern.all() -> AllPathsSelect every path matching this pattern.
Follow it with one of the following methods to return information about the kept paths:
.paths()— onePathTraversalper path..count()— how many paths. Not implemented yet.
Returns:
AllPaths- Can be chained with.paths()or.count()to return results.
Examples:
m.path(Person.follows.repeat(1, 3)).all().paths()Notes:
PERFORMANCE WARNING: Returning all .paths() can be exponentially large in the size of the graph. This
can produce a lot of data, and can take a long time, so make sure to only actually materialize
all().paths() if your pattern carefully restricts down the data size, or if you are okay producing
a large result.
.exists()
PathPattern.exists() -> FragmentWhether a path matching this pattern exists.
It computes which (initial_node, final_node) endpoint pairs are connected by at least
one matching path. The pattern’s endpoints unify with the enclosing scope
exactly as with .all().paths(). The result is a condition (“(initial_node, final_node)
is a connected pair”), a where() Fragment
usable in two ways:
- as a filter within
Model.where: for example,m.where(m.path(initial_node, ..., final_node).exists()).select(initial_node, final_node)returns only the connected pairs of endpoints. - as a boolean value within
Model.select, by callingVariable.as_boolon it: for example,m.select(m.path(initial_node, ..., final_node).exists().as_bool()[, initial_node][, final_node])returnsTrue/Falsevalues indicating whether there exists a matching path.
In the latter (boolean values) case, if neither initial_node nor final_node are selected,
the result is a single True/False value. If initial_node is part of the select
statement, it will return one tuple per initial node, and similarly for final_node and
initial_node, final_node combinations. In this case, the .as_bool() call is required;
selecting PathPattern.exists() directly results in an Invalid Expression
error.
Returns:
Fragment- Awhere()fragment carrying the connectivity condition. Pass it toModel.whereto filter, or callVariable.as_boolon it to selectTrue/Falsevalues.
Examples:
List connected pairs:
A, Z = Person.ref(), Person.ref()pattern = m.path(A, Person.follows.repeat(1, 5), Z)m.where(pattern.exists()).select(A.name, Z.name)Connectivity filter (filter on the initial node and output the final node):
m.where( pattern.exists(), A.name == "Alice",).select(Z.name)Connectivity as True/False values:
m.select( A.name, Z.name, pattern.exists().as_bool(),)Deriving a relationship:
Reaches = m.Relationship(f"{Person:src} reaches {Person:dst}")m.where(pattern.exists()).define(Reaches(A, Z))Composed with attached filters, same as .all():
pattern_adult_filter = pattern.where(A.age >= 18)m.where(pattern_adult_filter.exists()).select(A.name, Z.name)Notes:
Only the path’s initial_node and final_node unify with the enclosing scope; filters
on interior nodes must be attached with PathPattern.where on the
pattern, exactly as with .all().paths().
Performance: existence is computed without enumerating paths. For a given
PathPattern, this is possible in time linear in the size of the graph and
in the number of initial nodes: O(|G| * |initial_nodes|), whereas .all().paths()
is worst-case exponential. If you only need to know whether or which
endpoints connect, not the paths themselves, always prefer .exists().
.any()
PathPattern.any() -> AnyPathSelect any one path matching this pattern, per group.
Say how to group matching paths with .per(...) (required; see
AnyPath.per), then follow it with .paths() to get one
PathTraversal per group. When a group
has more than one matching path, which one it yields is arbitrary.
Returns:
AnyPath- The selector; call.per(...)on it, then.paths().
Examples:
reached_person = Person.ref()m.path(Person.follows.repeat(1, 5), reached_person).any().per( Person, reached_person).paths().any_shortest()
PathPattern.any_shortest() -> AnyShortestPathSelect any one shortest path matching this pattern, per group.
Say how to group matching paths with .per(...) (required; see
AnyShortestPath.per), then follow it with .paths() to get one
PathTraversal per group. When a group has
more than one shortest path, which one it yields is arbitrary.
Returns:
AnyShortestPath- The selector; call.per(...)on it, then.paths().
Examples:
reached_person = Person.ref()m.path(Person.follows.repeat(1, 5), reached_person).any_shortest().per( Person, reached_person).paths().all_paths()
PathPattern.all_paths() -> DerivedColumnEnumerate every path matching this pattern.
.. deprecated:
Write ``.all().paths()`` instead. This spelling warns on every call and willbe removed.Returns:
DerivedColumn- What.all().paths()returns.