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

Note

The remove() and delete() methods work the same way. They correspond to Cypher’s REMOVE and DELETE.

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

Cypher: (a)-[:KNOWS]->(b) NeoAlchemy: (a)['KNOWS'](b)

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}