The QueryBuilder API¶
Note
If you don’t need the QueryBuilder API, feel free to skip straight to learning about the Schema ORM.
The QueryBuilder API allows you to express familiar Cypher queries using normal
Python objects and operators. To demonstrate it, we will use a simple
NodeType
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 NodeType, Property
Person = NodeType('Person',
Property('name', indexed=True),
Property('age', type=int),
Property('hair_color')
)
Don’t forget to create the indexes and constraints you specified using
graph.schema.add()
:
graph.schema.add(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. These are located in the neoalchemy.cypher
module:
from neoalchemy.cypher 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 (n:Person {age: {age_n}, name: {name_n}, hair_color: {hair_color_n}})
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
{'age_n': None, 'hair_color_n': None, 'name_n': None}
Each parameter is named according to its associated property and the variable
representing its associated node in the underlying Cypher. By Neo4J convention,
the default parameter is n
. This can be freely changed to whatever you like
by specifying a second argument to Create
:
>>> create = Create(Person, 'm')
>>> print(create)
CREATE (m:Person {age: {age_m}, name: {name_m}, hair_color: {hair_color_m}})
This is an important feature which will come in handy when specifying more complex queries, as we will see later.
Properties can either be set one at a time using
set()
:
create = Create(Person).set(Person.hair_color, 'red')
Or set directly using the params
dict:
>>> create.params['name_n'] = 'Ali'
>>> ali_params = {'age_n': 29, 'hair_color_n': 'red'}
>>> create.params.update(ali_params)
Once you’re satisfied with your settings, 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.cypher import Match
Match has a very similar interface to Create. For a simple use case, we get almost identical results:
>>> match = Match(Person)
>>> print(match)
MATCH (n:Person {hair_color: {hair_color_n}, name: {name_n}, age: {age_n}})
...but this isn’t a very interesting MATCH
statement. For one thing, it’s
not a full query yet. In order to make this useful, at a minimum we need to
return something:
>>> print(match.return_())
MATCH (n:Person {hair_color: {hair_color_n}, name: {name_n}, age: {age_n}})
RETURN *
Note
Notice the function is return_, not return. The latter would cause
a syntax error since return
is a Python reserved word.
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_('node') |
RETURN node |
Many nodes | return_(['n', 'm']) |
RETURN n, m |
One property | return_({'n': 'name'}) |
RETURN n.name |
Many properties | return_({'n': ['x', 'y']}) |
RETURN n.x, n.y |
Nodes with properties | return_({'m': 'x', 'n': 'y'}) |
RETURN m.x, n.y |
Nodes with many properties | return_({'m': 'x', 'n': ['y', 'z']}) |
RETURN m.x, n.y, n.z |
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_({'n': '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)
Relationships¶
Like Cypher, NeoAlchemy “describes patterns in graphs visually using an ascii-art syntax”:
Create(Person, 'a')['KNOWS'](Person, 'b')
This creates exactly the relationship you would expect:
>>> Person = NodeType('Person', Property('name'))
>>> create = Create(Person, 'a')['KNOWS'](Person, 'b')
>>> print(create)
CREATE (a:Person {name: {name_a}})-[r1:KNOWS]->(b:Person {name: {name_b}})
>>> create.params
{'name_a': None, 'name_b': None}
This is another form of chaining! This not only means that relationship chains can be arbitrarily long:
Create(Person)['KNOWS'](Person)['KNOWS'](Person)['KNOWS'](Person)
It also means that you can write things like this:
Match(Person).where(Person.name=='Ali')['KNOWS'](Person)
# MATCH (n:Person)-[r1:KNOWS]->(n1:Person) WHERE n.name = {name_n}