Advanced queries¶
Neomodel contains an API for querying sets of nodes without having to write cypher:
class SupplierRel(StructuredRel):
since = DateTimeProperty(default=datetime.now)
class Supplier(StructuredNode):
name = StringProperty()
delivery_cost = IntegerProperty()
coffees = RelationshipTo('Coffee', 'SUPPLIES')
class Coffee(StructuredNode):
name = StringProperty(unique_index=True)
price = IntegerProperty()
suppliers = RelationshipFrom(Supplier, 'SUPPLIES', model=SupplierRel)
Node sets and filtering¶
The .nodes
property of a class returns all nodes of that type from the database.
This set (or NodeSet) can be iterated over and filtered on. Under the hood it uses labels introduced in Neo4J 2:
# nodes with label Coffee whose price is greater than 2
Coffee.nodes.filter(price__gt=2)
try:
java = Coffee.nodes.get(name='Java')
except Coffee.DoesNotExist:
print "Couldn't find coffee 'Java'"
The filter method borrows the same Django filter format with double underscore prefixed operators:
lt - less than
gt - greater than
lte - less than or equal to
gte - greater than or equal to
ne - not equal
in - item in list
isnull - True IS NULL, False IS NOT NULL
exact - string equals
iexact - string equals, case insensitive
contains - contains string value
icontains - contains string value, case insensitive
startswith - starts with string value
istartswith - starts with string value, case insensitive
endswith - ends with string value
iendswith - ends with string value, case insensitive
regex - matches a regex expression
iregex - matches a regex expression, case insensitive
Complex lookups with Q
objects¶
Keyword argument queries – in filter,
etc. – are “AND”ed together. To execute more complex queries (for
example, queries with OR
statements), Q objects <neomodel.Q> can
be used.
A Q object (neomodel.Q
) is an object
used to encapsulate a collection of keyword arguments. These keyword arguments
are specified as in “Field lookups” above.
For example, this Q
object encapsulates a single LIKE
query:
from neomodel import Q
Q(name__startswith='Py')
Q
objects can be combined using the &
and |
operators. When an
operator is used on two Q
objects, it yields a new Q
object.
For example, this statement yields a single Q
object that represents the
“OR” of two "name__startswith"
queries:
Q(name__startswith='Py') | Q(name__startswith='Jav')
This is equivalent to the following SQL WHERE
clause:
WHERE name STARTS WITH 'Py' OR name STARTS WITH 'Jav'
Statements of arbitrary complexity can be composed by combining Q
objects
with the &
and |
operators and use parenthetical grouping. Also, Q
objects can be negated using the ~
operator, allowing for combined lookups
that combine both a normal query and a negated (NOT
) query:
Q(name__startswith='Py') | ~Q(year=2005)
Each lookup function that takes keyword-arguments
(e.g. filter, exclude, get) can also be passed one or more
Q
objects as positional (not-named) arguments. If multiple
Q
object arguments are provided to a lookup function, the arguments will be “AND”ed
together. For example:
Lang.nodes.filter(
Q(name__startswith='Py'),
Q(year=2005) | Q(year=2006)
)
This roughly translates to the following Cypher query:
MATCH (lang:Lang) WHERE name STARTS WITH 'Py'
AND (year = 2005 OR year = 2006)
return lang;
Lookup functions can mix the use of Q
objects and keyword arguments. All
arguments provided to a lookup function (be they keyword arguments or Q
objects) are “AND”ed together. However, if a Q
object is provided, it must
precede the definition of any keyword arguments. For example:
Lang.nodes.get(
Q(year=2005) | Q(year=2006),
name__startswith='Py',
)
This would be a valid query, equivalent to the previous example;
Has a relationship¶
The has method checks for existence of (one or more) relationships, in this case it returns a set of Coffee nodes which have a supplier:
Coffee.nodes.has(suppliers=True)
This can be negated by setting suppliers=False, to find Coffee nodes without suppliers.
Iteration, slicing and more¶
Iteration, slicing and counting is also supported:
# Iterable
for coffee in Coffee.nodes:
print coffee.name
# Sliceable using python slice syntax
coffee = Coffee.nodes.filter(price__gt=2)[2:]
The slice syntax returns a NodeSet object which can in turn be chained.
Length and boolean methods dont return NodeSet objects and cannot be chained further:
# Count with __len__
print len(Coffee.nodes.filter(price__gt=2))
if Coffee.nodes:
print "We have coffee nodes!"
Filtering by relationship properties¶
Filtering on relationship properties is also possible using the match method. Note that again these relationships must have a definition.:
coffee_brand = Coffee.nodes.get(name="BestCoffeeEver")
for supplier in coffee_brand.suppliers.match(since_lt=january):
print(supplier.name)
Ordering by property¶
Ordering results by a particular property is done via th order_by method:
# Ascending sort
for coffee in Coffee.nodes.order_by('price'):
print(coffee, coffee.price)
# Descending sort
for supplier in Supplier.nodes.order_by('-delivery_cost'):
print(supplier, supplier.delivery_cost)
Removing the ordering from a previously defined query, is done by passing None to order_by:
# Sort in descending order
suppliers = Supplier.nodes.order_by('-delivery_cost')
# Don't order; yield nodes in the order neo4j returns them
suppliers = suppliers.order_by(None)
For random ordering simply pass ‘?’ to the order_by method:
Coffee.nodes.order_by('?')
Retrieving paths¶
You can retrieve a whole path of already instantiated objects corresponding to the nodes and relationship classes with a single query.
Suppose the following schema:
class PersonLivesInCity(StructuredRel):
some_num = IntegerProperty(index=True,
default=12)
class CountryOfOrigin(StructuredNode):
code = StringProperty(unique_index=True,
required=True)
class CityOfResidence(StructuredNode):
name = StringProperty(required=True)
country = RelationshipTo(CountryOfOrigin,
'FROM_COUNTRY')
class PersonOfInterest(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(unique_index=True)
age = IntegerProperty(index=True,
default=0)
country = RelationshipTo(CountryOfOrigin,
'IS_FROM')
city = RelationshipTo(CityOfResidence,
'LIVES_IN',
model=PersonLivesInCity)
Then, paths can be retrieved with:
q = db.cypher_query("MATCH p=(:CityOfResidence)<-[:LIVES_IN]-(:PersonOfInterest)-[:IS_FROM]->(:CountryOfOrigin) RETURN p LIMIT 1",
resolve_objects = True)
Notice here that resolve_objects
is set to True
. This results in q
being a
list of result, result_name
and q[0][0][0]
being a NeomodelPath
object.
NeomodelPath
nodes, relationships
attributes contain already instantiated objects of the
nodes and relationships in the query, in order of appearance.
It would be particularly useful to note here that each object is read exactly once from
the database. Therefore, nodes will be instantiated to their neomodel node objects and
relationships to their relationship models if such a model exists. In other words,
relationships with data (such as PersonLivesInCity
above) will be instantiated to their
respective objects or StrucuredRel
otherwise. Relationships do not “reload” their
end-points (unless this is required).