Mark Needham

Thoughts on Software Development

neo4j/cypher: Using a WHERE clause to filter paths

with 7 comments

One of the cypher queries that I wanted to write recently was one to find all the players that have started matches for Arsenal this season and the number of matches that they’ve played in.

The data model that I’m querying looks like this:

Games

I started off with the following query which traverses from Arsenal to all the games that they’ve taken part in and finds all the players who’ve played in those games:

START team = node:teams('name:"Arsenal"')
MATCH team-[:home_team|away_team]-game-[:played_in]-player
RETURN player.name, COUNT(player.name) as games
ORDER BY games desc

It returns the following result set:

------------------------------+
| player.name          | games |
+------------------------------+
| "Cazorla"            | 25    |
| "Arteta"             | 22    |
| "Mertesacker"        | 22    |
| "Vermaelen"          | 22    |
| "Podolski"           | 21    |
| "Gibbs"              | 18    |
| "Szczesny"           | 17    |
…
| "Tiote"              | 1     |
| "Diame"              | 1     |
| "Ridgewell"          | 1     |
| "Lampard"            | 1     |
| "Bramble"            | 1     |
| "Simpson"            | 1     |
+------------------------------+
258 rows

which is partially right but also includes a bunch of players who played for the opposition rather than for Arsenal.

The reason for this is that we’re following the ‘played_in’ relationship from the games and that relationship doesn’t distinguish between players playing for a specific team in the match.

I initially tried to reduce the number of rows returned by adding to the MATCH clause like this:

START team = node:teams('name:"Arsenal"')
MATCH team-[:home_team|away_team]-game-[:played_in]-player, 
      player-[:played]-()-[:for]-team
RETURN player.name, COUNT(player.name) as games
ORDER BY games desc

This did work to an extent – it now returned only Arsenal players but the count of games was completely wrong:

+------------------------------+
| player.name          | games |
+------------------------------+
| "Cazorla"            | 625   |
| "Mertesacker"        | 484   |
| "Arteta"             | 484   |
| "Vermaelen"          | 484   |
| "Podolski"           | 441   |
| "Giroud"             | 400   |
| "Gibbs"              | 324   |
...
+------------------------------+
21 rows

The reason this doesn’t work is that for every Team -> Game -> Player traversal in the first MATCH clauses we are doing a Player->Stats->Team traversal.

Taking Cazorla as an example: the first traversal matches 25 paths and then the second traversal also matches 25 paths.

The reason we end up with 625 matching paths for him is that we’re doing that second traversal 25 times, once for each of the matching paths from the first traversal.

What we actually need to do is make that second MATCH clause a WHERE clause which I hadn’t realised was possible until a few days ago.

In all the previous queries I’ve written with cypher I only ever used the WHERE clause to filter based on node or relationship properties.

If we make this change we end up with the following query

START team = node:teams('name:"Arsenal"')
MATCH team-[:home_team|away_team]-game-[:played_in]-player
WHERE player-[:played]-()-[:for]-team
RETURN player.name, COUNT(player.name) as games
ORDER BY games desc

This returns the following result set:

+------------------------------+
| player.name          | games |
+------------------------------+
| "Cazorla"            | 25    |
| "Arteta"             | 22    |
| "Mertesacker"        | 22    |
| "Vermaelen"          | 22    |
| "Podolski"           | 21    |
| "Gibbs"              | 18    |
| "Szczesny"           | 17    |
| "Sagna"              | 16    |
| "Wilshere"           | 16    |
...
+------------------------------+
21 rows

Which is exactly what we want!

The use of WHERE clauses in this way is explained in more detail towards the end of the documentation.

Be Sociable, Share!

Written by Mark Needham

February 19th, 2013 at 12:03 am

Posted in neo4j

Tagged with ,

  • http://twitter.com/andres_taylor Andrés Taylor

    Nice write-up, Mark.

    I remember being very excited about this concept when Neo4j 1.8 was released, but I guess it was dwarfed by the addition of updating capabilities to Cypher. Thanks for sharing!

  • http://www.markhneedham.com/blog Mark Needham

    @twitter-50762276:disqus ah I didn’t realise it was a new feature, didn’t read the release notes closely enough!

    I meant to ask you is there a way to make it so only the read capabilities of cypher work? Presumably starting the database in ‘read-only’ mode would effectively disable those features? http://docs.neo4j.org/chunked/milestone/shell-starting.html#read-only-mode

  • Pingback: Geek Reading February 19, 2013 | Regular Geek

  • Pingback: Using a WHERE clause to filter paths « Another Word For It

  • http://andypalmer.com Andy Palmer

    I’m not sure if my comments are in moderation, or if Disqus is playing up.

    What do you get if you try:

    START team = node:teams(‘name:”Arsenal”‘)
    MATCH player-[:played]-a_game-[:for]-team
    RETURN player.name, COUNT(player.name) as games
    ORDER BY games desc

  • http://www.markhneedham.com/blog Mark Needham

    @andypalmer:disqus yep that gives the same result as the query I specified

  • Pingback: neo4j/cypher: Combining COUNT and COLLECT in one query at Mark Needham