The QueryBuilder API¶
Note
If you don’t need the QueryBuilder API, feel free to skip straight to learning about the Schema OGM.
The QueryBuilder API allows you to express familiar Cypher queries using normal
Python objects and operators. To demonstrate it, we will use a simple
Node
like the user
we defined in the previous section.
We’ll call this one person
and give it a few simple characteristics:
from neoalchemy import Node, Property
person = Node('Person',
name=Property(indexed=True),
age=Property(type=int),
hair_color=Property()
)
Don’t forget to create the indexes and constraints you specified using
graph.schema.create()
:
graph.schema.create(person)
Warning
From the Neo4J Docs:
Indexes in Neo4j are eventually available. That means that when you first create an index the operation returns immediately. The index is populating in the background and so is not immediately available for querying. When the index has been fully populated it will eventually come online. That means that it is now ready to be used in queries.
Create¶
NeoAlchemy features QueryBuilder Classes which correspond to familiar Cypher verbs:
from neoalchemy import Create
Let’s start by constructing perhaps the simplest query possible:
create = Create(person)
We can see the query this generates by printing it:
>>> print(create)
CREATE (node:`Person`)
SET node.name = {node_name}, node.age = {node_age}, node.hair_color = {node_hair_color}
NeoAlchemy has automatically applied the Person
label and created
parameters associated with each of the properties we defined. We can see
the current values for each parameter by inspecting the
params
dict:
>>> create.params
{'node_age': None, 'node_hair_color': None, 'node_name': None}
Each parameter is named according to its associated property and the variable
representing its associated node in the underlying Cypher. The default node
variable is node
. This can be freely changed to whatever you like:
>>> person.var = 'n'
>>> print(Create(person))
CREATE (n:`Person`)
SET n.name = {n_name}, n.age = {n_age}, n.hair_color = {n_hair_color}
Properties can be set individually on the Node:
>>> person.name = 'Ali'
>>> person.age = 30
>>> person.hair_color = 'red'
Once you’re satisfied, you can write it to the graph using
graph.query
:
>>> graph.query(create, **create.params)
Note
You can run arbitrary queries against the database using
graph.query
. It takes a string as its first argument and
accepts parameters as keyword arguments. It returns a Neo4J
StatementResult. We’ll learn more in depth about what Graph
can do a little later.
Match¶
Now that we’ve experimented a bit with writing to the database, let’s take a look at how to read data from it:
from neoalchemy import Match
Match has a very similar interface to Create. In the simplest case, Match looks only at labels:
>>> match = Match(person)
>>> print(match)
MATCH (n:`Person`)
…but this isn’t a full query yet. In order to make this useful, we need to return something:
>>> print(match.return_())
MATCH (n:`Person`)
RETURN *
Note
Notice the function is return_, not return. The latter would cause
a syntax error since return
is a reserved word in Python.
Return¶
If you call return_()
with no arguments,
the resulting query will RETURN *
, returning everything you have matched.
For performance reasons, however, this is often not the best choice. There
are several ways to return only what you need instead of everything you’ve
touched.
What to Return | NeoAlchemy | Cypher Equivalent |
---|---|---|
One node | return_(person_n) |
RETURN n |
Many nodes | return_(person_n, person_m) |
RETURN n, m |
One property | return_(person_n['name']) |
RETURN n.name |
Many properties | return_(person_n['x'], person_n['y']) |
RETURN n.x, n.y |
Many nodes and properties | return_(person_m['x'], person_n['y']) |
RETURN m.x, n.y |
Where¶
As with set()
, the
where()
method can be used to set
parameters one at a time:
match = Match(person).where(person['name']=='Ali')
The first argument is a CypherExpression
object, which is
automatically created when you perform the corresponding Python comparison
using one of the NodeType’s Properties.
Comparison Type | NeoAlchemy CypherExpression | Cypher Equivalent |
---|---|---|
Equal to | person['name'] == 'Ali' |
n.name = 'Ali' |
Not equal to | person['name'] != 'Ali' |
n.name <> 'Ali' |
Greater than | person['age'] > 29 |
n.age > 29 |
Greater than or equal | person['age'] >= 29 |
n.age >= 29 |
Lesser than | person['age'] < 29 |
n.age < 29 |
Lesser than or equal | person['age'] <= 29 |
n.age <= 29 |
Chaining¶
An important concept in NeoAlchemy is method chaining. Most methods return
self
so you can call them like so:
match = Match(person).where(person['name']=='Ali').return_(person['name'])
This makes for convenient and expressive one-liners. However, this also means that state is easy to build up over time and as part of larger algorithms:
match = Match(person)
# ... some code ...
match.where(person['age']=age)
# ... more code...
match.return_(ret_params)
Binding & Primary Keys¶
Often instead of specifying individual where clauses, it will be preferable to match on a set of the Node’s Properties that define what it is. One way to do this in NeoAlchemy is by binding the Node to those Properties:
>>> print(Match(person))
MATCH (n:`Person`)
>>> ali = Node('Person', name='Ali', var='n')
>>> print(Match(ali.bind('name')))
MATCH (n:`Person`)
WHERE n.name = {n_name}
Setting certain Properties as the primary keys of a Node will give it a default binding:
>>> person = Node('Person', name=Property(primary_key=True), var='n')
>>> print(Match(person.bind()))
MATCH (n:`Person`)
WHERE n.name = {n_name}
Relationships¶
So far, we have only worked with nodes. NeoAlchemy also provides a
Relationship
class. Relationships in NeoAlchemy always have a
type. To create a relationship:
>>> from neoalchemy import Relationship
>>> knows = Relationship('KNOWS')
Relationships aren’t much good without start and end nodes, though. Let’s connect two Person nodes who know each other:
>>> knows.start_node = person.copy(var='a')
>>> knows.end_node = person.copy(var='b')
>>> print(Create(knows))
CREATE (a)-[rel:`KNOWS`]->(b)
But wait! This isn’t the right Cypher query. In order to use relationships with Cypher query builders, we must first build up match statements to grab the right end nodes.
Set Combinations¶
Not all Cypher queries are one line, and neither are all NeoAlchemy queries.
You can use Python’s set operators to combine several NeoAlchemy objects into
multi-line queries before returning. The &
(set intersection) operator
is used for line-by-line cominbation. The most typical way this will be used
is with relationships in order to fully specify them for Creating or Matching:
>>> ali = Node('Person', name='Ali', var='ali').bind('name')
>>> frank = Node('Person', name='Frank', var='frank').bind('name')
>>> knows = Relationship('KNOWS', ali, frank)
>>> print(Match(ali) & Match(frank) & Match(knows))
MATCH (ali:`Person`)
WHERE ali.name = {ali_name}
MATCH (frank:`Person`)
WHERE frank.name = {frank_name}
MATCH (ali)-[rel:`KNOWS`]->(frank)
The |
(set union) operator is used for UNION ALL
. To borrow an
example from the Cypher docs:
>>> movie = Node('Movie', title=Property(primary_key=True), var='movie')
>>> actor = Node('Actor', name=Property(primary_key=True), var='actor')
>>> acted_in = Relationship('ACTED_IN', actor, movie)
>>> directed = Relationship('DIRECTED', actor, movie)
>>> actor_match = (
... (Match(actor) & Match(movie) & Match(acted_in))
... .return_(actor['name'], movie['title'])
... )
>>> director_match = (
... (Match(actor) & Match(movie) & Match(directed))
... .return_(actor['name'], movie['title'])
... )
>>> print(actor_match | director_match)
MATCH (actor:`Actor`)
MATCH (movie:`Movie`)
MATCH (actor)-[rel:`ACTED_IN`]->(movie)
RETURN actor.name, movie.title
UNION ALL
MATCH (actor:`Actor`)
MATCH (movie:`Movie`)
MATCH (actor)-[rel:`DIRECTED`]->(movie)
RETURN actor.name, movie.title
If you instead want UNION
, use the ^
(exclusive or) operator.
Note
UNION
must be performed on queries with very similar result structures.
You must take this into account when building your queries.