In this post we work through an end-to-end example of movie analytics with Neo4j. We start by building the graph, and then query it to answer questions about actors, directors, producers, writers, and the people who review the films.
Create the Movie Dataset in Neo4j
We do not need to hunt for a dataset: the Movie graph ships with every Neo4j installation as a built-in example. It is the dataset behind the :play movies guide in Neo4j Browser, a small graph of around 170 nodes built from Movie and Person nodes connected by ACTED_IN, DIRECTED, PRODUCED, WROTE, REVIEWED, and FOLLOWS relationships. Being tiny, well known, and richly connected makes it ideal for learning Cypher and for demonstrating graph analytics.
In Neo4j Browser you can load the same data interactively by running :guide movies (or :play movies on older versions) and clicking through the guide. Running the Cypher below yourself is equivalent, and has the advantage that the whole dataset is reproducible from this page.
Adding uniqueness constraints
Before importing, we add uniqueness constraints on the two identifying properties. Besides preventing duplicates, each constraint creates an index that makes the MERGE statements below much faster:
CREATE CONSTRAINT movie_title IF NOT EXISTS
FOR (m:Movie) REQUIRE m.title IS UNIQUE;
CREATE CONSTRAINT person_name IF NOT EXISTS
FOR (p:Person) REQUIRE p.name IS UNIQUE;
Importing the movies, people, and reviews
The script below creates the entire Movie graph. It is written with MERGE throughout, so it is idempotent: running it twice will not create duplicate nodes or relationships. Each block introduces one film, the people involved in it, and the relationships that connect them, and the final block adds the reviewers and their REVIEWED and FOLLOWS relationships.
MERGE (TheMatrix:Movie {title:'The Matrix'})
ON CREATE SET TheMatrix.released=1999,
TheMatrix.tagline='Welcome to the Real World'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'}) ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix)
MERGE (LillyW)-[:DIRECTED]->(TheMatrix)
MERGE (LanaW)-[:DIRECTED]->(TheMatrix)
MERGE (JoelS)-[:PRODUCED]->(TheMatrix)
MERGE (Emil:Person {name:'Emil Eifrem'}) ON CREATE SET Emil.born=1978
MERGE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix);
MERGE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded'})
ON CREATE SET TheMatrixReloaded.released=2003,
TheMatrixReloaded.tagline='Free your mind'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'}) ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded)
MERGE (LillyW)-[:DIRECTED]->(TheMatrixReloaded)
MERGE (LanaW)-[:DIRECTED]->(TheMatrixReloaded)
MERGE (JoelS)-[:PRODUCED]->(TheMatrixReloaded);
MERGE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions'})
ON CREATE SET TheMatrixRevolutions.released=2003,
TheMatrixRevolutions.tagline='Everything that has a beginning has an end'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Carrie:Person {name:'Carrie-Anne Moss'}) ON CREATE SET Carrie.born=1967
MERGE (Laurence:Person {name:'Laurence Fishburne'})
ON CREATE SET Laurence.born=1961
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions)
MERGE (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions)
MERGE (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions)
MERGE (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions)
MERGE (LillyW)-[:DIRECTED]->(TheMatrixRevolutions)
MERGE (LanaW)-[:DIRECTED]->(TheMatrixRevolutions)
MERGE (JoelS)-[:PRODUCED]->(TheMatrixRevolutions);
MERGE (TheDevilsAdvocate:Movie {
title:"The Devil's Advocate",
released:1997,
tagline:'Evil has its winning ways'})
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Charlize:Person {name:'Charlize Theron'})
ON CREATE SET Charlize.born=1975
MERGE (Al:Person {name:'Al Pacino'}) ON CREATE SET Al.born=1940
MERGE (Taylor:Person {name:'Taylor Hackford'}) ON CREATE SET Taylor.born=1944
MERGE (Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate)
MERGE (Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate)
MERGE (Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate)
MERGE (Taylor)-[:DIRECTED]->(TheDevilsAdvocate);
MERGE (AFewGoodMen:Movie {title:'A Few Good Men'})
ON CREATE SET AFewGoodMen.released=1992,
AFewGoodMen.tagline='In the heart of the nation\'s capital, in a courthouse
of the U.S. government, one man will stop at nothing to keep his honor, and
one will stop at nothing to find the truth.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DemiM:Person {name:'Demi Moore'}) ON CREATE SET DemiM.born=1962
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (KieferS:Person {name:'Kiefer Sutherland'})
ON CREATE SET KieferS.born=1966
MERGE (NoahW:Person {name:'Noah Wyle'}) ON CREATE SET NoahW.born=1971
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (KevinP:Person {name:'Kevin Pollak'}) ON CREATE SET KevinP.born=1957
MERGE (JTW:Person {name:'J.T. Walsh'}) ON CREATE SET JTW.born=1943
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (ChristopherG:Person {name:'Christopher Guest'})
ON CREATE SET ChristopherG.born=1948
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (AaronS:Person {name:'Aaron Sorkin'}) ON CREATE SET AaronS.born=1961
MERGE (TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen)
MERGE (JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen)
MERGE (DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen)
MERGE (KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen)
MERGE (KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen)
MERGE (NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen)
MERGE (CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen)
MERGE (KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen)
MERGE (JTW)-[:ACTED_IN {
roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen)
MERGE (JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen)
MERGE (ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen)
MERGE (AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen)
MERGE (RobR)-[:DIRECTED]->(AFewGoodMen)
MERGE (AaronS)-[:WROTE]->(AFewGoodMen);
MERGE (TopGun:Movie {title:'Top Gun'})
ON CREATE SET TopGun.released=1986,
TopGun.tagline='I feel the need, the need for speed.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (KellyM:Person {name:'Kelly McGillis'}) ON CREATE SET KellyM.born=1957
MERGE (ValK:Person {name:'Val Kilmer'}) ON CREATE SET ValK.born=1959
MERGE (AnthonyE:Person {name:'Anthony Edwards'})
ON CREATE SET AnthonyE.born=1962
MERGE (TomS:Person {name:'Tom Skerritt'}) ON CREATE SET TomS.born=1933
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (TonyS:Person {name:'Tony Scott'}) ON CREATE SET TonyS.born=1944
MERGE (JimC:Person {name:'Jim Cash'}) ON CREATE SET JimC.born=1941
MERGE (TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun)
MERGE (KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun)
MERGE (ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun)
MERGE (AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun)
MERGE (TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun)
MERGE (MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun)
MERGE (TonyS)-[:DIRECTED]->(TopGun)
MERGE (JimC)-[:WROTE]->(TopGun);
MERGE (JerryMaguire:Movie {title:'Jerry Maguire'})
ON CREATE SET JerryMaguire.released=2000,
JerryMaguire.tagline='The rest of his life begins now.'
MERGE (TomC:Person {name:'Tom Cruise'}) ON CREATE SET TomC.born=1962
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (ReneeZ:Person {name:'Renee Zellweger'}) ON CREATE SET ReneeZ.born=1969
MERGE (KellyP:Person {name:'Kelly Preston'}) ON CREATE SET KellyP.born=1962
MERGE (JerryO:Person {name:'Jerry O\'Connell'}) ON CREATE SET JerryO.born=1974
MERGE (JayM:Person {name:'Jay Mohr'}) ON CREATE SET JayM.born=1970
MERGE (BonnieH:Person {name:'Bonnie Hunt'}) ON CREATE SET BonnieH.born=1961
MERGE (ReginaK:Person {name:'Regina King'}) ON CREATE SET ReginaK.born=1971
MERGE (JonathanL:Person {name:'Jonathan Lipnicki'})
ON CREATE SET JonathanL.born=1996
MERGE (CameronC:Person {name:'Cameron Crowe'}) ON CREATE SET CameronC.born=1957
MERGE (TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire)
MERGE (CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire)
MERGE (ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire)
MERGE (KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire)
MERGE (JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire)
MERGE (JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire)
MERGE (BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire)
MERGE (ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire)
MERGE (JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire)
MERGE (CameronC)-[:DIRECTED]->(JerryMaguire)
MERGE (CameronC)-[:PRODUCED]->(JerryMaguire)
MERGE (CameronC)-[:WROTE]->(JerryMaguire);
MERGE (StandByMe:Movie {title:'Stand By Me'})
ON CREATE SET StandByMe.released=1986,
StandByMe.tagline='For some, it\'s the last real taste of innocence, and
the first real taste of life. But for everyone, it\'s the time that
memories are made of.'
MERGE (RiverP:Person {name:'River Phoenix'}) ON CREATE SET RiverP.born=1970
MERGE (CoreyF:Person {name:'Corey Feldman'}) ON CREATE SET CoreyF.born=1971
MERGE (JerryO:Person {name:'Jerry O\'Connell'}) ON CREATE SET JerryO.born=1974
MERGE (WilW:Person {name:'Wil Wheaton'}) ON CREATE SET WilW.born=1972
MERGE (KieferS:Person {name:'Kiefer Sutherland'})
ON CREATE SET KieferS.born=1966
MERGE (JohnC:Person {name:'John Cusack'}) ON CREATE SET JohnC.born=1966
MERGE (MarshallB:Person {name:'Marshall Bell'})
ON CREATE SET MarshallB.born=1942
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe)
MERGE (RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe)
MERGE (JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe)
MERGE (CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe)
MERGE (JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe)
MERGE (KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe)
MERGE (MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe)
MERGE (RobR)-[:DIRECTED]->(StandByMe);
MERGE (AsGoodAsItGets:Movie {title:'As Good as It Gets'})
ON CREATE SET AsGoodAsItGets.released=1997,
AsGoodAsItGets.tagline='A comedy from the heart that goes for the throat.'
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (GregK:Person {name:'Greg Kinnear'}) ON CREATE SET GregK.born=1963
MERGE (JamesB:Person {name:'James L. Brooks'}) ON CREATE SET JamesB.born=1940
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets)
MERGE (HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets)
MERGE (GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets)
MERGE (CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets)
MERGE (JamesB)-[:DIRECTED]->(AsGoodAsItGets);
MERGE (WhatDreamsMayCome:Movie {title:'What Dreams May Come'})
ON CREATE SET WhatDreamsMayCome.released=1998,
WhatDreamsMayCome.tagline='After life there is more.
The end is just the beginning.'
MERGE (AnnabellaS:Person {name:'Annabella Sciorra'})
ON CREATE SET AnnabellaS.born=1960
MERGE (MaxS:Person {name:'Max von Sydow'})
ON CREATE SET MaxS.born=1929
MERGE (WernerH:Person {name:'Werner Herzog'}) ON CREATE SET WernerH.born=1942
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (VincentW:Person {name:'Vincent Ward'}) ON CREATE SET VincentW.born=1956
MERGE (CubaG:Person {name:'Cuba Gooding Jr.'}) ON CREATE SET CubaG.born=1968
MERGE (Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome)
MERGE (CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome)
MERGE (AnnabellaS)-[
:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome)
MERGE (MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome)
MERGE (WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome)
MERGE (VincentW)-[:DIRECTED]->(WhatDreamsMayCome);
MERGE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars'})
ON CREATE SET SnowFallingonCedars.released=1999,
SnowFallingonCedars.tagline='First loves last. Forever.'
MERGE (EthanH:Person {name:'Ethan Hawke'}) ON CREATE SET EthanH.born=1970
MERGE (RickY:Person {name:'Rick Yune'}) ON CREATE SET RickY.born=1971
MERGE (JamesC:Person {name:'James Cromwell'}) ON CREATE SET JamesC.born=1940
MERGE (ScottH:Person {name:'Scott Hicks'}) ON CREATE SET ScottH.born=1953
MERGE (MaxS:Person {name:'Max von Sydow'}) ON CREATE SET MaxS.born=1929
MERGE (EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars)
MERGE (RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars)
MERGE (MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars)
MERGE (JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars)
MERGE (ScottH)-[:DIRECTED]->(SnowFallingonCedars);
MERGE (YouveGotMail:Movie {title:'You\'ve Got Mail'})
ON CREATE SET YouveGotMail.released=1998,
YouveGotMail.tagline='At odds in life... in love on-line.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (GregK:Person {name:'Greg Kinnear'}) ON CREATE SET GregK.born=1963
MERGE (ParkerP:Person {name:'Parker Posey'}) ON CREATE SET ParkerP.born=1968
MERGE (DaveC:Person {name:'Dave Chappelle'}) ON CREATE SET DaveC.born=1973
MERGE (SteveZ:Person {name:'Steve Zahn'}) ON CREATE SET SteveZ.born=1967
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail)
MERGE (MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail)
MERGE (GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail)
MERGE (ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail)
MERGE (DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail)
MERGE (SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail)
MERGE (NoraE)-[:DIRECTED]->(YouveGotMail);
MERGE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle'})
ON CREATE SET SleeplessInSeattle.released=1993,
SleeplessInSeattle.tagline='What if someone you never met,
someone you never saw, someone you never knew was the only someone for you?'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (RitaW:Person {name:'Rita Wilson'}) ON CREATE SET RitaW.born=1956
MERGE (BillPull:Person {name:'Bill Pullman'}) ON CREATE SET BillPull.born=1953
MERGE (VictorG:Person {name:'Victor Garber'}) ON CREATE SET VictorG.born=1949
MERGE (RosieO:Person {name:'Rosie O\'Donnell'}) ON CREATE SET RosieO.born=1962
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle)
MERGE (MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle)
MERGE (RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle)
MERGE (BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle)
MERGE (VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle)
MERGE (RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle)
MERGE (NoraE)-[:DIRECTED]->(SleeplessInSeattle);
MERGE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano'})
ON CREATE SET JoeVersustheVolcano.released=1990,
JoeVersustheVolcano.tagline='A story of love, lava and burning desire.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (JohnS:Person {name:'John Patrick Stanley'})
ON CREATE SET JohnS.born=1950
MERGE (Nathan:Person {name:'Nathan Lane'}) ON CREATE SET Nathan.born=1956
MERGE (TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano)
MERGE (MegR)-[:ACTED_IN {
roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]
->(JoeVersustheVolcano)
MERGE (Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano)
MERGE (JohnS)-[:DIRECTED]->(JoeVersustheVolcano);
MERGE (WhenHarryMetSally:Movie {title:'When Harry Met Sally'})
ON CREATE SET WhenHarryMetSally.released=1998,
WhenHarryMetSally.tagline='Can two friends sleep together and still love
each other in the morning?'
MERGE (MegR:Person {name:'Meg Ryan'}) ON CREATE SET MegR.born=1961
MERGE (BillyC:Person {name:'Billy Crystal'}) ON CREATE SET BillyC.born=1948
MERGE (CarrieF:Person {name:'Carrie Fisher'}) ON CREATE SET CarrieF.born=1956
MERGE (BrunoK:Person {name:'Bruno Kirby'}) ON CREATE SET BrunoK.born=1949
MERGE (RobR:Person {name:'Rob Reiner'}) ON CREATE SET RobR.born=1947
MERGE (NoraE:Person {name:'Nora Ephron'}) ON CREATE SET NoraE.born=1941
MERGE (BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally)
MERGE (MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally)
MERGE (CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally)
MERGE (BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally)
MERGE (RobR)-[:DIRECTED]->(WhenHarryMetSally)
MERGE (RobR)-[:PRODUCED]->(WhenHarryMetSally)
MERGE (NoraE)-[:PRODUCED]->(WhenHarryMetSally)
MERGE (NoraE)-[:WROTE]->(WhenHarryMetSally);
MERGE (ThatThingYouDo:Movie {title:'That Thing You Do'})
ON CREATE SET ThatThingYouDo.released=1996,
ThatThingYouDo.tagline='In every life there comes a time when that
thing you dream becomes that thing you do'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (LivT:Person {name:'Liv Tyler'}) ON CREATE SET LivT.born=1977
MERGE (Charlize:Person {name:'Charlize Theron'})
ON CREATE SET Charlize.born=1975
MERGE (TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo)
MERGE (LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo)
MERGE (Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo)
MERGE (TomH)-[:DIRECTED]->(ThatThingYouDo);
MERGE (TheReplacements:Movie {title:'The Replacements'})
ON CREATE SET TheReplacements.released=2000,
TheReplacements.tagline='Pain heals, Chicks dig scars... Glory lasts forever'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Brooke:Person {name:'Brooke Langton'}) ON CREATE SET Brooke.born=1970
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (Orlando:Person {name:'Orlando Jones'}) ON CREATE SET Orlando.born=1968
MERGE (Howard:Person {name:'Howard Deutch'}) ON CREATE SET Howard.born=1950
MERGE (Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements)
MERGE (Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements)
MERGE (Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements)
MERGE (Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements)
MERGE (Howard)-[:DIRECTED]->(TheReplacements);
MERGE (RescueDawn:Movie {title:'RescueDawn'})
ON CREATE SET RescueDawn.released=2006,
RescueDawn.tagline='Based on the extraordinary true story of
one man\'s fight for freedom'
MERGE (ChristianB:Person {name:'Christian Bale'})
ON CREATE SET ChristianB.born=1974
MERGE (ZachG:Person {name:'Zach Grenier'}) ON CREATE SET ZachG.born=1954
MERGE (MarshallB:Person {name:'Marshall Bell'})
ON CREATE SET MarshallB.born=1942
MERGE (SteveZ:Person {name:'Steve Zahn'}) ON CREATE SET SteveZ.born=1967
MERGE (WernerH:Person {name:'Werner Herzog'}) ON CREATE SET WernerH.born=1942
MERGE (MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn)
MERGE (ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn)
MERGE (ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn)
MERGE (SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn)
MERGE (WernerH)-[:DIRECTED]->(RescueDawn);
MERGE (TheBirdcage:Movie {title:'The Birdcage'})
ON CREATE SET TheBirdcage.released=1996,
TheBirdcage.tagline='Come as you are'
MERGE (MikeN:Person {name:'Mike Nichols'}) ON CREATE SET MikeN.born=1931
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (Nathan:Person {name:'Nathan Lane'}) ON CREATE SET Nathan.born=1956
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage)
MERGE (Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage)
MERGE (Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage)
MERGE (MikeN)-[:DIRECTED]->(TheBirdcage);
MERGE (Unforgiven:Movie {title:'Unforgiven'})
ON CREATE SET Unforgiven.released=1992,
Unforgiven.tagline='It\'s a hell of a thing, killing a man'
MERGE (Gene:Person {name:'Gene Hackman'}) ON CREATE SET Gene.born=1930
MERGE (RichardH:Person {name:'Richard Harris'}) ON CREATE SET RichardH.born=1930
MERGE (ClintE:Person {name:'Clint Eastwood'}) ON CREATE SET ClintE.born=1930
MERGE (RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven)
MERGE (ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven)
MERGE (Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven)
MERGE (ClintE)-[:DIRECTED]->(Unforgiven);
MERGE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic'})
ON CREATE SET JohnnyMnemonic.released=1995,
JohnnyMnemonic.tagline='The hottest data on earth. In the
coolest head in town'
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (Takeshi:Person {name:'Takeshi Kitano'}) ON CREATE SET Takeshi.born=1947
MERGE (Dina:Person {name:'Dina Meyer'}) ON CREATE SET Dina.born=1968
MERGE (IceT:Person {name:'Ice-T'}) ON CREATE SET IceT.born=1958
MERGE (RobertL:Person {name:'Robert Longo'}) ON CREATE SET RobertL.born=1953
MERGE (Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic)
MERGE (Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic)
MERGE (Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic)
MERGE (IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic)
MERGE (RobertL)-[:DIRECTED]->(JohnnyMnemonic);
MERGE (CloudAtlas:Movie {title:'Cloud Atlas'})
ON CREATE SET CloudAtlas.released=2012,
CloudAtlas.tagline='Everything is connected'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (HalleB:Person {name:'Halle Berry'}) ON CREATE SET HalleB.born=1966
MERGE (JimB:Person {name:'Jim Broadbent'}) ON CREATE SET JimB.born=1949
MERGE (TomT:Person {name:'Tom Tykwer'}) ON CREATE SET TomT.born=1965
MERGE (DavidMitchell:Person {name:'David Mitchell'})
ON CREATE SET DavidMitchell.born=1969
MERGE (StefanArndt:Person {name:'Stefan Arndt'})
ON CREATE SET StefanArndt.born=1961
MERGE (LillyW:Person {name:'Lilly Wachowski'})
ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'})
ON CREATE SET LanaW.born=1965
MERGE (TomH)-[:ACTED_IN {
roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]
->(CloudAtlas)
MERGE (Hugo)-[:ACTED_IN {
roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring',
'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas)
MERGE (HalleB)-[:ACTED_IN {
roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas)
MERGE (JimB)-[:ACTED_IN {
roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]
->(CloudAtlas)
MERGE (TomT)-[:DIRECTED]->(CloudAtlas)
MERGE (LillyW)-[:DIRECTED]->(CloudAtlas)
MERGE (LanaW)-[:DIRECTED]->(CloudAtlas)
MERGE (DavidMitchell)-[:WROTE]->(CloudAtlas)
MERGE (StefanArndt)-[:PRODUCED]->(CloudAtlas);
MERGE (TheDaVinciCode:Movie {title:'The Da Vinci Code'})
ON CREATE SET TheDaVinciCode.released=2006,
TheDaVinciCode.tagline='Break The Codes'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (IanM:Person {name:'Ian McKellen'}) ON CREATE SET IanM.born=1939
MERGE (AudreyT:Person {name:'Audrey Tautou'}) ON CREATE SET AudreyT.born=1976
MERGE (PaulB:Person {name:'Paul Bettany'}) ON CREATE SET PaulB.born=1971
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode)
MERGE (IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode)
MERGE (AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode)
MERGE (PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode)
MERGE (RonH)-[:DIRECTED]->(TheDaVinciCode);
MERGE (VforVendetta:Movie {title:'V for Vendetta'})
ON CREATE SET VforVendetta.released=2006,
VforVendetta.tagline='Freedom! Forever!'
MERGE (Hugo:Person {name:'Hugo Weaving'}) ON CREATE SET Hugo.born=1960
MERGE (NatalieP:Person {name:'Natalie Portman'})
ON CREATE SET NatalieP.born=1981
MERGE (StephenR:Person {name:'Stephen Rea'}) ON CREATE SET StephenR.born=1946
MERGE (JohnH:Person {name:'John Hurt'}) ON CREATE SET JohnH.born=1940
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta)
MERGE (NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta)
MERGE (StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta)
MERGE (JohnH)-[:ACTED_IN {
roles:['High Chancellor Adam Sutler']}]->(VforVendetta)
MERGE (BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta)
MERGE (JamesM)-[:DIRECTED]->(VforVendetta)
MERGE (LillyW)-[:PRODUCED]->(VforVendetta)
MERGE (LanaW)-[:PRODUCED]->(VforVendetta)
MERGE (JoelS)-[:PRODUCED]->(VforVendetta)
MERGE (LillyW)-[:WROTE]->(VforVendetta)
MERGE (LanaW)-[:WROTE]->(VforVendetta);
MERGE (SpeedRacer:Movie {title:'Speed Racer'})
ON CREATE SET SpeedRacer.released=2008,
SpeedRacer.tagline='Speed has no limits'
MERGE (EmileH:Person {name:'Emile Hirsch'}) ON CREATE SET EmileH.born=1985
MERGE (JohnG:Person {name:'John Goodman'}) ON CREATE SET JohnG.born=1960
MERGE (SusanS:Person {name:'Susan Sarandon'}) ON CREATE SET SusanS.born=1946
MERGE (MatthewF:Person {name:'Matthew Fox'}) ON CREATE SET MatthewF.born=1966
MERGE (ChristinaR:Person {name:'Christina Ricci'})
ON CREATE SET ChristinaR.born=1980
MERGE (Rain:Person {name:'Rain'}) ON CREATE SET Rain.born=1982
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer)
MERGE (JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer)
MERGE (SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer)
MERGE (MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer)
MERGE (ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer)
MERGE (Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer)
MERGE (BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer)
MERGE (LillyW)-[:DIRECTED]->(SpeedRacer)
MERGE (LanaW)-[:DIRECTED]->(SpeedRacer)
MERGE (LillyW)-[:WROTE]->(SpeedRacer)
MERGE (LanaW)-[:WROTE]->(SpeedRacer)
MERGE (JoelS)-[:PRODUCED]->(SpeedRacer);
MERGE (NinjaAssassin:Movie {title:'Ninja Assassin'})
ON CREATE SET NinjaAssassin.released=2009,
NinjaAssassin.tagline='Prepare to enter a secret world of assassins'
MERGE (NaomieH:Person {name:'Naomie Harris'})
MERGE (Rain:Person {name:'Rain'}) ON CREATE SET Rain.born=1982
MERGE (BenM:Person {name:'Ben Miles'}) ON CREATE SET BenM.born=1967
MERGE (LillyW:Person {name:'Lilly Wachowski'}) ON CREATE SET LillyW.born=1967
MERGE (LanaW:Person {name:'Lana Wachowski'}) ON CREATE SET LanaW.born=1965
MERGE (RickY:Person {name:'Rick Yune'}) ON CREATE SET RickY.born=1971
MERGE (JamesM:Person {name:'James Marshall'}) ON CREATE SET JamesM.born=1967
MERGE (JoelS:Person {name:'Joel Silver'}) ON CREATE SET JoelS.born=1952
MERGE (Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin)
MERGE (NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin)
MERGE (RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin)
MERGE (BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin)
MERGE (JamesM)-[:DIRECTED]->(NinjaAssassin)
MERGE (LillyW)-[:PRODUCED]->(NinjaAssassin)
MERGE (LanaW)-[:PRODUCED]->(NinjaAssassin)
MERGE (JoelS)-[:PRODUCED]->(NinjaAssassin);
MERGE (TheGreenMile:Movie {title:'The Green Mile'})
ON CREATE SET TheGreenMile.released=1999,
TheGreenMile.tagline='Walk a mile you\'ll never forget.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (JamesC:Person {name:'James Cromwell'}) ON CREATE SET JamesC.born=1940
MERGE (BonnieH:Person {name:'Bonnie Hunt'}) ON CREATE SET BonnieH.born=1961
MERGE (MichaelD:Person {name:'Michael Clarke Duncan'})
ON CREATE SET MichaelD.born=1957
MERGE (DavidM:Person {name:'David Morse'}) ON CREATE SET DavidM.born=1953
MERGE (SamR:Person {name:'Sam Rockwell'}) ON CREATE SET SamR.born=1968
MERGE (GaryS:Person {name:'Gary Sinise'}) ON CREATE SET GaryS.born=1955
MERGE (PatriciaC:Person {name:'Patricia Clarkson'})
ON CREATE SET PatriciaC.born=1959
MERGE (FrankD:Person {name:'Frank Darabont'}) ON CREATE SET FrankD.born=1959
MERGE (TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile)
MERGE (MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile)
MERGE (DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile)
MERGE (BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile)
MERGE (JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile)
MERGE (SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile)
MERGE (GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile)
MERGE (PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile)
MERGE (FrankD)-[:DIRECTED]->(TheGreenMile);
MERGE (FrostNixon:Movie {title:'Frost/Nixon'})
ON CREATE SET FrostNixon.released=2008,
FrostNixon.tagline='400 million people were waiting for the truth.'
MERGE (FrankL:Person {name:'Frank Langella'}) ON CREATE SET FrankL.born=1938
MERGE (MichaelS:Person {name:'Michael Sheen'}) ON CREATE SET MichaelS.born=1969
MERGE (OliverP:Person {name:'Oliver Platt'}) ON CREATE SET OliverP.born=1960
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (SamR:Person {name:'Sam Rockwell'}) ON CREATE SET SamR.born=1968
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon)
MERGE (MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon)
MERGE (KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon)
MERGE (OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon)
MERGE (SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon)
MERGE (RonH)-[:DIRECTED]->(FrostNixon);
MERGE (Hoffa:Movie {title:'Hoffa'})
ON CREATE SET Hoffa.released=1992,
Hoffa.tagline='He didn\'t want law. He wanted justice.'
MERGE (DannyD:Person {name:'Danny DeVito'}) ON CREATE SET DannyD.born=1944
MERGE (JohnR:Person {name:'John C. Reilly'}) ON CREATE SET JohnR.born=1965
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (JTW:Person {name:'J.T. Walsh'}) ON CREATE SET JTW.born=1943
MERGE (JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa)
MERGE (DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa)
MERGE (JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa)
MERGE (JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa)
MERGE (DannyD)-[:DIRECTED]->(Hoffa);
MERGE (Apollo13:Movie {title:'Apollo 13'})
ON CREATE SET Apollo13.released=1995,
Apollo13.tagline='Houston, we have a problem.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (EdH:Person {name:'Ed Harris'}) ON CREATE SET EdH.born=1950
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (KevinB:Person {name:'Kevin Bacon'}) ON CREATE SET KevinB.born=1958
MERGE (GaryS:Person {name:'Gary Sinise'}) ON CREATE SET GaryS.born=1955
MERGE (RonH:Person {name:'Ron Howard'}) ON CREATE SET RonH.born=1954
MERGE (TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13)
MERGE (KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13)
MERGE (EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13)
MERGE (BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13)
MERGE (GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13)
MERGE (RonH)-[:DIRECTED]->(Apollo13);
MERGE (Twister:Movie {title:'Twister'})
ON CREATE SET Twister.released=1996,
Twister.tagline='Don\'t Breathe. Don\'t Look Back.'
MERGE (PhilipH:Person {name:'Philip Seymour Hoffman'})
ON CREATE SET PhilipH.born=1967
MERGE (JanB:Person {name:'Jan de Bont'}) ON CREATE SET JanB.born=1943
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (ZachG:Person {name:'Zach Grenier'}) ON CREATE SET ZachG.born=1954
MERGE (BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister)
MERGE (HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister)
MERGE (ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister)
MERGE (PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister)
MERGE (JanB)-[:DIRECTED]->(Twister);
MERGE (CastAway:Movie {title:'Cast Away'})
ON CREATE SET CastAway.released=2000,
CastAway.tagline='At the edge of the world, his journey begins.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (HelenH:Person {name:'Helen Hunt'}) ON CREATE SET HelenH.born=1963
MERGE (RobertZ:Person {name:'Robert Zemeckis'}) ON CREATE SET RobertZ.born=1951
MERGE (TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway)
MERGE (HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway)
MERGE (RobertZ)-[:DIRECTED]->(CastAway);
MERGE (OneFlewOvertheCuckoosNest:Movie {
title:'One Flew Over the Cuckoo\'s Nest'})
ON CREATE SET OneFlewOvertheCuckoosNest.released=1975,
OneFlewOvertheCuckoosNest.tagline='If he\'s crazy, what does that make you?'
MERGE (MilosF:Person {name:'Milos Forman'}) ON CREATE SET MilosF.born=1932
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DannyD:Person {name:'Danny DeVito'}) ON CREATE SET DannyD.born=1944
MERGE (JackN)-[:ACTED_IN {
roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest)
MERGE (DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest)
MERGE (MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest);
MERGE (SomethingsGottaGive:Movie {title:'Something\'s Gotta Give'})
ON CREATE SET SomethingsGottaGive.released=2003
MERGE (JackN:Person {name:'Jack Nicholson'}) ON CREATE SET JackN.born=1937
MERGE (DianeK:Person {name:'Diane Keaton'}) ON CREATE SET DianeK.born=1946
MERGE (NancyM:Person {name:'Nancy Meyers'}) ON CREATE SET NancyM.born=1949
MERGE (Keanu:Person {name:'Keanu Reeves'}) ON CREATE SET Keanu.born=1964
MERGE (JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive)
MERGE (DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive)
MERGE (Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive)
MERGE (NancyM)-[:DIRECTED]->(SomethingsGottaGive)
MERGE (NancyM)-[:PRODUCED]->(SomethingsGottaGive)
MERGE (NancyM)-[:WROTE]->(SomethingsGottaGive);
MERGE (BicentennialMan:Movie {title:'Bicentennial Man'})
ON CREATE SET BicentennialMan.released=1999,
BicentennialMan.tagline='One robot\'s 200 year journey to
become an ordinary man.'
MERGE (ChrisC:Person {name:'Chris Columbus'}) ON CREATE SET ChrisC.born=1958
MERGE (Robin:Person {name:'Robin Williams'}) ON CREATE SET Robin.born=1951
MERGE (OliverP:Person {name:'Oliver Platt'}) ON CREATE SET OliverP.born=1960
MERGE (Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan)
MERGE (OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan)
MERGE (ChrisC)-[:DIRECTED]->(BicentennialMan);
MERGE (CharlieWilsonsWar:Movie {title:'Charlie Wilson\'s War'})
ON CREATE SET CharlieWilsonsWar.released=2007,
CharlieWilsonsWar.tagline='A stiff drink. A little mascara.
A lot of nerve. Who said they couldn\'t bring down the Soviet empire.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (PhilipH:Person {name:'Philip Seymour Hoffman'})
ON CREATE SET PhilipH.born=1967
MERGE (JuliaR:Person {name:'Julia Roberts'}) ON CREATE SET JuliaR.born=1967
MERGE (MikeN:Person {name:'Mike Nichols'}) ON CREATE SET MikeN.born=1931
MERGE (TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar)
MERGE (JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar)
MERGE (PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar)
MERGE (MikeN)-[:DIRECTED]->(CharlieWilsonsWar);
MERGE (ThePolarExpress:Movie {title:'The Polar Express'})
ON CREATE SET ThePolarExpress.released=2004,
ThePolarExpress.tagline='This Holiday Season... Believe'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (RobertZ:Person {name:'Robert Zemeckis'}) ON CREATE SET RobertZ.born=1951
MERGE (TomH)-[:ACTED_IN {
roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge',
'Santa Claus']}]->(ThePolarExpress)
MERGE (RobertZ)-[:DIRECTED]->(ThePolarExpress);
MERGE (ALeagueofTheirOwn:Movie {title:'A League of Their Own'})
ON CREATE SET ALeagueofTheirOwn.released=1992,
ALeagueofTheirOwn.tagline='Once in a lifetime you get
a chance to do something different.'
MERGE (TomH:Person {name:'Tom Hanks'}) ON CREATE SET TomH.born=1956
MERGE (Madonna:Person {name:'Madonna'}) ON CREATE SET Madonna.born=1954
MERGE (GeenaD:Person {name:'Geena Davis'}) ON CREATE SET GeenaD.born=1956
MERGE (LoriP:Person {name:'Lori Petty'}) ON CREATE SET LoriP.born=1963
MERGE (PennyM:Person {name:'Penny Marshall'}) ON CREATE SET PennyM.born=1943
MERGE (RosieO:Person {name:'Rosie O\'Donnell'}) ON CREATE SET RosieO.born=1962
MERGE (BillPax:Person {name:'Bill Paxton'}) ON CREATE SET BillPax.born=1955
MERGE (TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn)
MERGE (GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn)
MERGE (LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn)
MERGE (RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn)
MERGE (Madonna)-[:ACTED_IN {
roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn)
MERGE (BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn)
MERGE (PennyM)-[:DIRECTED]->(ALeagueofTheirOwn);
MATCH (CloudAtlas:Movie {title:'Cloud Atlas'})
MATCH (TheReplacements:Movie {title:'The Replacements'})
MATCH (Unforgiven:Movie {title:'Unforgiven'})
MATCH (TheBirdcage:Movie {title:'The Birdcage'})
MATCH (TheDaVinciCode:Movie {title:'The Da Vinci Code'})
MATCH (JerryMaguire:Movie {title:'Jerry Maguire'})
MERGE (PaulBlythe:Person {name:'Paul Blythe'})
MERGE (AngelaScope:Person {name:'Angela Scope'})
MERGE (JessicaThompson:Person {name:'Jessica Thompson'})
MERGE (JamesThompson:Person {name:'James Thompson'})
MERGE (JamesThompson)-[:FOLLOWS]->(JessicaThompson)
MERGE (AngelaScope)-[:FOLLOWS]->(JessicaThompson)
MERGE (PaulBlythe)-[:FOLLOWS]->(AngelaScope)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'An amazing journey', rating:95}]->(CloudAtlas)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'Silly, but fun', rating:65}]->(TheReplacements)
MERGE (JamesThompson)-[:REVIEWED {
summary:'The coolest football movie ever', rating:100}]->(TheReplacements)
MERGE (AngelaScope)-[:REVIEWED {
summary:'Pretty funny at times', rating:62}]->(TheReplacements)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'Dark, but compelling', rating:85}]->(Unforgiven)
MERGE (JessicaThompson)-[:REVIEWED {
summary:"Slapstick redeemed only by the Robin Williams and
Gene Hackman's stellar performances", rating:45}]->(TheBirdcage)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'A solid romp', rating:68}]->(TheDaVinciCode)
MERGE (JamesThompson)-[:REVIEWED {
summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode)
MERGE (JessicaThompson)-[:REVIEWED {
summary:'You had me at Jerry', rating:92}]->(JerryMaguire);
Inspecting the schema
Before writing queries, it helps to see the shape of the graph we have just created. Neo4j keeps track of every label and relationship type it has seen, and exposes that summary through a built-in procedure:
CALL db.schema.visualization
CALL invokes a stored procedure rather than running a pattern match, and db.schema.visualization returns two collections: the nodes it has observed (one per label) and the relationships between them (one per relationship type). The Neo4j Browser renders those collections as a small graph, so the result is a meta-graph: each circle is a label, not an individual node, and each arrow is a relationship type, not an individual relationship.
The procedure reads from the database’s internal token store, not from the data itself, so it is fast even on large graphs. The flip side is that it describes what can exist rather than what does: a label or relationship type that was created and later deleted may still appear until the store is recomputed.
Movie and Person, joined by five relationship types pointing from Person to Movie, plus a self-referencing FOLLOWS relationship between people.
Figure 1 confirms that the import behaved as intended. There are only two labels, Movie and Person, and every relationship carrying a person’s contribution to a film, ACTED_IN, DIRECTED, PRODUCED, WROTE, and REVIEWED, points from Person to Movie. The loop on Person is FOLLOWS, the one relationship that connects people to each other and gives the reviewers a small social network of their own. Knowing these directions matters: an arrow drawn the wrong way in a query pattern is one of the most common reasons a Cypher query silently returns no rows.
With the graph in place, we can start asking questions of it.
CRUD operations in Cypher
Before working through the query patterns in detail, it is worth seeing the four basic operations that every database supports, create, read, update, and delete, expressed in Cypher. Unlike SQL, which uses a different statement for each one (INSERT, SELECT, UPDATE, DELETE), Cypher builds all four from a small set of clauses:
| Operation | Cypher clause |
|---|---|
| Create | CREATE, or MERGE when duplicates must be avoided |
| Read | MATCH combined with WHERE and RETURN |
| Update | SET to write properties, REMOVE to drop them |
| Delete | DELETE, or DETACH DELETE for connected nodes |
The clauses in Table 1 combine freely, which is why an update is written as a MATCH followed by a SET rather than as a statement of its own. The examples below use a film that is not part of the Movie graph, Inception, so that the dataset built above is left untouched. The last example removes it again, which means the whole sequence can be run and re-run safely.
Creating a node
CREATE writes a new node into the graph:
//Query : Creating a Node
CREATE (m:Movie {title: 'Inception', released: 2010, duration: 148})
RETURN m
The pattern inside CREATE is written exactly like a MATCH pattern: m is the variable, :Movie is the label the new node is given, and the map in braces supplies its properties. Because CREATE always inserts, running this query twice would normally produce two identical Inception nodes; here the uniqueness constraint on Movie.title created earlier rejects the second attempt with an error instead. RETURN m hands the newly created node back so the browser can draw it.
Movie node drawn on its own, with the details panel listing the three properties it was given. The status bar underneath confirms what the write actually did: one node created, three properties set, and one label added.
Figure 2 shows the result. The node floats unconnected because nothing links it to the rest of the graph yet, and the caption is truncated to “Incepti-on” only because the browser is wrapping the text inside the circle. The <id> in the details panel is the internal element identifier Neo4j assigned to the node; it is not a property we set and should not be relied on as a key, which is precisely why the import script identifies films by title instead.
Use MERGE rather than CREATE whenever a node might already exist. MERGE matches the pattern first and only creates it if nothing was found, which is why the whole import script above is idempotent. CREATE is the right choice only when the data is known to be new.
Creating a relationship
Nodes on their own carry little information; the value of a graph lies in the edges between them. The query below adds an actor and connects him to the film we just created:
//Query: Creating a Relationship
CREATE (a:Person {name: 'Leonardo DiCaprio'})
WITH a
MATCH (m:Movie {title: 'Inception'})
CREATE (a)-[:ACTED_IN]->(m)
RETURN a, m
Three things are happening here. The first CREATE makes the Person node. WITH a then passes that node on to the rest of the query, which is necessary because a MATCH cannot follow a CREATE directly; WITH acts as a pipe between query parts, carrying forward only the variables it names. The MATCH finds the existing Inception node, and the second CREATE draws the relationship between the two. Note that the relationship pattern (a)-[:ACTED_IN]->(m) reuses the variables a and m without repeating their labels or properties, because both nodes are already bound.
Person. The status bar reports one node created, one relationship created, one property set, and one label added.
Figure 3 is worth looking at carefully, because the picture and the status bar appear to disagree. The bar confirms that a relationship was created, yet no arrow is drawn between the two circles. Nothing has gone wrong: RETURN a, m asked for the two nodes and nothing else, and the browser draws only what the query returned. Adding the relationship to the result makes it visible:
MATCH (a:Person {name: 'Leonardo DiCaprio'})-[r:ACTED_IN]->(m:Movie)
RETURN a, r, m
Naming the relationship r in the pattern binds it to a variable in the same way a and m bind the nodes, and returning it gives the browser the edge to draw.
The arrow direction is part of the data, not decoration. Writing (m)-[:ACTED_IN]->(a) would store the claim that the film acted in the person, and later queries written the conventional way round would silently miss it.
Reading data
Reading is what the rest of this post is about, but its simplest form is a MATCH followed by a RETURN:
//Query: Reading Data
MATCH (m:Movie)
RETURN m.title, m.released
MATCH (m:Movie) binds every film in the graph to m, and the RETURN picks two properties from each one. Since the result is made of plain values rather than nodes, the browser shows a table.
Reading data with a filter
Adding a WHERE clause between MATCH and RETURN narrows the rows down:
//Query: Reading Data with a Filter
MATCH (m:Movie)
WHERE m.released > 2000
RETURN m.title, m.released
The MATCH still produces every film, and WHERE then keeps only those for which the condition evaluates to true. Section 4 works through the filtering techniques that build on this pattern.
Updating a node
SET overwrites the value of an existing property:
//Query: Updating a Node
MATCH (m:Movie {title: 'Inception'})
SET m.duration = 150
RETURN m
An update is always a read followed by a write: the MATCH locates the node first, and SET then changes it. Every node the pattern matches is updated, so a MATCH that is too loose will change more of the graph than intended. Here the inline {title: 'Inception'} filter pins the match to a single film.
duration now 150 in the details panel while released and title are untouched. The status bar reads “Set 1 property”, confirming that a single write took place.
Figure 4 shows that SET is surgical: only the property named on the left of the = is written, and the rest of the node is left exactly as it was. The <id> is also unchanged from Figure 2, which confirms that this is the same node being modified in place rather than a replacement. Because SET overwrites unconditionally, the previous value of 148 is gone; Cypher keeps no history, so recovering it would mean writing it back explicitly.
Adding a new property
The same clause also creates properties that did not exist before:
//Query: Adding a New Property
MATCH (m:Movie {title: 'Inception'})
SET m.genre = 'Science Fiction'
RETURN m
Cypher makes no distinction between changing a property and adding one: because Neo4j is schema-flexible, SET simply writes the key and value onto the node. Nothing forces the other Movie nodes to acquire a genre property as well, which is convenient but also the reason a misspelled property name returns null rather than an error. REMOVE m.genre would take the property away again.
Deleting a node
Finally, DETACH DELETE removes a node together with everything attached to it:
//Query: Deleting a Node
MATCH (m:Movie {title: 'Inception'})
DETACH DELETE m
Neo4j refuses to delete a node that still has relationships, because doing so would leave dangling edges behind. DETACH instructs it to remove the node’s relationships first, so this single statement deletes both the Inception node and the ACTED_IN edge created earlier. A plain DELETE m would fail here, and is only appropriate for isolated nodes. There is no RETURN because the node no longer exists once the statement completes.
The Person node for Leonardo DiCaprio survives this deletion; only the film and its relationships are gone. Running MATCH (p:Person {name: 'Leonardo DiCaprio'}) DELETE p restores the Movie graph to exactly the state the import script produced.
Matching nodes and returning properties
Every query in this post follows the same three-part skeleton:
MATCH
WHERE
RETURN
MATCH states the pattern to find, WHERE filters the rows that pattern produced, and RETURN says which parts of them to hand back. Only MATCH and RETURN are required; WHERE is optional. Reading a query in that order is usually enough to work out what it does. This section covers the two ends of that skeleton, the simplest patterns and the two ways of returning what they match; Section 4 takes up the middle.
Returning whole nodes
The simplest possible query asks for all nodes of one kind:
MATCH (m:Movie)
RETURN m
MATCH describes a pattern to look for in the graph, and here the pattern is as small as it can be: a single node. Inside the parentheses, m is a variable we invent so we can refer to the matched node later, and :Movie is a label that restricts the search to movie nodes. Because there is nothing else in the pattern, no relationships and no property filter, the pattern matches every Movie node in the database. RETURN m then hands each matched node back as a result row, with all of its properties (title, released, tagline) attached.
The label is what keeps this query fast. Thanks to the constraint we created earlier, Neo4j has an index on Movie nodes and can jump straight to them instead of scanning the whole store. Dropping the label and writing MATCH (m) RETURN m would return every node in the database, people included.
Movie nodes returned by the query, drawn as isolated circles labelled with each film’s title.
Figure 5 shows part of the result in the Neo4j Browser. Note that the nodes float unconnected: the query asked only for movies, so the people and the ACTED_IN, DIRECTED, and REVIEWED relationships that join them are simply not part of the result. The browser draws what the query returned, not the surrounding graph. To bring the relationships back we would have to put them in the pattern, for example MATCH (p:Person)-[r]->(m:Movie) RETURN p, r, m.
Also note the shortened captions in the circles, such as “V for Vendett…” and “Ninja Assassi…”. That is only the browser truncating long captions to fit inside the node; the full title property is intact and visible in the details panel when a node is selected, or in the table view.
The same pattern works for any label. Swapping Movie for Person returns every Person node in the graph, actors, directors, producers, writers, and reviewers alike, because Person is the only label the import gave them:
MATCH (n:Person)
RETURN n
name property as the caption. The browser truncates long names to fit inside the circle, but the full value is available in the details panel when a node is selected.
Returning specific properties
Returning whole nodes is convenient for exploring, but most of the time we want a few named values rather than every property. Instead of returning the variable itself, we return properties of it using dot notation:
// Query: Retrieving Specific Properties
MATCH (m:Movie)
RETURN m.title, m.releaseYear
The MATCH clause is unchanged: it still binds every Movie node to m. What changes is the RETURN. Writing m.title asks for the value of the title property on the matched node, so each result row now holds two scalar values rather than one node object. The line beginning with // is a Cypher comment and is ignored when the query runs.
Because the result is made of plain values instead of nodes, the Neo4j Browser has nothing to draw and shows a table instead of a graph. The column headers are taken literally from the expressions we wrote, which is why they read m.title and m.releaseYear.
m.releaseYear column is entirely null because no such property exists on these nodes.
Figure 7 shows an important Cypher behaviour. The titles come back as expected, but every value in the second column is null. Nothing has gone wrong with the data: the property is simply not called releaseYear. We created it as released, and asking for a property a node does not have returns null rather than raising an error.
This silent null is a common source of confusion. Cypher has no fixed schema, so it cannot know whether releaseYear is a typo or a property that only some nodes happen to carry. If a column comes back entirely null, suspect a misspelled property name first. Running CALL db.propertyKeys lists every property name the database knows about, which is a quick way to check the spelling.
Correcting the name gives the intended result:
MATCH (m:Movie)
RETURN m.title, m.released
Figure 8 shows the same table with real values: 1999 for The Matrix, 2003 for both Reloaded and Revolutions, and so on. The browser also renders the two columns differently, quoting the titles as strings while showing the years as bare numbers, which is a useful visual confirmation that released was stored as an integer rather than as text.
Filtering results
So far every query has returned all 38 movies. To narrow the result down to the films we are actually interested in, we add a WHERE clause. WHERE sits between MATCH and RETURN and acts as a predicate on the rows that MATCH produced: each candidate row is tested against the condition, and only the rows for which it evaluates to true survive to the RETURN. The examples below work through the filtering techniques that come up most often, starting with the simplest.
Filtering on an exact property value
The most direct filter tests a property for equality, which is how we pick a single named node out of the graph:
//Query: Filtering Based on Properties
MATCH (m:Movie)
WHERE m.title = 'The Matrix'
RETURN m
MATCH (m:Movie) still describes every film, but WHERE m.title = 'The Matrix' reduces the candidates to the one whose title property is exactly that string. Cypher’s = is case-sensitive and compares the whole value, so 'the matrix' would match nothing. Because RETURN m hands back the node itself rather than its properties, the browser has something to draw and shows a graph rather than a table.
m.title returns a single Movie node. The details panel on the right shows all of its properties, released 1999, tagline “Welcome to the Real World”, and title “The Matrix”, together with the internal <id> Neo4j assigned to it.
Figure 9 shows the result: one green circle, and one record streamed back in 22 ms. The filter is fast because the uniqueness constraint we created at the start put an index behind Movie.title, so Neo4j looks the value up directly instead of scanning all 38 films.
An equality test like this one can also be written inline in the pattern:
MATCH (m:Movie {title: 'The Matrix'})
RETURN m
The two queries are equivalent and the planner treats them the same way. The inline form is more compact, but it only supports exact matches, which is why WHERE remains the general tool for everything that follows.
Filtering on a numeric range
Swapping = for a comparison operator turns the same clause into a range filter:
//Query: Filtering Results
MATCH (m:Movie)
WHERE m.released > 2000
RETURN m.title, m.released
Because released is stored as an integer, > performs a numeric comparison; had the values been strings, the same expression would have compared them lexicographically and given misleading results.
Figure 7 returned every film in the database, whereas Figure 10 returns only 18 rows, beginning with The Matrix Reloaded (2003) and RescueDawn (2006). Note that the comparison is strict: a film released exactly in 2000, such as Cast Away, is excluded. Writing >= instead would include it.
Combining and negating conditions
Several predicates can be joined with AND, OR, and NOT. Using AND to combine two comparisons turns the open-ended filter above into a bounded window:
//Query: Filtering with Logical Operators
MATCH (m:Movie)
WHERE m.released >= 2000 AND m.released <= 2010
RETURN m.title, m.released
Both operands are evaluated against the same row, so a film is kept only if it satisfies the lower bound and the upper bound. Note that both comparisons are inclusive this time: >= keeps the films released exactly in 2000 that the strict > in Figure 10 discarded.
>= rather than the strict > used earlier.
Figure 11 returns 14 rows, four fewer than the 18 of Figure 10: the upper bound removes Cloud Atlas (2012) and the other later films, while the inclusive lower bound adds back the two from 2000. The rows come back in whatever order the planner produces them, which is why 2003 appears before 2000; adding ORDER BY m.released would sort them.
OR and NOT complete the set, and can be mixed with AND in one predicate:
MATCH (m:Movie)
WHERE m.released >= 2000 AND m.released <= 2010
AND NOT m.title = 'The Matrix Reloaded'
RETURN m.title, m.released
ORDER BY m.released
AND binds more tightly than OR, so parentheses are worth adding whenever the two are combined. The same range reads more clearly with the IN operator when the values are a fixed set rather than an interval, as in WHERE m.released IN [1999, 2003, 2012].
Matching on strings
Equality is too blunt an instrument for text, so Cypher adds three operators for partial matches, STARTS WITH, ENDS WITH, and CONTAINS:
// Query: Filtering with Regular Expressions
MATCH (m:Movie)
WHERE m.title STARTS WITH 'The'
RETURN m.title
STARTS WITH tests whether the property begins with the given prefix, so a film qualifies on the strength of its first three characters alone and the rest of the title is ignored. All three operators are case-sensitive, which matters here: a title stored as 'the Matrix' would not match 'The'. ENDS WITH and CONTAINS work the same way on the suffix and on any position in the string respectively, so WHERE m.title CONTAINS 'Matrix' would find the trilogy wherever the word appeared in the title.
Figure 12 shows the result, and the alphabetical ordering is worth noticing: no ORDER BY was written, but the rows arrive sorted anyway. That is a side effect of the index behind Movie.title, which stores values in sorted order and can be scanned from the point where 'The' begins. This is why prefix matching is efficient while the other two operators are not: ENDS WITH and CONTAINS cannot use the index that way and fall back to examining every value.
For anything more elaborate than a prefix, suffix, or substring, the =~ operator matches a property against a full regular expression:
MATCH (m:Movie)
WHERE m.title =~ '(?i)the matrix.*'
RETURN m.title
The (?i) flag makes the match case-insensitive, so the lowercase pattern still matches the capitalised titles. The trailing .* is needed because =~ matches the whole value rather than a fragment of it, so without it only The Matrix itself would qualify.
(?i) flag.
Figure 13 returns just 3 rows, against the 12 or so that STARTS WITH 'The' produced in Figure 12.
Filtering across a pattern
Everything so far has filtered a single node, but the predicate does not have to apply to the node the query starts from. Once relationships are in the pattern, a WHERE clause on one end filters the whole path:
// Query: Pattern-Based Filtering
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
WHERE m.released > 2000
RETURN a.name, m.title, m.released
The predicate is exactly the one from Figure 10, but the pattern around it has grown: instead of matching films on their own, the query walks the ACTED_IN relationship first and then discards any path whose film falls outside the range. Filtering and traversing compose freely in this way, which is what makes WHERE the general tool it is.
ACTED_IN relationship rather than one film.
Figure 14 makes the row-versus-node distinction visible again. The same 18 films from Figure 10 are involved, but the result has 48 rows because each cast member of each film contributes one. The Matrix Reloaded accounts for four of them, one each for Hugo Weaving, Laurence Fishburne, Carrie-Anne Moss, and Keanu Reeves, and The Matrix Revolutions repeats the same four names. This is the counting subtlety we return to in Section 6.2.
Filtering on the existence of a relationship
A pattern can also be placed inside the WHERE clause itself, in which case Cypher tests only whether the traversal succeeds and does not add its rows to the result:
MATCH (p:Person)
WHERE NOT (p)-[:ACTED_IN]->()
RETURN p.name
The difference from the previous query is important. There, ACTED_IN was part of the MATCH pattern, so the relationship shaped the result and produced one row per traversal. Here it is a predicate, so the result still holds one row per person and the pattern acts purely as a yes-or-no test. Negated with NOT, it returns the people who never acted, the directors, producers, writers, and reviewers, and we return to exactly this query in Section 6.2. Dropping the NOT gives the opposite set.
Filtering on a collected list
Every filter so far has tested one row at a time. Sometimes the condition we want applies to a group of rows instead, for instance “films with more than one actor”. That requires collapsing the rows into a list first, and Cypher does it with WITH:
//Query: Using List Functions
MATCH (m:Movie)<-[:ACTED_IN]-(a:Person)
WITH m, collect(a) AS actors
WHERE size(actors) > 1
RETURN m.title, size(actors) AS NumberOfActors
Four things are worth unpacking here.
The pattern is written right-to-left, (m:Movie)<-[:ACTED_IN]-(a:Person), with the arrowhead on the left. This is the same relationship as (a:Person)-[:ACTED_IN]->(m:Movie) used earlier, just read from the film’s point of view; Cypher allows either direction to be written, and the choice is purely one of readability.
WITH acts as a pipeline stage: it takes the rows produced so far, reshapes them, and passes the result to the next clause. It behaves exactly like RETURN except that the query continues afterwards instead of ending. Here it keeps m and replaces the many person rows with a single aggregated value.
collect(a) is the aggregating function doing that work. Where count reduces a group of rows to a number, collect gathers the values into a list. Because m is the only non-aggregated expression in the WITH, it becomes the implicit grouping key, so the many rows for one film collapse into one row holding that film and a list of its cast.
size(actors) then measures the list, and the WHERE that follows filters on that measurement. The position of the WHERE is the essential point: it sits after the WITH, so it is applied to the aggregated rows rather than the raw ones. Placing it before the WITH would filter individual person-film pairs, which is a different question entirely and could not reference actors at all.
Figure 15 returns 37 rows out of the 38 films in the graph. The one missing film is the single-cast entry that size(actors) > 1 filters out. The counts also line up with the import script: The Matrix shows 5 rather than 4 because Emil Eifrem was added to its cast, and both Matrix sequels show 4.
size(collect(a)) and count(a) give the same number here, and count is cheaper because it never materialises the list. collect earns its keep when the list itself is needed, for example returning [x IN actors | x.name] AS castNames alongside the count, or testing its contents with any(), all(), or none().
The null behaviour we met earlier matters throughout this section. A predicate that evaluates to null, which is what happens when the property is missing, is not true, so those nodes are dropped just as if the comparison had failed. Filtering on a misspelled property name therefore returns no rows at all rather than an error. To keep nodes that lack the property, test for it explicitly with WHERE m.released IS NULL OR m.released > 2000.
Following relationships
Relationships have appeared only in passing so far, as a way of narrowing a filter. They are what makes a graph database useful in their own right, and they are written into the pattern with a hyphen, square brackets, and an arrow, so (p)-[r:ACTED_IN]->(m) reads as “p has an ACTED_IN relationship pointing to m”:
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
RETURN p, r, m
ACTED_IN edges linking actors (light circles) to the films they appeared in (dark circles). Selecting a node opens the details panel, here showing the released, tagline, and title properties of Sleepless in Seattle.
The variables work the same way on both sides: p binds to the person, m to the movie, and r to the relationship joining them. Returning all three gives the browser real nodes and edges to draw, so unlike Figure 5 the result appears as a connected graph rather than isolated circles. The arrow direction matters, and it must match the direction shown in Figure 1: ACTED_IN runs from Person to Movie, so writing the pattern the other way round would return nothing.
As before, we can return properties instead of whole entities. Relationships carry properties too, and ACTED_IN stores the roles an actor played:
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
RETURN p.name, m.title, r.roles
ACTED_IN relationship with a total of 172 relationships, naming the actor, the film, and the list of characters they played. The r.roles column comes from the relationship rather than either node, and is displayed as a list because that is how it was stored.
Each row now names an actor, a film, and the list of characters they played in it. The same person appears once per film, so Hugo Weaving is listed separately as “Agent Smith” in The Matrix, The Matrix Reloaded, and The Matrix Revolutions.
Returning properties always produces a table (never a graph). The Neo4j Browser can only draw a visualisation when the result contains nodes or relationships as whole entities, as in RETURN p, r, m. As soon as the RETURN lists properties such as p.name or r.roles, the values are plain strings, numbers, and lists with no identity or connections attached, so the Graph tab disappears and only the Table and Raw views remain. The same rule is what makes aggregations undrawable, as Section 6.4 explains.
Adding a WHERE clause narrows the traversal to one starting point, which is the most common shape of all:
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks'
RETURN p, m, r
Person node at the centre and one ACTED_IN arrow reaching out to each Movie. The details panel on the right shows the properties of the selected node, born 1956 and name “Tom Hanks”, together with its internal <id>.
Here the filter applies to the node at one end of the pattern, and because Person.name is backed by the uniqueness constraint we created at the start, Neo4j uses the index to find Tom Hanks directly and then walks outwards along his ACTED_IN relationships. This is the essential difference from a relational database: rather than joining a large table, the query starts from one node and follows the connections it already holds. Figure 18 shows the result: a single starting node surrounded by the twelve films reached in one hop.
Aggregation
Rather than listing rows, we often want to summarise them. Cypher does this with aggregating functions: count, avg, sum, min, and max all collapse the rows handed to them by MATCH into a single value, and any expression in the RETURN that is not an aggregation silently becomes the grouping key. This section works through each of them on the Movie graph, and along the way meets two traps that catch beginners: aggregating a property that does not exist, and traversing a relationship type the graph never had.
Cypher keywords and function names are case-insensitive, so COUNT(m) and count(m) are the same function. Some queries below are written in upper case to match the style commonly used in tutorials and others in lower case; they behave identically. Only the labels, relationship types, property names, and string values are case-sensitive.
Counting nodes
The simplest aggregating function is count, which reduces a set of rows to their number:
//Query: Counting Nodes
MATCH (m:Movie)
RETURN count(m) AS NumberOfMovies
MATCH again produces one row per Movie node, but count(m) collapses those rows into a single value: the number of rows it was given. Aggregation in Cypher is implicit, so there is no GROUP BY to write. Anything that is not an aggregating expression in the RETURN becomes the grouping key, and here there is nothing else, so the whole result is reduced to one row.
The AS NumberOfMovies part is an alias. Without it the column would be headed count(m), which is how the expression was written. Aliasing gives the column a readable name, and is also what lets a later clause refer back to the value.
Figure 19 confirms that the import created 38 Movie nodes, which matches the size of the built-in Movie graph. Since the script uses MERGE throughout, running it a second time and re-running this count will still return 38, which is a quick way to verify that the import is idempotent.
The same shape counts any label, so swapping Movie for Person counts the people instead:
MATCH (p:Person)
RETURN count(p)
Person nodes returns 133, fewer than the 172 ACTED_IN relationships in Figure 17.
count(m) counts rows in which m is not null, whereas count(*) counts every row regardless. The distinction matters once optional patterns are involved. To count only distinct values, for instance the number of different release years, use count(DISTINCT m.released).
Counting nodes versus counting relationships
The two counts above invite an obvious question, given the traversals of Section 5: how many people are there really in the graph? At first glance the numbers look contradictory: 133 people in Figure 20, but 172 ACTED_IN relationships in Figure 17. They are counting different things. count(p) counts nodes, one per person, whereas the earlier traversal counted rows, one per ACTED_IN relationship. There are two plausible explanations for the difference:
- Actors with several films add rows. The pattern
(p:Person)-[r:ACTED_IN]->(m:Movie)produces one row per relationship, so Tom Hanks contributes 12 rows, Keanu Reeves 7, and Hugo Weaving 5. Each is a single node but many relationships. - Many people never acted. Directors, producers, and writers such as Nora Ephron, Joel Silver, and Frank Darabont are
Personnodes carrying onlyDIRECTED,PRODUCED, orWROTErelationships, and the four reviewers have onlyREVIEWEDandFOLLOWS. None of them appear among the 172 rows at all.
To count the people who actually acted, rather than the relationships they accumulated, we deduplicate with DISTINCT:
MATCH (p:Person)-[:ACTED_IN]->(:Movie)
RETURN count(DISTINCT p) AS NumberOfActors
This is the count(DISTINCT ...) form mentioned earlier, and it returns a value smaller than both 133 and 172. Note also that r has been dropped from the pattern and the second node left anonymous as (:Movie): a variable is only needed when something later in the query refers to it.
Breaking the totals down by relationship type
Rather than running one query per relationship type, we can ask for all six at once and let Cypher group the results:
MATCH (p:Person)-[r:ACTED_IN|DIRECTED|PRODUCED|WROTE|REVIEWED|FOLLOWS]->()
RETURN type(r) AS relationshipType,
count(r) AS total,
count(DISTINCT p) AS distinctPeople
ORDER BY total DESC
Three pieces of syntax are doing the work here. The vertical bars inside the square brackets are a type disjunction: [r:ACTED_IN|DIRECTED|...] matches a relationship whose type is any one of those listed, so a single MATCH covers every kind of contribution a person can make. The node at the far end is left as an empty (), which matches anything; that matters because five of the types point at a Movie while FOLLOWS points at another Person, and an anonymous node accommodates both. Finally, type(r) returns the relationship’s type as a string, and because it is the only expression in the RETURN that is not an aggregation, it becomes the implicit grouping key. Cypher therefore produces one row per relationship type, exactly as GROUP BY type(r) would in SQL.
The two count columns then answer different questions within each group. count(r) counts the relationships themselves, while count(DISTINCT p) counts how many different people contribute them. ORDER BY total DESC sorts the groups from most to least common, and it can refer to total because the alias is established in the RETURN.
total column counts relationships and distinctPeople counts the people producing them, so the gap between the two columns shows how much repeat contribution each type involves.
Figure 21 summarises the whole graph in six rows. ACTED_IN dominates with 172 relationships from 102 distinct people, which is the number the previous count(DISTINCT p) query returned, and it is comfortably smaller than the 133 Person nodes in Figure 20. Next come 44 DIRECTED relationships from 28 directors and 15 PRODUCED from 8 producers, followed by 10 WROTE from 8 writers. The two smallest rows belong to the reviewers: 9 REVIEWED and 3 FOLLOWS, both from just 3 people, since Paul Blythe follows Angela Scope without having reviewed anything himself.
The 102 and the 133 count different populations, which is worth spelling out. The 133 is every node carrying the Person label, anyone who appears in the graph in any capacity at all. The 102 is the number of people reached by the pattern (p:Person)-[:ACTED_IN]->(), so count(DISTINCT p) deduplicates the 172 rows and counts Tom Hanks once rather than twelve times, but a person is only counted at all if they have at least one ACTED_IN relationship to begin with. The 31-person gap is made up of people whose only relationships are of some other type: directors such as Nora Ephron, Ron Howard, and Frank Darabont, producers such as Joel Silver and Stefan Arndt, writers such as David Mitchell and Jim Cash, and the four reviewers. None of them ever acted, so the traversal never binds them to p and they contribute no rows to deduplicate. The two effects therefore push in opposite directions: repeat actors make 172 relationships larger than 102 people, while non-acting people make 102 smaller than the 133 nodes in total, giving 102 < 133 < 172. The excluded group can be confirmed directly, and returns 31:
MATCH (p:Person)
WHERE NOT (p)-[:ACTED_IN]->()
RETURN count(p) AS NeverActed
The ratio between the two columns is the interesting part. PRODUCED at 15 relationships from 8 people and REVIEWED at 9 from 3 are the most lopsided, reflecting prolific producers such as Joel Silver and the handful of reviewers who rated several films each. FOLLOWS is the only type where the numbers coincide, because each of the three people follows exactly one other person.
Why aggregations never produce a graph view
Every count so far has come back as a table rather than a picture, and the browser makes the reason visible:
Movie nodes are matched but only one row is returned, headed NumberOfMovies after the alias rather than COUNT(m). Only the Table and Raw tabs are offered: the aggregation has replaced the nodes with a plain number, so there is nothing left for the browser to draw as a graph.
Figure 22 is the count query of Section 6.1 as the browser renders it, and the missing Graph tab is the point worth dwelling on.
The Neo4j Browser draws a visualisation only when the result set contains nodes or relationships as whole entities, because those are the only values that carry an identity and a set of connections to draw. This is the same rule we met when returning properties in Figure 17, but aggregation makes it unavoidable rather than merely likely.
An aggregating function is by definition a reduction: it takes many rows and returns a single value of a scalar type, a number for COUNT, AVG, SUM, MIN, and MAX. The node identities that went in do not come out. COUNT(m) yields the integer 38, which knows nothing about which 38 films were counted or how they connect to anyone, so the browser has no vertices to place and no edges to route. The Graph tab is therefore absent, not empty.
collect is the instructive exception. It aggregates without reducing to a scalar, gathering the matched nodes into a list and keeping their identities intact, so MATCH (m:Movie) RETURN collect(m) does render as a graph. If you want both the summary and the picture, keep the entities alongside the number:
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks'
RETURN p, collect(m) AS films, count(m) AS total
The count column is still a scalar, but p and the collected films give the browser something to draw, so the Graph tab reappears.
p and the nodes inside films are whole entities rather than scalars. Tom Hanks appears as the single brown Person circle and the twelve films he acted in as green Movie circles, with Cast Away selected so its released, tagline, and title properties appear in the details panel. No arrows are drawn between them: the pattern bound the ACTED_IN relationships to r, but r was never returned, so the browser has the nodes without the edges that connect them.
p is the only non-aggregated expression and therefore the grouping key. The films column holds one list of twelve Movie chips rather than twelve separate rows, and total holds the scalar 12 that count(m) reduced those rows to.
Figure 23 and Figure 24 are the same result set drawn two ways, which is what makes the pair useful. Both tabs are offered because the row contains entities; had we returned p.name instead of p, and dropped collect, only the Table tab would have survived, exactly as in Figure 22.
The contrast between them also shows what aggregation did to the row count. The MATCH produced twelve rows, one per ACTED_IN relationship, and both collect(m) and count(m) consumed all twelve. collect folded them into one list value and count into one integer, so a single row emerges carrying both. The twelve circles in Figure 23 are the contents of that one list being unpacked for drawing, not twelve separate result rows.
Adding r to the RETURN restores the missing arrows:
MATCH (p:Person)-[r:ACTED_IN]->(m:Movie)
WHERE p.name = 'Tom Hanks'
RETURN p, collect(r) AS roles, collect(m) AS films, count(m) AS total
Movie and 1 Person, together with 12 ACTED_IN relationships. Nothing about the data has changed since Figure 23; the only difference is that r is now returned rather than merely bound by the pattern.
roles now sits between p and films holding a list of twelve ACTED_IN chips, one per relationship, while total remains the scalar 12. The three columns illustrate the difference between the two kinds of aggregation side by side: collect preserves twelve values, count reduces them to one.
Figure 25 redraws the star shape of Figure 18 while still reporting the count, and Figure 26 shows the same result as one row. Comparing the two tables is the clearest summary of the whole callout: in Figure 26 the roles and films lists carry twelve entities each, which is what the browser draws, while total carries a single number, which it cannot.
Calculating the average
Swapping the function changes what is computed from the group, but not how the group is formed:
//Query: Calculating the Average
MATCH (m:Movie)
RETURN AVG(m.released) AS AverageReleaseYear
AVG takes the arithmetic mean of a numeric expression across all the rows in the group. Because released was stored as an integer, the mean is a genuine number rather than a lexicographic accident, and the result is returned as a floating-point value somewhere in the late 1990s. Averaging a year is statistically a little odd, but it is a compact way of asking “when is this collection of films centred?”.
AVG has reduced the matched nodes to one scalar. The value is shown in full floating-point precision rather than rounded, and ROUND(AVG(m.released), 1) would trim it.
AVG ignores null values rather than treating them as zero, and it divides by the number of non-null rows. That is usually what you want, but it also means the denominator is invisible: a mean computed over three of thirty-eight films looks no different from one computed over all thirty-eight. Returning count(m.released) alongside the average makes the sample size explicit.
Summing values
SUM adds the values in the group instead of averaging them:
//Query: Summing Values
MATCH (m:Movie)
RETURN SUM(m.duration) AS TotalDuration
This query is syntactically perfect and semantically useless on our graph: it returns 0. The reason is the one we met in Figure 7, where m.releaseYear came back entirely null. The Movie graph stores only title, released, and tagline on its film nodes, so m.duration evaluates to null on all 38 rows. SUM skips nulls, and summing nothing at all gives the identity element of addition, which is zero.
TotalDuration holding 0. Nothing has failed: the query matched all 38 films and returned a well-formed answer, but duration is not a property the Movie graph carries, so every value SUM was given was null.
The contrast with the other functions is worth remembering. Over an entirely null column, SUM returns 0, COUNT returns 0, but AVG, MIN, and MAX all return null, because there is no meaningful mean or extreme of an empty set whereas an empty sum is well defined. A 0 from SUM is therefore ambiguous: it can mean “the values added up to zero” or “there were no values”. CALL db.propertyKeys again settles which of the two you are looking at.
To make the query meaningful, the property has to exist first. Adding it to a single film is enough to see the behaviour change:
MATCH (m:Movie {title: 'The Matrix'})
SET m.duration = 136
RETURN m.title, m.duration
Re-running the SUM now returns 136 rather than 0, since one row contributes a value and the other 37 are still skipped.
Finding minimum and maximum values
MIN and MAX return the smallest and largest value in the group, and because several aggregations may appear in one RETURN, both can be computed in a single pass:
//Query: Finding Minimum and Maximum Values
MATCH (m:Movie)
RETURN MIN(m.released) AS EarliestRelease,
MAX(m.released) AS LatestRelease
RETURN give two columns in a single row, EarliestRelease 1975 and LatestRelease 2012. Both share the same 38-row group produced by MATCH (m:Movie), so the pattern is scanned once rather than twice.
On the Movie graph this returns 1975 and 2012, the years of One Flew Over the Cuckoo’s Nest and Cloud Atlas. Note what the query does not give us: the titles. MIN returns the extreme value, discarding the row it came from, so there is no m left to ask for a title. Recovering the film means sorting and taking the first rows instead:
MATCH (m:Movie)
RETURN m.title, m.released
ORDER BY m.released ASC
LIMIT 5
released ascending. The first row, One Flew Over the Cuckoo’s Nest at 1975, carries the same year MIN returned in Figure 29, but here the title comes with it.
Figure 30 is the same information as the EarliestRelease column of Figure 29, expanded into rows that keep their identity. ORDER BY ... ASC sorts the 38 films from oldest to newest and LIMIT 5 keeps the top of that list, so the first row is the minimum and the rest give it context: Top Gun and Stand By Me tie on 1986, followed by Joe Versus the Volcano in 1990 and A Few Good Men in 1992. Reversing the sort with DESC would put Cloud Atlas and the 2012 end of the range at the top instead, and LIMIT 1 would return the single extreme row.
The tie at 1986 is also a reminder of what MIN and MAX cannot tell you. Both return one value, so a shared extreme is invisible; sorting exposes it.
Both MIN and MAX work on strings and dates as well as numbers, comparing them lexicographically and chronologically respectively.
Grouping and aggregating
Adding a non-aggregated expression to the RETURN turns a single total into one row per group. The query below is the first in this section to do that, and it also contains two mistakes that are instructive to walk through:
//Query: Grouping and Aggregating
MATCH (m:Movie)
RETURN m.released COUNT(m) AS MoviesPerYear
ORDER BY m.releaseYear
The first problem is a syntax error. There is no comma between m.released and COUNT(m), so Cypher cannot tell where one return item ends and the next begins, and the query is rejected before it ever touches the data. The second is the familiar misspelling: ORDER BY m.releaseYear sorts on a property that does not exist, so even once the comma is added every sort key would be null and the rows would come back in an arbitrary order. Correcting both gives a query that works:
MATCH (m:Movie)
RETURN m.released, COUNT(m) AS MoviesPerYear
ORDER BY m.released
m.released is not an aggregation, so it becomes the implicit grouping key and Cypher returns one row per distinct release year with the number of films in it.
m.released column holds the grouping key and MoviesPerYear the size of each group, so the 38 Movie nodes collapse into roughly two dozen rows.
Figure 31 begins at 1975 with a single film, the One Flew Over the Cuckoo’s Nest row that MIN and the sorted query both picked out earlier, and runs forward from there: 2 films in 1986, 1 in 1990, 4 in 1992, then 1, 2, 3, 2, 3, and 4 through to 1999. Most years contribute one or two, with 1992 and 1999 the busiest of those visible.
The grouping key is what makes this different from every aggregation before it. COUNT(m) still reduces rows to a number, but it now does so once per distinct value of m.released instead of once for the whole result, which is why a table of many rows comes back rather than the single row of Figure 22.
Sorting by the count rather than the year, with ORDER BY MoviesPerYear DESC, answers a different question: which year the collection is most concentrated in. ORDER BY can refer to MoviesPerYear because the alias is established in the RETURN, but it cannot refer to COUNT(m) written out again in some queries, which is one practical reason to alias aggregations.
Grouping and averaging
Grouping works just as well on a value reached through a relationship as on a property of the matched node:
//Query: Grouping and Averaging
MATCH (m:Movie)-[:HAS_GENRE]->(g:Genre)
RETURN g.name AS Genre, AVG(m.rating) AS AverageRating
ORDER BY AverageRating DESC
Read structurally, this is the same shape as the previous query: g.name is the non-aggregated expression and therefore the grouping key, AVG(m.rating) is computed within each group, and the results are ordered from the highest-rated genre downwards.
On our graph, however, it returns no rows at all, and the reason is different from the empty SUM above. There is no Genre label and no HAS_GENRE relationship type in the Movie graph, as Figure 1 showed: the only labels are Movie and Person, and the only relationship types are ACTED_IN, DIRECTED, PRODUCED, WROTE, REVIEWED, and FOLLOWS. The MATCH pattern therefore matches nothing, and an aggregation over zero rows with a grouping key produces zero rows rather than a row of nulls.
01N51 for the missing HAS_GENRE relationship type and 01N50 for the missing Genre label, each with a caret marking the offending token in the query.
Figure 32 is worth reading carefully, because the query did not fail. Cypher is schema-optional, so a label or relationship type it has never seen is a legitimate thing to ask for; it simply matches nothing. The database says so as a warning rather than an error, which is exactly the same permissiveness that let m.releaseYear return null in Figure 7. Warnings like these are easy to miss because the result panel above them looks merely empty, and they are the fastest way to diagnose a query that returns nothing for no apparent reason.
This is the difference between a failed pattern and a missing property. SUM(m.duration) still had 38 rows to aggregate, they just carried null values, so one row came back. AVG(m.rating) had no rows to begin with, so nothing came back. When a grouped query returns an empty table, suspect the pattern; when it returns a single row of null or 0, suspect the property name.
The equivalent question that the Movie graph can answer uses the ratings the reviewers left on the REVIEWED relationship:
MATCH (p:Person)-[r:REVIEWED]->(m:Movie)
RETURN m.title AS Movie, AVG(r.rating) AS AverageRating
ORDER BY AverageRating DESC
Here rating lives on the relationship rather than on either node, which is one of the things a property graph does that a relational schema handles less gracefully. The Replacements is the interesting row: it collapses three separate reviews, scored 100, 65, and 62, into a single mean of roughly 75.7.
Finding the actor with the most movies
The final query combines a traversal, a grouping, a sort, and a limit, which together form one of the most common analytical shapes in Cypher:
//Query: Finding the Actor with Most Movies
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
RETURN a.name AS Actor, COUNT(m) AS NumberOfMovies
ORDER BY NumberOfMovies DESC
LIMIT 1
The pattern produces the 172 ACTED_IN rows we counted in Section 6.2. a.name is the only non-aggregated expression, so those rows are grouped by actor, and COUNT(m) gives the number of films each one appeared in. ORDER BY ... DESC puts the most prolific first and LIMIT 1 keeps only the top row, which is Tom Hanks with the 12 films drawn in Figure 18.
Actor column is quoted as a string and NumberOfMovies shown as a bare number, the same visual distinction seen throughout.
Figure 33 is the endpoint of the chain this section has been building. The 12 in NumberOfMovies is the same 12 that count(m) produced in Figure 26, and the same twelve films drawn around Tom Hanks in Figure 18; what has changed is that the query found him rather than being told his name in advance.
LIMIT is applied last, after the sort has finished, so the single row returned really is the global maximum rather than the first row that happened to be produced. Raising it to LIMIT 10 gives the leaderboard instead of the winner, with Keanu Reeves on 7 and Hugo Weaving on 5 beneath Tom Hanks.
LIMIT 1 hides ties. If two actors were level on 12 films, this query would return one of them arbitrarily and give no hint that the other existed. Ordering by count and returning several rows, or grouping the counts with a second aggregation, is safer whenever ties are plausible.
MERGE, WITH, and RETURN
Three clauses have appeared repeatedly in the queries above without being examined on their own. MERGE is the write clause that matches before it creates, and it is what makes the import script in Section 2 safe to run twice. WITH is the pipe that joins one part of a query to the next, which is what allows a write to be followed by a read or an aggregate to be filtered. RETURN shapes the final result, and its modifiers DISTINCT, ORDER BY, and LIMIT decide which rows survive and in what order. This section works through each of them in turn.
Matching before creating with MERGE
MERGE is best read as “find this pattern, and if it is not there, create it”:
//Query: Using MERGE
MERGE (m:Movie {title: 'The Matrix'})
ON CREATE SET m.released = 1999, m.duration = 136
RETURN m
The pattern in braces is both the search key and the blueprint for the new node. Because The Matrix already exists in the graph, this query matches the node that the import script wrote and creates nothing; the ON CREATE SET clause fires only on the creating branch, so no duration property is added and the status bar reports zero writes. Deleting the film first and re-running the query would take the other branch, creating the node and setting both properties in one step.
Movie node returned by the MERGE, with the details panel listing released, tagline, and title but no duration. The absent property is the evidence that the matching branch was taken and ON CREATE SET never ran.
Figure 34 makes the two branches easy to tell apart without reading the status bar. The tagline property is the giveaway: nothing in this query mentions it, so it can only have come from the import script, which means the node was matched rather than created. Had MERGE created a fresh node, the details panel would show title, released, and duration and no tagline at all.
The counterpart is ON MATCH SET, which runs only when the pattern was found. The two can be combined in a single statement:
MERGE (m:Movie {title: 'The Matrix'})
ON CREATE SET m.released = 1999, m.duration = 136
ON MATCH SET m.duration = 136
RETURN m
This form gives an upsert: the node is created with its properties if it is missing, and topped up with the missing duration if it is already there.
ON MATCH SET. duration is now present alongside tagline, and the status bar reads “Set 1 property”: the matching branch ran and wrote the one property named on it.
Comparing Figure 35 with Figure 34 shows what the second clause changed. The <id> is identical in both, so this is the same node being updated in place rather than a duplicate, and tagline still confirms that the node came from the import script. What is new is duration, written by ON MATCH SET, and the status bar’s “Set 1 property” rather than the earlier report of no changes. ON CREATE SET still did nothing, which is why only one property was set even though it also names released.
ON MATCH SET overwrites unconditionally, exactly like a plain SET. Running this query against a film that already had a different duration would replace it silently. Where the intention is to fill gaps rather than overwrite, ON MATCH SET m.duration = coalesce(m.duration, 136) keeps any existing value and writes only when the property is missing.
MERGE matches on every property written inside the pattern. Before the upsert above ran, the node had no duration, so MERGE (m:Movie {title: 'The Matrix', duration: 136}) would have failed to find it and Neo4j would have created a second The Matrix instead. Keep the pattern restricted to the identifying properties and push everything else into ON CREATE SET.
The uniqueness constraints added in Section 2 matter here for two reasons. They make the match fast, because a constraint creates a supporting index, and they act as a backstop: if a MERGE pattern is written too loosely and attempts a duplicate, the constraint rejects the write with an error rather than silently splitting the film into two nodes.
Merging nodes and relationships together
MERGE applies to relationships in exactly the same way, and several MERGE clauses can be chained so that each one builds on the variables bound before it:
//Query: Using MERGE with Relationships
MERGE (a:Person {name: 'Keanu Reeves'})
MERGE (m:Movie {title: 'The Matrix'})
ON CREATE SET m.released = 1999, m.duration = 136
MERGE (a)-[:ACTED_IN]->(m)
RETURN a, m
Each clause is evaluated in order. The first two find the person and the film, the third looks for an ACTED_IN edge between the two nodes now bound to a and m, and because that edge already exists, nothing is written. The ON CREATE SET belongs to the MERGE immediately above it, not to the statement as a whole, which is why the film’s clause is the only one carrying it.
Note that RETURN a, m asks for the two nodes and not the relationship, so the browser draws two unconnected circles for the same reason discussed in Figure 3. Naming the edge, as in MERGE (a)-[r:ACTED_IN]->(m) RETURN a, r, m, gives the browser something to draw between them.
ACTED_IN relationship exists. The details panel shows the film carrying all four properties, and the status bar reports a read of one record rather than any writes.
Figure 36 confirms that all three MERGE clauses matched. The film’s <id> is the same one seen in Figure 34 and Figure 35, duration is still the 136 written by the previous query rather than something this one set, and the status bar reports no changes at all. The missing arrow is the presentation artefact described above, not a missing edge: the ACTED_IN relationship was found by the third MERGE, but RETURN a, m never asked for it.
When a relationship MERGE refers to nodes that are not already bound, the whole pattern becomes the search key. MERGE (a:Person {name: 'Keanu Reeves'})-[:ACTED_IN]->(m:Movie {title: 'The Matrix'}) is safe only because both endpoints exist; had either been missing, Cypher would have created the two nodes and the edge rather than reusing what was there. Merging the nodes first and the relationship afterwards, as above, avoids the ambiguity.
Merging a relationship between nodes found by MATCH
When both endpoints are known to exist, MATCH is the clearer way to bind them, leaving MERGE responsible for the edge alone:
//Query: Using MERGE with Patterns
MATCH (a:Person {name: 'Keanu Reeves'})
MATCH (m:Movie {title: 'The Matrix'})
MERGE (a)-[:ACTED_IN]->(m)
RETURN a, m
The difference from the previous query is one of intent rather than result. MATCH fails quietly when a node is absent: the pattern produces no rows, so the MERGE never runs and nothing is written. MERGE on the same pattern would have created the missing node instead. Using MATCH for data that must already exist and MERGE only for the part that may need creating states that expectation directly, and is the safer default when attaching new relationships to an established graph.
Running MERGE without a RETURN
A write does not have to hand anything back:
//Query: Using MERGE to Avoid Duplicates
MERGE (m:Movie {title: 'The Matrix'})
ON CREATE SET m.released = 1999, m.duration = 136
Dropping the RETURN leaves a statement whose only purpose is the write, and the status bar rather than the result pane reports what happened. This is the form used throughout the import script in Section 2, and it is what makes the script idempotent: the first run creates each node, and every subsequent run matches it and reports no changes.
Figure 37 is what idempotence looks like in the browser. “No records” is the consequence of dropping the RETURN, and “no changes” is the consequence of the node already existing: the pattern matched, so ON CREATE SET was skipped and nothing was written. A first run against an empty database would show “Added 1 label, created 1 node, set 3 properties” instead, and every run after that would return to the line shown here.
Chaining query parts with WITH
WITH ends one part of a query and starts the next, passing on only the variables it names. It was introduced in Section 2 as the bridge that lets a MATCH follow a CREATE; the same clause chains two reads together:
//Query: Using WITH to Chain Queries
MATCH (a:Person{name: 'Keanu Reeves'})-[:ACTED_IN]->(m)
WITH a, m
MATCH (m)<-[r:DIRECTED]-(d:Person)
RETURN a, m, r, d
The first MATCH produces one row per film Keanu Reeves acted in. WITH a, m forwards those two variables, and the second MATCH extends each row by finding the people who directed that film. Anything not listed in the WITH is dropped at that boundary, which is both a filter on the working set and a guard against accidentally reusing a variable later in the query.
Two details of the pattern are worth noting. The first MATCH writes (m) with no label, which works because ACTED_IN only ever points at a Movie in this graph; adding :Movie would make the intent explicit and let the planner rule out other labels. The second MATCH binds the relationship to r and returns it alongside the nodes, which is what allows the browser to draw the edges rather than a set of disconnected circles.
Person colour and the Matrix films in the Movie colour, joined by DIRECTED edges. The details panel shows the selected node, The Matrix Reloaded, carrying the properties the import script wrote.
Figure 38 shows the browser collapsing rows back into a graph. Each returned row is one actor, one film, one DIRECTED edge, and one director, but a film directed by both Wachowskis contributes two rows while remaining a single node on screen, which is why three films carry six arrows between them. The visible portion of the result is the Matrix trilogy; panning the canvas reveals the rest of Keanu Reeves’ films and the Person node the query started from.
The Movie graph stores the relationship as (:Person)-[:DIRECTED]->(:Movie). If a query MATCH is written as (m)-[:DIRECTED_BY]->(d:Person), the query would return no rows at all. Neither a misspelled relationship type nor a reversed arrow raises an error in Cypher. The pattern simply fails to match and the result is empty. The CALL db.schema.visualization query shown when the schema was inspected is the quickest way to confirm the type names and directions the data actually uses.
Because the second MATCH here is a plain one, a film with no recorded director would drop out of the result entirely, taking the actor row with it. OPTIONAL MATCH keeps such rows and fills the missing columns with null.
Aggregating in the middle of a query with WITH
WITH also accepts aggregate functions, which is what allows a query to aggregate first and then keep working with the result:
//Query: Using WITH for Aggregation
MATCH (a:Person)-[:ACTED_IN]->(m)
WITH a, count(m) as movies
RETURN a.name, movies
ORDER BY movies DESC
The grouping rule is the one described in Section 6: a is the only non-aggregated expression in the WITH, so it becomes the grouping key and count(m) is evaluated once per actor. Written this way the counts exist as an ordinary variable for the remainder of the query, so a WHERE movies > 5 could be added straight after the WITH to keep only the prolific actors. That is the Cypher equivalent of SQL’s HAVING, and it is the main reason to aggregate in a WITH rather than in the RETURN.
Figure 39 shows the aggregation doing its work. The 172 ACTED_IN rows the pattern produced have been reduced to the 102 rows counted in Section 6.2, one for each actor, and the leaderboard at the top is the same one Figure 33 arrived at by a different route: Tom Hanks on 12, Keanu Reeves on 7, then Hugo Weaving, Jack Nicholson, and Meg Ryan level on 5. The column header reads a.name rather than Actor because this version omits the AS alias; adding RETURN a.name AS Actor, movies would rename the column without changing a single value.
Removing duplicate rows with DISTINCT
RETURN produces one row per match, not one row per node, so a pattern that matches the same person several times repeats that person in the output:
//Query: Using RETURN with DISTINCT
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
RETURN DISTINCT a.name AS Actor
Without DISTINCT this query returns 172 rows, one per ACTED_IN relationship, with Tom Hanks appearing 12 times. DISTINCT deduplicates the rows after the projection, leaving the 102 distinct actors counted in Section 6.2. It applies to the whole row rather than to a single column, so adding m.title back into the RETURN would restore all 172 rows, every one of them already unique.
Actor column with one row per person, beginning with the Matrix cast. The footer reports 102 records, the same total the aggregating query produced, but reached by deduplication rather than by counting.
Figure 40 is the same 102 actors seen in Figure 39, which is the point worth noticing: one query counted the films per actor and the other threw the films away, yet both end with one row per person. The rows are also unordered here, arriving in whatever sequence the pattern matched rather than alphabetically, since no ORDER BY was written. The Actor header comes from the AS alias, and the quotation marks around each value are the browser’s way of marking the column as strings.
RETURN DISTINCT a.name and count(DISTINCT a) deduplicate different things. The first removes duplicate rows from the output; the second removes duplicates inside an aggregate before counting. They agree here only because names happen to be unique in this dataset, and two different actors sharing a name would collapse into one row under RETURN DISTINCT a.name while still counting as two under count(DISTINCT a).
Sorting and truncating the result
ORDER BY sorts the rows and LIMIT cuts the result short. Neither changes which rows the pattern matched, only how they are presented:
//Query: Using RETURN with ORDER BY
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
RETURN a.name AS Actor, m.title AS Movie
ORDER BY Actor, Movie
Listing two expressions sorts on the first and uses the second only to break ties, so the result is grouped by actor with each actor’s films in alphabetical order. Each key can carry its own direction, as in ORDER BY Actor ASC, Movie DESC, and ASC is the default when nothing is written. Sorting on an alias such as Actor works because the alias is defined in the RETURN that ORDER BY belongs to.
LIMIT truncates whatever the preceding clauses produced:
//Query: Using RETURN with LIMIT
MATCH (a:Person)-[:ACTED_IN]->(m:Movie)
RETURN a.name AS Actor, m.title AS Movie
LIMIT 5
Five rows come back, but which five is undefined: with no ORDER BY the order is whatever the query planner produced, and it may change between runs or after a write. A bare LIMIT is therefore a sampling tool for inspecting the shape of a result, not a way of selecting the top of it. Combining the two clauses, as the leaderboard query in Section 6 does, is what makes the choice deterministic: ORDER BY runs first over the complete result and LIMIT then keeps the head of the sorted list, which is why LIMIT 1 after a sort really does return the global maximum.
SKIP pairs with LIMIT to page through a large result: ORDER BY Actor SKIP 20 LIMIT 10 returns the third page of ten rows. Paging is only meaningful alongside an ORDER BY, since without a stable sort the pages can overlap or miss rows entirely.
How to find the shortest path between nodes
Every pattern written so far has fixed the number of hops in advance: (p)-[:ACTED_IN]->(m) is one hop, and the chained MATCH in Section 7 was two. Many interesting questions do not work that way. “How are these two people connected?” has no known length, and the answer is a path, a sequence of nodes and relationships joining one end to the other. Cypher can search for the shortest such path directly, which is the graph-database counterpart of the six-degrees-of-separation game and the reason the Movie graph is so often used to demonstrate it.
Two functions do this, and they differ only in how many answers they give back.
shortestPath returns one three-hop route from A to I, while allShortestPaths returns all three routes of that length. Nodes D and E stay unhighlighted in both because no shortest route passes through them.
Figure 41 sets out the distinction before any Cypher is written. Both sides search the same graph and both find routes of the same minimal length, three hops; the left picks one of them and the right returns every one. The longer route A–C–E–H–I exists too, at four hops, but neither function reports it, because both stop as soon as the shortest length has been found. Deciding which of the two to use is therefore a question of whether the alternatives matter: the left is enough to answer “are these connected, and how closely?”, while the right is needed to answer “in how many ways?”.
Variable-length patterns
The building block is the * in a relationship pattern, which turns a fixed hop into a range. -[*]- means “one or more relationships of any type, in either direction”, -[:ACTED_IN*2]- means exactly two ACTED_IN hops, and -[*1..4]- bounds the search between one and four. Leaving the arrowhead off makes the pattern undirected, which matters here: an ACTED_IN edge points from person to film, so a route between two actors has to travel forwards along one edge and backwards along the next.
The other new piece is naming the path itself. Assigning a pattern to a variable with path = ... binds the whole route rather than its endpoints, so RETURN path hands the browser every node and relationship along the way.
Finding one shortest path
shortestPath() wraps a variable-length pattern and returns a single shortest route between its two endpoints:
//shortestPath function
MATCH (a1:Person {name: 'Keanu Reeves'}), (a2:Person {name: 'Carrie-Anne Moss'})
MATCH path = shortestPath((a1)-[*]-(a2))
RETURN path
The first MATCH binds the two endpoints, using a comma to separate two independent patterns rather than joining them into one. The comma is important: written as a single pattern the two would have to be connected, whereas here each is matched on its own and both are simply carried into the next clause. The second MATCH then searches for the shortest route between the nodes already bound to a1 and a2.
Keanu Reeves and Carrie-Anne Moss are two hops apart, having acted in the same films, so the path returned takes the form (Keanu)-[:ACTED_IN]->(a film)<-[:ACTED_IN]-(Carrie-Anne Moss). Note that the query never mentioned ACTED_IN: [*] follows whatever relationship types exist, and it is the data rather than the query that decides the route. Restricting the search to [:ACTED_IN*] would be both faster and more precise when the question really is about shared films.
Person nodes pointing at the single Movie node between them. The path is also spelled out as a row underneath, Keanu Reeves -ACTED_IN-> The Matrix Revolutions <-ACTED_IN- Carrie-Anne Moss.
Figure 42 shows both views of the same result. The graph makes the shape obvious, and the row beneath it is how the browser renders a path value: the nodes and relationships in order, with the arrows drawn in the direction the data stores them rather than the direction the search travelled. Reading it left to right, the route goes forwards out of Keanu Reeves and backwards into Carrie-Anne Moss, which is exactly why the pattern had to be written undirected.
The film in the middle is The Matrix Revolutions, which is the first thing to question: the pair also share The Matrix and The Matrix Reloaded, and either would have been an equally short route. Nothing about the data makes this one preferable; it is simply the one the search happened to return.
shortestPath() returns exactly one path even when several are equally short, and gives no indication that the others exist. Which one comes back is not defined, so it should be treated as a shortest path rather than the shortest path.
Finding all shortest paths
allShortestPaths() takes the same pattern and returns every route of that minimal length:
//allShortestPaths function
MATCH (a1:Person {name: 'Keanu Reeves'}), (a2:Person {name: 'Carrie-Anne Moss'})
MATCH path = allShortestPaths((a1)-[*]-(a2))
RETURN path
Here the result is three paths rather than one, because the two actors share all three Matrix films and each provides an independent two-hop route. The browser draws them as a single figure with the two Person nodes on the outside and the three Movie nodes between them, which makes the shape of the connection much clearer than a single path would.
Person nodes, three Movie nodes, and six ACTED_IN edges. The table underneath lists the routes separately, through The Matrix, The Matrix Reloaded, and The Matrix Revolutions.
Figure 43 puts the arbitrariness of Figure 42 in context. The row that shortestPath() returned is the third one here, and the other two were equally valid all along. The graph pane deduplicates shared nodes, so three paths of three nodes each are drawn as five circles rather than nine; the table pane keeps them separate, which is why the two views are worth reading together.
Both functions search only for the minimum length. A four-hop connection through a third actor also exists, but neither function will report it while a two-hop route is available; finding those requires a bounded pattern such as (a1)-[*..4]-(a2) without the shortestPath wrapper, which enumerates every route within the bound instead.
An unbounded [*] is only safe inside shortestPath() or allShortestPaths(), where Neo4j uses a breadth-first search that stops as soon as the target is reached. Writing MATCH path = (a1)-[*]-(a2) without the wrapper asks for every route between the two nodes, which on a well-connected graph explodes combinatorially and will exhaust memory. Always either wrap the pattern or give the * an upper bound.
These two functions measure distance in hops, treating every relationship as equally costly. Weighted questions, such as the cheapest route when edges carry a cost, need Dijkstra or A* from the Graph Data Science library rather than the Cypher built-ins shown here.
References
- Cramlays and Gujral, N. (2026) Neo4j: Cypher, GDS, GraphQL, LLM, Knowledge Graphs for RAG. Hands-on Course on Neo4j, Cypher, GDS, GraphQL, GraphRAG and Building Knowledge Graph from Unstructured Data Using LLM [Online course]. Udemy, April 2026. Available at: https://www.udemy.com/course/knowledge-graph-with-neo4j-cypher-gds/.