Let's get our database up and running! Run the following.
docker run -dit --rm --name=my-neo4j -p 7474:7474 -p 7687:7687 --env=NEO4J_AUTH=none neo4j:2026-community-trixie
This will spin up a new instance of Neo4j in docker and expose both its HTTP client and its querying port. We're going to start by querying the commandline called cypher-shell and then we'll move on to the awesome browser experience.
First thing to know is that the database is called Neo4j and the query language is called Cypher, just like the database is PostgreSQL and the query language is SQL. There are other graph query languages like Gremlin but we'll just be talking Cypher today.
So let's get connected. Run the following:
docker exec -it my-neo4j cypher-shell
If you see warnings on startup that has something to do with
org.fusesource.jansi.internal.JansiLoadermaking restricted method calls, that's just Java being Java and the cypher-shell needing to update its dependencies. It's nothing to do with you and doesn't affect what we're doing.
This should drop you into an interactive with Neo4j. The first thing we're going to do is use a CREATE statement to make our first actor, Michael Cera. I love the movie Scott Pilgrim vs. the World so we're going to describe the actors in it here.
CREATE (:Person {name:'Michael Cera', born:1988});
That
:before Person is very important - it means it's a label. If you leave it out, it's a variable declaration.
You can see we created a new node with a label of Person and two attributes: a name of Michael Cera and a birth year of 1988.
Now what if we want to find that same record?
MATCH (p {name: "Michael Cera"}) RETURN p;
- Now we're using a shorthand variable,
p. We could call this anything. - The first part represents what we're querying for. We didn't specify a label here but we could have. Then it would look like
MATCH (p {name: "Michael Cera"}) RETURN p; - You need the return at the end or you wouldn't get anything back.
Let's create a movie and then query for it.
CREATE (m:Movie {title: 'Scott Pilgrim vs the World', released: 2010, tagline: 'An epic of epic epicness.' }) RETURN m;
- This will create and return all in the same query because there's no semicolon so it's treated as one query.
- It's easy to make big complicated queries with Cypher.
- I did not make up that tag line. It is genuinely the tag line of that movie 😄
Let's now make them associated with each other so that Michael Cera acted in Scott Pilgrim vs the World.
MATCH (Michael:Person),(ScottVsWorld:Movie)
WHERE Michael.name = "Michael Cera" AND ScottVsWorld.title = "Scott Pilgrim vs the World"
CREATE (Michael)-[relationship:ACTED_IN {roles:["Scott Pilgrim"]}]->(ScottVsWorld)
RETURN relationship;
- The first match says we're looking for two separate things, a Person and a Movie.
- We then give a WHERE (there are a few ways to write queries that all work).
- We then identify that we're going to CREATE something new.
- This reads like ASCII art You have (node) - [RELATIONSHIP] -> (node). This identifies that Michael ACTED_IN Movie. The -> identifies the direction of the relationship. We also can totally write it as (Scott Pilgrim vs the World) <- [ACTED_IN] - (Michael Cera). Both work.
relationshipis a variable that refers to the new relationship we just created. It's optional but I wanted to return it at the end.- Neo4j recommends you do CapitalCasing with label names (like Person and Movie) and that you SCREAMING_CASE relationship types (like DIRECTED and ACTED_IN.) I just follow their recommendations. See here.
Okay, so let's put a few more relationships to the movie in. Copy/paste this to add a few more actors, and the director. (How many good actors were in Scott Pilgrim!?)
MATCH (ScottVsWorld:Movie) WHERE ScottVsWorld.title = "Scott Pilgrim vs the World"
CREATE (Anna:Person {name:'Anna Kendrick', born:1985})
CREATE (Brie:Person {name:'Brie Larson', born:1989})
CREATE (Aubrey:Person {name:'Aubrey Plaza', born:1984})
CREATE (Mary:Person {name:'Mary Elizabeth Winstead', born:1984})
CREATE (Kieran:Person {name:'Kieran Culkin', born:1982})
CREATE (Chris:Person {name:'Chris Evans', born:1981})
CREATE (Edgar:Person {name:'Edgar Wright', born:1974})
CREATE
(Anna)-[:ACTED_IN {roles:['Stacey Pilgrim']}]->(ScottVsWorld),
(Brie)-[:ACTED_IN {roles:['Envy Adams']}]->(ScottVsWorld),
(Aubrey)-[:ACTED_IN {roles:['Julie Powers']}]->(ScottVsWorld),
(Mary)-[:ACTED_IN {roles:['Ramona Flowers']}]->(ScottVsWorld),
(Kieran)-[:ACTED_IN {roles:['Wallace Wells']}]->(ScottVsWorld),
(Chris)-[:ACTED_IN {roles:['Lucas Lee']}]->(ScottVsWorld),
(Edgar)-[:DIRECTED]->(ScottVsWorld);
Shouldn't be anything too surprising, just a lot of stuff being added at once.
So let's do a relationship query now. Find all movies Aubrey Plaza has acted in according to our database.
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE p.name = "Aubrey Plaza"
RETURN m.title;
- It's actually pretty easy when you see it written out. That's one of the nice parts of Cypher is that it reads well for the most part. It can start having a lot of
([{}])which can get to be a bit much. - The
>part of->is optional. If you omit the direction in the query it just assumes you're saying "find me a relationship here, I don't care which way the direction goes." Because it's an actor acting in a movie and the inverse relation doesn't make any sense (a movie acting in an actor?) it's fine here to just make that assumption.
One thing you'll notice is that Aubrey Plaza isn't connected to any of the other people directly, just via being attached to the same movie. What if we wanted to find every person who acted in the same movie as Aubrey (in this case everyone we've added so far.)
MATCH (p:Person)-[:ACTED_IN]->(Movie)<-[:ACTED_IN]-(q:Person)
WHERE p.name = "Aubrey Plaza" AND q.name <> "Aubrey Plaza"
RETURN q.name;
- There you go! We just describe out the relationship a bit further and use those variables.
<>is how you do not equals in Cypher- Technically Aubrey was in the movie with herself? In any case we have to say don't include her in the results if we don't want to have her.
But what if we wanted to find everyone who was younger than Aubrey that acted in the same movie?
MATCH (p:Person)-[:ACTED_IN]->(Movie)<-[:ACTED_IN]-(q:Person)
WHERE p.name = "Aubrey Plaza" AND q.born > p.born
RETURN q.name;
Constraint
Just like in the other databases you can enforce uniqueness which can be helpful. Here's how you'd do that (though a bad idea in this case because there are lots of actors and directors named the same thing as there are multiple movies called the same thing.)
CREATE CONSTRAINT FOR (a:Person) REQUIRE a.name IS UNIQUE;
CREATE CONSTRAINT FOR (a:Movie) REQUIRE a.title IS UNIQUE;
Neo4j version 5 removed the old syntax that used
ON/ASSERTand now you have to useFOR/REQUIRE.