When GraphQL was published as part of Facebook’s React efforts, it made a big buzz as an straightforward means to declare what kind of projection of your domain data you need for a certain UI component. Using a JSON-like syntax you define which properties of your entity and related entities you want to be part of the data structure you get back from the server.

Update: We now use exactly these mechanisms in our Neo4j-GraphQL integration.

Here is an example from a Stack Overflow query using the data model from our previous blog post on that topic.

Learn all about these new features in Cypher inspired by GraphQL and get a sneak peek at Neo4j 3.1

  question {
    author {
    tags {
    answers {
      author {

Cypher, with its rich support for literal maps and collections and the very powerful COLLECT aggregation function, already allows for returning complex JSON documents.

MATCH (u:User)-[:ASKED]->(q:Question)-[:TAGGED]->(t:Tag),

RETURN { title: q.title, author: u.name, tags: collect(t.name),
       answers: collect({text: a.text, author: u2.name})} as question

This results in a document like the one below, which is similar to the original Stack Overflow query API result.

  "title": "neo4j cypher query to delete a middle node and connect all its parent node to child node",
  "author": "Soumya George",
  "tags": [
  "answers": [
      "text": "Some text",
      "author": "InverseFalcon"

Some things are not as convenient as what we saw in GraphQL. So, we thought it would be very helpful to add more syntactic sugar to the language.

Luckily, my friend Andrés found some spare time to add two really neat features to Cypher in Neo4j 3.1 which we want to look into today.

Map Projections

Map projections are very close to what you expect from a GraphQL query, you take an map or entity (node or relationship) and apply a map-like property-selector to it. The result of the projection is a (optionally nested) map of results.

Here is the example above rewritten using a map projection.

MATCH (u:User)-[:ASKED]->(q:Question)-[:TAGGED]->(t:Tag),

RETURN q{ .title, author : u.name, tags: collect(t.name),
       answers: collect( a {.text, author: u2.name})} as question

But even more things are possible. Within a map projection, you can also add literal values or aggregations to the data that you extract from the entity.

entity { .property1, .property2, .*,  literal: value,  values: collect(numbers), variable}

Here is a full list of possible selectors:

syntax description example


property lookup

p{.name} → {name : "John"}


all properties

p{.*} → {name:"John", age:42}


variable name as key, variable value as value

p{count} → {count: 1}

key : value

literal entry

p{awesome:true} → {awesome:true}

To demonstrate those options we could rewrite the statement to:

MATCH (u:User)-[:ASKED]->(q:Question)-[:TAGGED]->(t:Tag),

WITH q, u, collect(t.name) as tags, collect( a {.text, author: u2.name}) as answers
RETURN q{ .title, author : u{.*}, tags,  answers } as question

To pull in information from related entities, the other new feature, pattern comprehensions come into play.

Pattern Comprehensions

You’ve all (hopefully) used the list comprehensions in Cypher. They borrow from Haskell’s syntax and look like this:

[value IN list WHERE predicate(value) | expression(value)]

As a concrete example, this example query returns the squares of the first five even numbers:

RETURN [x IN range(1,10) WHERE x % 2 = 0 | x * x] -> [4, 16, 36, 64, 100]

Now, you can use any kind of collection here: Collections of maps, nodes or even paths.

If you use a graph pattern as an expression, it actually yields a collection of paths.

That’s cool, because now you can use a list comprehension to do pattern matching and extract a related node without actually using MATCH and changing your cardinality. So, instead of….

MATCH (u:User)-[:POSTED]->(q:Question)
WHERE q.title CONTAINS "Neo4j"
RETURN u.name, collect(q.title) as questions

…you could write:

MATCH (u:User)
RETURN u.name, [path IN (u)-[:ASKED]->(:Question)
                  WHERE (last(nodes(path))).title CONTAINS "Neo4j"
                      | (last(nodes(path))).title] as questions

BTW: This statement always returns a result, potentially even an empty collection, so it’s the same as if you were using OPTIONAL MATCH in the previous statement.

Wow, that’s ugly. Why? Because you can’t introduce new variables, like q in such a pattern expression. Only clauses could introduce new variables…until now!

With pattern comprehensions, you actually can introduce local variables in such a pattern and use them in the WHERE filter or expression at the end.

MATCH (u:User)
RETURN u.name,
       [(u)-[:ASKED]->(q:Question) WHERE q.title CONTAINS "Neo4j" | q.title] as questions

Now let’s take a stab at our “GraphQL” query again, and see how we can rewrite it just starting from the Question node and moving all projections of attributes and patterns into the RETURN clause.

MATCH (q:Question)

RETURN q{.title,
         author : [(q)<-[:ASKED]-(u) | u.name][0],
         tags   : [(q)<-[:TAGGED]-(t) | t.name],
         answers: [(q)<-[:ANSWERS]-(a)<-[:PROVIDED]-(u2) | a{ .text, author: u2.name } ] }

    • As pattern comprehensions always return a collection, we have to turn them into a single value as needed, e.g. with […​][0] or head([…​])
    • To combine attributes of two entities into one map, you have to spell out the second entity’s attributes. It would be nice to get support for combining maps in the future, then we could use: answers: [(q)<-[:ANSWERS]-(a)<-[:PROVIDED]-(u2) | a{ .text } + u2{ .name} ]

If you want to test these cool new features, please grab the recently published Neo4j 3.1.0-M07 release and give it a try.

We’d love to get your feedback on these and other new features like the brand-new [cypher-shell].

With a lot of thanks to Andrés for these GraphQL features in Cypher and to everyone in engineering for a really cool graph database,


Level up your skills with graph technology:
Register for our online training class, Introduction to Graph Databases and master the world of graphs.

Sign Me Up



About the Author

Michael Hunger , Developer Relations

Michael Hunger Image

Michael Hunger has been passionate about software development for a very long time. For the last few years he has been working on the open source Neo4j graph database filling many roles.

As caretaker of the Neo4j community and ecosystem he especially loves to work with graph-related projects, users and contributors. As a developer, Michael enjoys many aspects of programming languages, learning new things every day, participating in exciting and ambitious open source projects and contributing and writing software related books and articles.


James says:

This is great stuff! How would this work if you want to return a collection of nodes that also have a collection of nodes and you want to return them all? In my use case a user can have photos and each photo has a handful of meta nodes. Currently I use the object literal to the meta nodes and then pass them into another object containing the photo and then collect that.

WITH {photo: properties(photo), meta: collect(meta)} AS photo
RETURN collect(photos)

Also, i plan on adding privacy levels to each photo so they can be public or private, so i would need to also only return photos that the user requesting them can see. How would this be implemented with graphQL?

Thanks, James

Joost den Boer says:

Is the graphql-like syntax as described in this post not supported anymore by newer Neo4j versions?
We like this syntax since it make the code more readable. We are using this quite a bit so that would mean we would have a serious issue when we would like to upgrade.
Anyway, the reason why I came back to this page, is because I had an issue with duplicate items in a sub-query.

Like in the provided example, how to only get the distinct answers?
I tried ‘… | distinct a{ .text }’ and using ‘collect(..)’ and collect( distinct .. )’ over the subquery, but it does not work.

Andrew Bowman says:

Hi Joost,

The syntax here (for pattern comprehensions and map projections) still works as described.

Unfortunately `DISTINCT` within pattern comprehensions isn’t currently supported (but you can use the `apoc.coll.toSet()` function in APOC Procedures to ensure distinct list elements).

We have submitted a request to the OpenCypher project to allow DISTINCT, ORDER BY, SKIP, and LIMIT in pattern comprehensions, so this will come, eventually.


Leave a Reply

Your email address will not be published. Required fields are marked *


Upcoming Event


Have a Graph Question?

Stack Overflow
Community Forums
Contact Us

Share your Graph Story?

Email us: content@neo4j.com