How to Build Complex Queries Using Q Objects and Match Filtering in Neomodel
Use Q objects combined with bitwise operators & (AND), | (OR), and ~ (NOT) inside NodeSet.filter() to construct complex boolean predicates that Neomodel translates into optimized Cypher WHERE clauses.
Neomodel, the official Python Object-Graph Mapper (OGM) for Neo4j maintained at neo4j-contrib/neomodel, provides a Django-inspired query API. When you need to build complex queries using Q objects and match filtering, you leverage the Q class from neomodel/match_q.py and the filter method in neomodel/sync_/match.py to compose logical trees that resolve into efficient graph traversals.
Understanding Q Objects and Logical Predicates
The Q class in neomodel/match_q.py (lines 65-78) represents a logical predicate tree. Each Q instance acts as a node that can contain field lookups or other Q objects as children.
Key characteristics of Q objects:
- Leaf nodes store field lookups as keyword arguments (e.g.,
Q(price__lt=10)) - Branch nodes combine children using connectors (
ANDorOR) - Negation is tracked via a
negatedboolean flag
The class inherits from QBase, which implements the tree mechanics including children, connector, and negation attributes. This architecture allows you to build arbitrarily complex nested logic before any Cypher is generated.
How NodeSet.filter Processes Q Objects
The NodeSet.filter method in neomodel/sync_/match.py (lines 88-106) serves as the entry point for complex queries. This method accepts both Q objects as positional arguments and Django-style keyword arguments.
The internal flow works as follows:
- Argument collection –
filtergathers*args(expected to beQinstances) and**kwargs(field lookups) - Conversion – Keyword arguments are converted to
Qobjects viaprocess_filter_args - Tree assembly – The existing
self.q_filtersis combined with newQobjects using the&(AND) operator:self.q_filters = Q(self.q_filters & Q(*new_args, **kwargs)) - Cypher generation – Later,
QueryBuilder._parse_q_filtersrecursively walks the tree to emit CypherWHEREclauses
This design ensures that whether you pass Q objects, keyword arguments, or a mix, everything resolves into a single coherent predicate tree.
Building Complex Query Patterns
Combining Conditions with AND and OR
Use the & and | operators to build compound conditions without chaining multiple filter calls.
from neomodel import Q
# Find coffees cheaper than 10 OR more expensive than 100
results = Coffee.nodes.filter(
Q(price__lt=10) | Q(price__gt=100)
).all()
The | operator creates a parent Q node with connector='OR' and the two leaf nodes as children. According to the source in match_q.py, this returns a new Q instance rather than modifying in place, allowing you to reuse predicate components.
Negating Conditions with NOT
Apply the ~ operator to invert any predicate, creating NOT clauses in the resulting Cypher.
# All coffees NOT in the mid-range price (20-30)
not_mid_range = Coffee.nodes.filter(
~(Q(price__gte=20) & Q(price__lte=30))
).all()
The ~ operator sets the negated=True flag on the Q object. When QueryBuilder._parse_q_filters processes this node, it wraps the resulting Cypher fragment in NOT (...).
Traversing Relationships with Double Underscore Syntax
Use Django-style __ notation to traverse relationships and filter on properties of related nodes.
class Supplier(StructuredNode):
name = StringProperty()
coffees = RelationshipTo('Coffee', 'SUPPLIES')
class Coffee(StructuredNode):
name = StringProperty()
species = StringProperty()
# Suppliers that provide coffee named "Latte" OR whose coffee species is "Robusta"
suppliers = Supplier.nodes.filter(
Q(coffees__name="Latte") | Q(coffees__species__name="Robusta")
).all()
As implemented in neomodel/sync_/match.py, the double-underscore syntax triggers process_filter_args to split the path, create necessary traversal Path objects, and add the condition to the appropriate variable in the Cypher query.
Filtering on Relationship Properties with Pipe Syntax
When you need to filter on properties of the relationship itself (not the target node), use the | character in the filter key.
from datetime import datetime
# Suppliers with a "SUPPLIES" relationship having "since" property after 2018-01-01
recent_suppliers = Supplier.nodes.filter(
**{"coffees|since__gt": datetime(2018, 1, 1)}
).all()
The pipe syntax (coffees|since) is detected in neomodel/sync_/match.py (lines 103-108) by _process_filter_key, which distinguishes between node properties and relationship properties when building the Cypher WHERE clause.
How Q Objects Translate to Cypher
The transformation from Python Q objects to Cypher occurs in QueryBuilder._parse_q_filters within neomodel/sync_/match.py. The process follows these stages:
- Tree Walking – The method recursively traverses the
Qobject'schildrenlist - Leaf Resolution – For leaf nodes (actual field lookups), it:
- Resolves property paths including traversals (e.g.,
coffees__species__name) - Calls
process_filter_argsto map field names to database properties and deflate Python values - Emits Cypher fragments like
n.price > $price_1
- Resolves property paths including traversals (e.g.,
- Logical Assembly – Fragments are joined with
ANDorORbased on the node'sconnectorattribute - Negation Handling – If
negated=True, the entire sub-tree is wrapped inNOT (...) - Parameter Binding – All values are parameterized to prevent Cypher injection
This architecture allows complex nested logic to compile into optimized, parameterized Cypher queries without manual string concatenation.
Complete Code Examples
Reusable Predicate Components
from neomodel import Q, StructuredNode, StringProperty, FloatProperty
class Product(StructuredNode):
name = StringProperty()
category = StringProperty()
price = FloatProperty()
stock = FloatProperty()
# Build reusable Q components
in_stock = Q(stock__gt=0)
electronics = Q(category="Electronics")
premium = Q(price__gt=1000)
# Combine for specific queries
affordable_electronics = Product.nodes.filter(
electronics & in_stock & ~premium
).all()
clearance_items = Product.nodes.filter(
in_stock & Q(price__lt=50)
).all()
Complex Nested Query with Traversals
class Manufacturer(StructuredNode):
name = StringProperty()
country = StringProperty()
products = RelationshipTo('Product', 'MAKES')
# Find manufacturers in Japan or Germany that make either:
# 1. In-stock electronics under $500, OR
# 2. Premium products over $2000
complex_query = Manufacturer.nodes.filter(
Q(country="Japan") | Q(country="Germany"),
(
Q(products__category="Electronics") &
Q(products__price__lt=500) &
Q(products__stock__gt=0)
) | Q(products__price__gt=2000)
).all()
Summary
- Q objects in
neomodel/match_q.pyprovide a Pythonic way to build logical predicate trees using&(AND),|(OR), and~(NOT) operators. NodeSet.filterinneomodel/sync_/match.pyaccepts bothQobjects and Django-style keyword arguments, merging them into a unified query tree stored inq_filters.- Double-underscore syntax (
__) traverses relationships to filter on properties of connected nodes, while pipe syntax (|) targets properties on the relationships themselves. QueryBuilder._parse_q_filtersrecursively converts theQtree into parameterized CypherWHEREclauses, handling negation and logical connectors automatically.
Frequently Asked Questions
How do I combine multiple Q objects with AND and OR operators?
Use the & operator for AND and the | operator for OR. These operators return new Q instances rather than modifying existing ones, allowing you to build complex trees declaratively. For example, Q(price__lt=10) | Q(price__gt=100) creates an OR condition, while Q(stock__gt=0) & Q(category="Electronics") creates an AND condition.
Can I negate a Q object to create a NOT condition?
Yes, apply the ~ operator to any Q object to negate it. This sets the negated flag on the Q instance, which QueryBuilder._parse_q_filters interprets by wrapping the resulting Cypher fragment in NOT (...). For example, ~Q(price__gte=20) translates to NOT (n.price >= $price_1) in the generated Cypher query.
How do I filter on properties of relationships rather than nodes?
Use the pipe character (|) in your filter key to target relationship properties instead of node properties. For example, coffees|since__gt filters on the since property of the SUPPLIES relationship rather than properties of the Coffee node. This syntax is processed in neomodel/sync_/match.py by _process_filter_key, which distinguishes between node property paths and relationship property paths when constructing the Cypher query.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →