Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
4ae827a
filter expr impl
shivamka1 Jun 4, 2026
4460d19
split NodePropOp into NodePropOp and NodeMetaOp, remove is_metadata flag
shivamka1 Jun 4, 2026
9d4691c
remove NodeTypeStringOp, use ArcStr directly for node type comparisons
shivamka1 Jun 4, 2026
793b0e6
ref
shivamka1 Jun 5, 2026
c3c3da7
split CompositeNodeFilter::Node into typed Id/Name/Type variants, rem…
shivamka1 Jun 5, 2026
9889d5f
replace NodeIdFilterBuilder/InternalNodeIdFilterBuilder/NodeIdFilterO…
shivamka1 Jun 5, 2026
454e3d1
implement InternalNodeFilterBuilder on Name and Type directly, replac…
shivamka1 Jun 5, 2026
142699f
implement InternalNodeFilterBuilder on Name and Type directly, replac…
shivamka1 Jun 5, 2026
af438c1
add comment
shivamka1 Jun 5, 2026
18eaec7
some experiments
ljeub-pometry Jun 5, 2026
4659ba2
add temporal property NodeExpr with any/all quantifiers and aggregato…
shivamka1 Jun 8, 2026
6e853bb
Merge branch 'filter_expr' of github.com:Pometry/Raphtory into filter…
shivamka1 Jun 8, 2026
16e6304
split QuantifiedNodeOp into AnyNodeOp/AllNodeOp, fix post-merge build…
shivamka1 Jun 8, 2026
b09096a
start reworking some bits
ljeub-pometry Jun 8, 2026
9a1381f
break more things
ljeub-pometry Jun 9, 2026
e5ff97d
make things
shivamka1 Jun 9, 2026
fd45acf
start introducing prop_type for handling validation
ljeub-pometry Jun 9, 2026
fedf488
reorganise node_expr into exprs/ops/filters, drop NoWrap, document th…
shivamka1 Jun 10, 2026
d2cbd57
add is_true() / is_false() convenience filters for boolean properties
shivamka1 Jun 10, 2026
66ef4d1
rename TemporalPropContext/QuantifiedContextBuilder/NodeExprContextBu…
shivamka1 Jun 10, 2026
d74efe2
add NodeFilter::name/id/node_type associated fns and export NodeExprF…
shivamka1 Jun 11, 2026
4c02e77
implement InternalPropertyFilterBuilder for PropertyExpr/MetadataExpr…
shivamka1 Jun 11, 2026
54747e5
remove TemporalNodeExprBuilderOps and temporal_property shortcut — us…
shivamka1 Jun 11, 2026
ac0f1f6
impl edge sides of things
shivamka1 Jun 15, 2026
d58cdc5
merge from db_v4
shivamka1 Jun 15, 2026
02bb984
output type is now option of prop, ref
shivamka1 Jun 15, 2026
4a000c8
fix
shivamka1 Jun 15, 2026
30e4d7f
ref
shivamka1 Jun 15, 2026
0277285
refactor(filter): unify NodeExpr output to Option<Prop>, replace quan…
shivamka1 Jun 15, 2026
6678607
more changes
shivamka1 Jun 16, 2026
89b8c38
fix ops
shivamka1 Jun 16, 2026
998e526
experimenting
shivamka1 Jun 16, 2026
9048bbd
fix the other issues
ljeub-pometry Jun 16, 2026
49f6b94
add markers
ljeub-pometry Jun 16, 2026
ba90796
impl entity marker and rid duplications
shivamka1 Jun 17, 2026
49acda0
fix tests
shivamka1 Jun 17, 2026
5ea726e
fix tests
shivamka1 Jun 17, 2026
7061d8b
fix tests
shivamka1 Jun 17, 2026
ea7f63d
fix tests
shivamka1 Jun 17, 2026
eb2f921
remove degree temporal aggregation entries now caught at compile time
shivamka1 Jun 17, 2026
95b2b88
fix ordering of filters
shivamka1 Jun 17, 2026
a81ee5d
fix tests
shivamka1 Jun 17, 2026
58688de
add EntityExpr/NodeExpr/EdgeExpr impls for AndFilter/OrFilter to enab…
shivamka1 Jun 17, 2026
f275608
fix exploded edgefilter
shivamka1 Jun 17, 2026
9d2ee27
fix: complete type-safe filter API migration - EntityAggOps, EdgeEndp…
shivamka1 Jun 17, 2026
6629d70
fix tests
shivamka1 Jun 18, 2026
a611bc3
reorder quantifier chains and make broadcast_binary recursive to fix…
shivamka1 Jun 18, 2026
3ed6c10
use Prop::try_cast for cross-type numeric comparisons in filters
shivamka1 Jun 18, 2026
8df0447
fix: apply view transforms (window/latest/snapshot/layer) before dele…
shivamka1 Jun 19, 2026
a5b27cb
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 19, 2026
5c39363
fix tests
shivamka1 Jun 19, 2026
4a5c804
reorder temporal quantifier chains: comparator before quantifier; res…
shivamka1 Jun 19, 2026
a571fbf
rewrite tests
shivamka1 Jun 19, 2026
4d8c56f
fix tests
shivamka1 Jun 21, 2026
bff4ce7
fix test
shivamka1 Jun 21, 2026
a19e539
fix tests
shivamka1 Jun 21, 2026
ca43cd4
fix tests
shivamka1 Jun 21, 2026
0383aa0
fix tests
shivamka1 Jun 22, 2026
969b9b9
cleanup: stale comments, dead code, get_prop_id, drop static NodeFilt…
shivamka1 Jun 22, 2026
c2180d1
rename BinaryCmpFilter/StringFilter/UnaryFilter/PropValueSetFilter to…
shivamka1 Jun 22, 2026
b54460a
unify TemporalProp into TemporalExpr; delete TemporalPropOps shim trait
shivamka1 Jun 22, 2026
a68023b
And/Or bool ops broadcast over Prop::List inputs
shivamka1 Jun 23, 2026
6572425
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 23, 2026
762038d
ref
shivamka1 Jun 23, 2026
baee2e3
rid search tests rust
shivamka1 Jun 23, 2026
67ec8a6
rid search tests py
shivamka1 Jun 23, 2026
db868ba
rid search tests gql
shivamka1 Jun 23, 2026
5b51c5d
impl. py expr
shivamka1 Jun 24, 2026
3322b3a
impl py expr
shivamka1 Jun 24, 2026
4dea8f8
ref
shivamka1 Jun 24, 2026
b305f1b
ref
shivamka1 Jun 24, 2026
a95cb11
composable filters need not be exprs
shivamka1 Jun 24, 2026
b53c473
ref
shivamka1 Jun 24, 2026
7d3172c
expose EntityExprFilterOps surface on PyExpr
shivamka1 Jun 24, 2026
1207c20
expose aggregator methods on PyExpr
shivamka1 Jun 24, 2026
7b464ee
wip
shivamka1 Jun 25, 2026
46af857
impl missing traits
shivamka1 Jun 25, 2026
6a26411
ref
shivamka1 Jun 25, 2026
089eff5
impl CreateOp for UnaryExpr and missing ExplodedEdgeFilter variants
shivamka1 Jun 25, 2026
51ecb59
fix error messages
shivamka1 Jun 25, 2026
08d7bfc
align CreateOp/error-code dispatch for EntityMarker variants
shivamka1 Jun 25, 2026
7138962
fix node filter Python wrappers: add From<Arc<dyn DynNodeFilterFactor…
shivamka1 Jun 25, 2026
9f45b9d
mirror DynNodeFilterFactory pattern on edge side: add DynEdgeFilterFa…
shivamka1 Jun 25, 2026
36c77b7
relax CreateOp bound on PropertyExpr/MetadataExpr to allow edge-side use
shivamka1 Jun 25, 2026
e694c0e
add with_entity to BinaryCmpExpr and simplify EntityMarker dispatcher…
shivamka1 Jun 25, 2026
01fdbe4
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 25, 2026
b26c29f
drop search_nodes/search_edges from Python API and remove composite-p…
shivamka1 Jun 25, 2026
29cdf28
delete composite-path test invocations and trait bounds from raphtory…
shivamka1 Jun 25, 2026
080da9c
drop remaining TryAsCompositeFilter trait bounds and imports from tes…
shivamka1 Jun 25, 2026
1cf86fb
remove gql search apis
shivamka1 Jun 25, 2026
274d04f
fix isactivenode
shivamka1 Jun 26, 2026
7d89dda
break recursive Window bound on Dyn*FilterFactory blanket impls via g…
shivamka1 Jun 26, 2026
6c864cd
fix isactivenode
shivamka1 Jun 26, 2026
bb67535
add prop-type aware validation for IsActiveNode: prop_type/nullable o…
shivamka1 Jun 26, 2026
d29a721
migrate structural edge predicates (IsActive/IsValid/IsDeleted/IsSelf…
shivamka1 Jun 26, 2026
ca60173
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 26, 2026
bcbc358
ref
shivamka1 Jun 26, 2026
288bdbb
fix bounds
shivamka1 Jun 26, 2026
0ef95b0
add EntityExpr + CreateOp impls to IsActive/IsValid/IsDeleted/IsSelfL…
shivamka1 Jun 26, 2026
a47b471
fix by_state_column, ref
shivamka1 Jun 26, 2026
d5f07df
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 29, 2026
f7de3ea
simplify some trait implementations
ljeub-pometry Jun 29, 2026
a0aac86
fix the trait implementations and clean up redundant implementations …
ljeub-pometry Jun 29, 2026
00b5a93
still recursing
ljeub-pometry Jun 29, 2026
5f348b1
narrow EntityExprFilterOps to EntityExprBuilder marker to avoid stdli…
shivamka1 Jun 29, 2026
5e06c06
expr for gql
shivamka1 Jun 29, 2026
a5baeb6
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Jun 29, 2026
153d54e
Merge remote-tracking branch 'origin/db_v4' into filter-expr-rebase
shivamka1 Sep 1, 2026
2e78541
bridge the expression layer onto the db_v4 trait substrate
shivamka1 Sep 2, 2026
1ca57a3
python bindings, predicates and benchmarks on the bridged API
shivamka1 Sep 2, 2026
fa54b89
edge endpoint fields are expressions; test suites ride the expr path
shivamka1 Sep 2, 2026
474b482
fixes from the first full runs of the merged suites
shivamka1 Sep 2, 2026
b7e63a4
Merge remote-tracking branch 'origin/db_v4' into filter-expr-rebase
shivamka1 Sep 2, 2026
1bc6a48
python filter factories: keep the erased type set finite
shivamka1 Sep 2, 2026
04487cc
comments describe the code, not the branches it came from
shivamka1 Sep 2, 2026
2997f5e
make the python filter surface work end to end
shivamka1 Sep 2, 2026
7c1352d
leading any/all qualifiers on filter expressions
shivamka1 Sep 2, 2026
e0409ff
type the expression ops end to end; python filter suite passes in full
shivamka1 Sep 2, 2026
e618923
GraphQL filters lower onto expressions
shivamka1 Sep 3, 2026
0b1bcd8
record the wire form alongside python filter expressions
shivamka1 Sep 3, 2026
3c629fc
delete the composite filter execution path
shivamka1 Sep 3, 2026
da298f0
delete the builder filter DSL; the expression API is the only path
shivamka1 Sep 4, 2026
2f94a55
Merge branch 'db_v4': node-id domain fixes land on expression ops
shivamka1 Sep 4, 2026
dc862ca
narrow id-filter domains, widen sum's declared type, refresh stubs
shivamka1 Sep 4, 2026
db8d4b1
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Sep 4, 2026
d443d44
compare constants within a type family, as the property filters do
shivamka1 Sep 11, 2026
a1a0038
Merge branch 'db_v4': node-type indexing keeps the expression ops
shivamka1 Sep 11, 2026
5dba89a
Merge remote-tracking branch 'origin/filter_expr_wip' into filter-exp…
shivamka1 Sep 11, 2026
978498d
restore the class-with-static-methods shape of the python filter surface
shivamka1 Sep 14, 2026
7b82b11
show a filter's wire form in its python repr
shivamka1 Sep 15, 2026
3c14bfb
scope endpoint reads by the views applied to the edge
shivamka1 Sep 15, 2026
2925977
compare numbers by value, as the property filters always did
shivamka1 Sep 16, 2026
b2dae04
The filter tree is the only filter grammar (#2785)
shivamka1 Sep 17, 2026
bd40a5d
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Sep 17, 2026
e2f4e20
chore: apply tidy-public auto-fixes
github-actions[bot] Sep 17, 2026
7b2239a
Merge branch 'db_v4' into filter_expr_wip
shivamka1 Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,061 changes: 906 additions & 2,155 deletions docs/reference/graphql/graphql_API.md

Large diffs are not rendered by default.

202 changes: 182 additions & 20 deletions docs/user-guide/views/6_filtering.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,203 @@
# Filtering

The `filter` module provides a variety of functions prefixed with 'filter' that take a [filter expression][raphtory.filter.FilterExpr] and return a corresponding view.
A filter picks out part of a graph: the nodes with a high score, the edges that start at a given
node, the updates inside a time window. You describe the part you want as a
[filter expression][raphtory.filter.FilterExpr] and hand it to `filter()` on a graph, a node
collection or a node. The result is a view, so nothing is copied.

The following functions can be called on a `graph` or `node`:
A filter expression is a small tree: *read something* (a name, a degree, a property), *compare it*
to a value or to another read, and *combine* comparisons with `&`, `|` and `~`. The same tree runs
locally and is what a remote graph sends to a server, and `repr()` prints it, so what you see is
what runs.

- [filter_edges][raphtory.GraphView.filter_edges]
- [filter_exploded_edges][raphtory.GraphView.filter_exploded_edges]
- [filter_nodes][raphtory.GraphView.filter_nodes]
The examples below use this graph:

Filter expressions allow you to create complex logical queries to select a narrower set of your data based on multiple criteria. This is useful when you already have some knowledge of the subset you want to isolate.
/// tab | :fontawesome-brands-python: Python

```python
from raphtory import Graph, filter

g = Graph()
g.add_node(0, "alice", properties={"score": 3.0})
g.add_node(2, "alice", properties={"score": 7.0})
g.add_node(1, "bob", properties={"score": 5.0})
g.add_node(0, "carol")
g.add_edge(1, "alice", "bob", layer="knows")
g.add_edge(2, "bob", "carol", layer="works")
```
///

## Where a filter starts

Every expression starts from one of four entry points. The entry point says what kind of thing is
being tested, and the rest of the expression is checked against it as you build it.

| start with | tests | example |
|---|---|---|
| [filter.Node][raphtory.filter.Node] | one node at a time | `filter.Node.property("score") > 4` |
| [filter.Edge][raphtory.filter.Edge] | one edge at a time | `filter.Edge.src().name() == "alice"` |
| [filter.ExplodedEdge][raphtory.filter.ExplodedEdge] | one edge update at a time | `filter.ExplodedEdge.property("weight") > 1` |
| [filter.Graph][raphtory.filter.Graph] | nothing; it is a view (window, layer, snapshot) | `filter.Graph.window(0, 2)` |

## What you can read

From a node, or from the end of an edge (`filter.Edge.src()` and `filter.Edge.dst()`):

| read | gives |
|---|---|
| `.name()`, `.id()`, `.node_type()` | the built-in fields |
| `.degree()`, `.in_degree()`, `.out_degree()` | how many neighbours the node has (nodes only, not edge ends) |
| `.property("score")` | the latest value of a temporal property |
| `.metadata("owner")` | a metadata (constant) value |

Edges and exploded edges read `.property(...)` and `.metadata(...)` too, and have yes/no tests of
their own: `.is_valid()`, `.is_deleted()`, `.is_active()`, `.is_self_loop()`.

## How you compare

A read is an [Expr][raphtory.filter.Expr]. Comparing it gives a `FilterExpr`.

| compare with | meaning |
|---|---|
| `==`, `!=`, `<`, `<=`, `>`, `>=` | the usual comparisons; the value must match the property's type family (a number for a number, a string for a string) |
| `.is_in([...])`, `.is_not_in([...])` | membership in a list of values |
| `.starts_with(s)`, `.ends_with(s)`, `.contains(s)`, `.not_contains(s)` | string tests |
| `.fuzzy_search(s, levenshtein_distance, prefix_match)` | approximate string match |
| `.is_some()`, `.is_none()` | whether the property has a value at all |

The right-hand side can be another read. `filter.Node.degree() > filter.Node.in_degree()` selects
nodes with a neighbour that does not point back at them.

/// tab | :fontawesome-brands-python: Python

```{.python continuation}
high = filter.Node.property("score") > 4
assert sorted(n.name for n in g.filter(high).nodes) == ["alice", "bob"]

missing = filter.Node.property("score").is_none()
assert [n.name for n in g.filter(missing).nodes] == ["carol"]

more_out_than_in = filter.Node.degree() > filter.Node.in_degree()
assert sorted(n.name for n in g.filter(more_out_than_in).nodes) == ["alice", "bob"]
```
///

## Combining filters

Use the bitwise operators: `&` for *and*, `|` for *or*, `~` for *not*. Python's `and`, `or` and
`not` do not work on filter expressions.

`~f` selects everything `f` did not select. A node without the property is not selected by
`property("score") > 4`, so it *is* selected by `~(property("score") > 4)`.

/// tab | :fontawesome-brands-python: Python

```{.python continuation}
assert [n.name for n in g.filter(~high).nodes] == ["carol"]

not_bob = high & ~(filter.Node.name() == "bob")
assert [n.name for n in g.filter(not_bob).nodes] == ["alice"]

either = (filter.Node.name() == "carol") | (filter.Node.property("score") > 6)
assert sorted(n.name for n in g.filter(either).nodes) == ["alice", "carol"]
```
///

## Reading through a view

A view can sit in front of a read. `filter.Node.window(0, 2).property("score")` reads the score
*as it was inside the window*, so alice's latest score there is 3, not 7. The same works for
`.layer(...)`, `.layers(...)`, `.latest()`, `.at(t)`, `.before(t)`, `.after(t)`, `.snapshot_at(t)`
and `.snapshot_latest()`, on nodes, edges and exploded edges, and they can be chained.

/// tab | :fontawesome-brands-python: Python

To construct a filter expression you can call [filter.Node()][raphtory.filter.Node], [filter.Edge()][raphtory.filter.Edge], [filter.Property()][raphtory.filter.Property], [filter.Metadata()][raphtory.filter.Metadata], or one of their methods. This creates a corresponding builder that requires a comparison to a specified value or values to produce a valid filter expression.
```{.python continuation}
early_high = filter.Node.window(0, 2).property("score") > 4
assert [n.name for n in g.filter(early_high).nodes] == ["bob"]
```
///

The available operators for comparisons depend on if you are looking at a [node][raphtory.filter.NodeFilterBuilder], [edge][raphtory.filter.EdgeFilterOp], or [property][raphtory.filter.PropertyFilterOps] and metadata have the same operators available as properties. Additionally, you can use any of the normal Python logical operators to combine filter expressions.
## Using a property's history

For example:
`.temporal()` switches a property read from its latest value to its whole history. An aggregate
then turns the history back into one value: `.sum()`, `.avg()`, `.min()`, `.max()`, `.first()`,
`.last()`, `.len()`. `.any()` and `.all()` ask whether the comparison holds for any, or every,
value in the history.

/// tab | :fontawesome-brands-python: Python

```{.python notest}
```{.python continuation}
total = filter.Node.property("score").temporal().sum() > 8
assert [n.name for n in g.filter(total).nodes] == ["alice"]

ever_low = filter.Node.property("score").temporal().any() < 4
assert [n.name for n in g.filter(ever_low).nodes] == ["alice"]
```
///

# Compare a numerical value
filter1 = filter.Property("uptime_days") <= 60
# Check equality of stings
filter2 = filter.Property("OS_version") == "Ubuntu 20.04"
# Create a single filter from a logical combination using Python bitwise or
filter3 = (filter.Property("primary_function").is_not_in(["Database", "Backup"])) | (filter.Property("uptime_days") < 45)
## Filtering edges

An edge filter can look at the edge itself or at either end of it.

/// tab | :fontawesome-brands-python: Python

```{.python continuation}
from_alice = filter.Edge.src().name() == "alice"
assert [(e.src.name, e.dst.name) for e in g.filter(from_alice).edges] == [("alice", "bob")]

in_works = filter.Edge.layer("works").is_active()
assert [(e.src.name, e.dst.name) for e in g.filter(in_works).edges] == [("bob", "carol")]
```
///

!!! Note
As the above example shows, you must use the bitwise operators `&` and `|` instead of `and` and `or` when combining filter expressions. For more information on logical operators consult the [Python Mapping of Operators to Functions](https://docs.python.org/3/library/operator.html#mapping-operators-to-functions).
## Applying a filter

| call | what you get back |
|---|---|
| `graph.filter(expr)` | a graph view with only the matching nodes, or only the matching edges. A node filter keeps the edges between the remaining nodes; an edge filter keeps every node. |
| `graph.filter(filter.Graph.window(0, 2))` | the graph seen through the view; the same as `graph.window(0, 2)` |
| `graph.filter(filter.Graph.window(0, 2) & expr)` | the view first, then `expr` inside it: the same as `graph.window(0, 2).filter(expr)`. A view can be combined with `&` but not with `\|` or `~` |
| `graph.nodes.filter(expr)` | every node stays, but each node's edges and neighbours are narrowed to the ones that match |
| `node.filter(expr)` | the node with its edges and neighbours narrowed the same way |

/// tab | :fontawesome-brands-python: Python

```{.python continuation}
assert g.filter(filter.Graph.window(0, 2)).count_edges() == 1

narrowed = g.nodes.filter(filter.Node.name() != "carol")
assert [n.name for n in narrowed] == ["alice", "bob", "carol"]
assert [n.degree() for n in narrowed] == [1, 1, 1]
```
///

## Seeing what a filter will do

`repr()` prints the tree. It is the same tree a remote graph sends, so there is no separate
server-side form to check.

/// tab | :fontawesome-brands-python: Python

```{.python continuation}
print(repr(filter.Node.window(0, 2).property("score") > 4))
```
///

!!! output

```
FilterExpr(WINDOW[0..2](score) > 4)
```

## Cybersecurity scenario

Consider a cybersecurity team investigating the impact of a CVE on your companies servers. They might use Raphtory to filter for nodes which function as public facing servers and that have a specific operating system. This would give the security team a view that contains only nodes that might be vulnerable.
Consider a cybersecurity team investigating the impact of a CVE on your company's servers. They
might use Raphtory to filter for nodes that are public-facing servers running a specific operating
system. This gives the security team a view that contains only the nodes that might be vulnerable.

Using the traffic dataset you can explore this scenario by using `filter_nodes()` to create a new `GraphView` that contains only the nodes that match the CVE description:
Using the traffic dataset you can explore this scenario with `filter()`, which creates a new
`GraphView` containing only the nodes that match the CVE description:

/// tab | :fontawesome-brands-python: Python

Expand Down Expand Up @@ -78,6 +239,7 @@ cve_view = traffic_graph.filter(my_filter)
print(cve_view.nodes)

```
///

You can print the nodes in the filtered view to see which machines you should investigate.

Expand Down
9 changes: 8 additions & 1 deletion graphql-bench/src/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,14 @@ export function heavyNameScan() {
graph: {
__args: { path: "big" },
nodes: {
__args: { select: { name: { where: { contains: { str: "99999" } } } } },
__args: {
select: {
contains: {
lhs: { read: { entity: "NODE", target: { field: "NAME" } } },
rhs: { const: { str: "99999" } },
},
},
},
count: true,
},
},
Expand Down
Loading
Loading