Neo4j 2.0: Labels, indexes and the like
Date: 2013-10-22 20:20:30

Last week I did a couple of talks about modelling with Neo4j meet ups in http://www.meetup.com/graphdb-netherlands/events/137335992/[Amsterdam] and http://www.meetup.com/graphdb-belgium/events/137336622/[Antwerp] and there were a few questions about how indexing works with http://www.terminalstate.net/2013/05/labels-and-schema-indexes-in-neo4j.html?m=1[labels that are being introduced in Neo4j 2.0]

As well as defining properties on nodes we can also assign them a label which can be used to categorise different groups of nodes.

For example in the football graph we might choose to tag player nodes with the label 'Player':

[source,cypher]
----

CREATE (randomPlayer:Player {name: "Random Player"})
----

If we then wanted to find that player we could use the following query:

[source,cypher]
----

MATCH (p:Player)
WHERE p.name = "Random Player"
RETURN p
----

A common assumption amongst the attendees was that labelled nodes are automatically indexed but this isn't actually the case which we can see by profiling the above query:

[source,cypher]
----

$ PROFILE MATCH (p:Player) WHERE p.name = "Random Player" RETURN p;
==> +-----------------------------------+
==> | p                                 |
==> +-----------------------------------+
==> | Node[31382]{name:"Random Player"} |
==> +-----------------------------------+
==> 1 row
==>
==> Filter(pred="(Product(p,name(0),true) == Literal(Random Player) AND hasLabel(p:Player(8)))", _rows=1, _db_hits=524)
==> NodeByLabel(label="Player", identifier="p", _rows=524, _db_hits=0)
----

Instead what we have is a 'label scan' whereby we search across the nodes labelled as 'Player' check whether they have a property 'name' which matches 'Random Player' and then return them if they do.

This is different than doing a 'full node scan', checking for the appropriate label and then property. e.g.

[source,cypher]
----

$ PROFILE MATCH p WHERE "Player" IN LABELS(p) AND p.name = "Random Player" RETURN p;
==> +-----------------------------------+
==> | p                                 |
==> +-----------------------------------+
==> | Node[31382]{name:"Random Player"} |
==> +-----------------------------------+
==> 1 row
==>
==> Filter(pred="(any(-_-INNER-_- in LabelsFunction(p) where Literal(Player) == -_-INNER-_-) AND Product(p,name(0),true) == Literal(Random Player))", _rows=1, _db_hits=524)
==> AllNodes(identifier="p", _rows=11443, _db_hits=11443)
----

If we want to index a specific property of 'Player' nodes then need to http://docs.neo4j.org/chunked/milestone/query-schema-index.html[explicitly index that property for that label]:

[source,cypher]
----

$ CREATE INDEX ON :Player(name);
==> +-------------------+
==> | No data returned. |
==> +-------------------+
==> Indexes added: 1
==> 0 ms
----

If we want to see the indexes defined on our database we can run the following command in webadmin:

[source,bash]
----

$ schema
==> Indexes
==>   ON :Player(name) ONLINE
==>
==> No constraints
----

or its equivalent in http://blog.neo4j.org/2013/10/neo4j-200-m06-introducing-neo4js-browser.html[Neo4j browser]:

image::{{<siteurl>}}/uploads/2013/10/2013-10-22_21-14-32.png[2013 10 22 21 14 32,600]

Now if we repeat our initial query we can see that it's a straight schema/index lookup:

[source,cypher]
----

$ PROFILE MATCH (p:Player) WHERE p.name = "Random Player" RETURN p;
==> +-----------------------------------+
==> | p                                 |
==> +-----------------------------------+
==> | Node[31382]{name:"Random Player"} |
==> +-----------------------------------+
==> 1 row
==>
==> SchemaIndex(identifier="p", _db_hits=0, _rows=1, label="Player", query="Literal(Random Player)", property="name")
----

Based on a few runs of the query with and without the index defined it takes 1ms and 10ms respectively. The 'full node scan' approach takes ~ 40ms and that's with a very small database of 30,000 nodes. I wouldn't recommend it with a production load.