Introduction
Welcome to our in-depth Neo4j tutorial focused on logical operators and relationship operations! This guide is based on the YouTube video "Neo4j - IN, AND, OR, Creating, Retrieving and Modifying Relationships" from the NoSQL Databases series. In this tutorial, we'll explore how to effectively use logical operators in Cypher queries and master the essential skills of creating, retrieving, and modifying relationships in your graph database.
Relationships are at the heart of what makes graph databases powerful. While nodes store your entities, it's the relationships between them that truly unlock the value of a graph database like Neo4j. By the end of this tutorial, you'll have a solid understanding of how to work with relationships and combine them with logical operators to create powerful queries.
Setting Up Our Database
Before diving into operators and relationships, let's set up a sample database to work with. We'll create a simple social network with people, their interests, and various connections between them.
Creating Person Nodes
// Create several person nodes
CREATE (john:Person {name: "John", age: 28, city: "New York"})
CREATE (sarah:Person {name: "Sarah", age: 26, city: "Boston"})
CREATE (mike:Person {name: "Mike", age: 32, city: "Chicago"})
CREATE (emily:Person {name: "Emily", age: 24, city: "New York"})
CREATE (david:Person {name: "David", age: 35, city: "San Francisco"})
CREATE (laura:Person {name: "Laura", age: 29, city: "Chicago"})
Creating Interest Nodes
// Create several interest nodes
CREATE (music:Interest {name: "Music", type: "Art"})
CREATE (coding:Interest {name: "Coding", type: "Technology"})
CREATE (hiking:Interest {name: "Hiking", type: "Outdoor"})
CREATE (photography:Interest {name: "Photography", type: "Art"})
CREATE (cooking:Interest {name: "Cooking", type: "Culinary"})
Understanding Logical Operators in Cypher
Let's first explore how to use the logical operators IN, AND, and OR in Cypher queries. These operators help us filter results based on multiple conditions.
The IN Operator
The IN operator allows you to check if a property value is in a specified list of values. It's a shorthand for multiple OR conditions.
// Find people who live in either New York or Chicago
MATCH (p:Person)
WHERE p.city IN ["New York", "Chicago"]
RETURN p.name, p.city
Expected result:
| p.name | p.city |
|---------|-----------|
| "John" | "New York"|
| "Emily" | "New York"|
| "Mike" | "Chicago" |
| "Laura" | "Chicago" |
The AND Operator
The AND operator is used to combine multiple conditions, all of which must be true for the record to be included in the results.
// Find people who are over 25 and live in Chicago
MATCH (p:Person)
WHERE p.age > 25 AND p.city = "Chicago"
RETURN p.name, p.age, p.city
Expected result:
| p.name | p.age | p.city |
|---------|-------|-----------|
| "Mike" | 32 | "Chicago" |
| "Laura" | 29 | "Chicago" |
The OR Operator
The OR operator is used to include records where at least one of the conditions is true.
// Find people who are either under 25 or over 30
MATCH (p:Person)
WHERE p.age < 25 OR p.age > 30
RETURN p.name, p.age
Expected result:
| p.name | p.age |
|---------|-------|
| "Mike" | 32 |
| "Emily" | 24 |
| "David" | 35 |
Combining Multiple Operators
You can combine these operators to create more complex queries. Use parentheses to control the order of evaluation.
// Find people who live in New York and are either under 25 or over 30
MATCH (p:Person)
WHERE p.city = "New York" AND (p.age < 25 OR p.age > 30)
RETURN p.name, p.age, p.city
Expected result:
| p.name | p.age | p.city |
|---------|-------|-----------|
| "Emily" | 24 | "New York"|
Creating Relationships
Now let's look at how to create relationships between nodes in our graph. Relationships in Neo4j are directed, have a type, and can contain properties.
Basic Relationship Creation
// Create friendship relationships
MATCH (john:Person {name: "John"}), (sarah:Person {name: "Sarah"})
CREATE (john)-[:FRIENDS_WITH {since: 2019}]->(sarah)
MATCH (mike:Person {name: "Mike"}), (john:Person {name: "John"})
CREATE (mike)-[:FRIENDS_WITH {since: 2018}]->(john)
MATCH (emily:Person {name: "Emily"}), (sarah:Person {name: "Sarah"})
CREATE (emily)-[:FRIENDS_WITH {since: 2020}]->(sarah)
MATCH (david:Person {name: "David"}), (mike:Person {name: "Mike"})
CREATE (david)-[:FRIENDS_WITH {since: 2017}]->(mike)
MATCH (laura:Person {name: "Laura"}), (david:Person {name: "David"})
CREATE (laura)-[:FRIENDS_WITH {since: 2021}]->(david)
Creating Multiple Relationships at Once
You can create multiple relationships in a single query. This is more efficient than creating them one by one.
// Create interest relationships
MATCH (john:Person {name: "John"}), (music:Interest {name: "Music"}),
(hiking:Interest {name: "Hiking"})
CREATE (john)-[:INTERESTED_IN {level: "high"}]->(music),
(john)-[:INTERESTED_IN {level: "medium"}]->(hiking)
MATCH (sarah:Person {name: "Sarah"}), (photography:Interest {name: "Photography"}),
(cooking:Interest {name: "Cooking"})
CREATE (sarah)-[:INTERESTED_IN {level: "high"}]->(photography),
(sarah)-[:INTERESTED_IN {level: "high"}]->(cooking)
MATCH (mike:Person {name: "Mike"}), (coding:Interest {name: "Coding"}),
(hiking:Interest {name: "Hiking"})
CREATE (mike)-[:INTERESTED_IN {level: "high"}]->(coding),
(mike)-[:INTERESTED_IN {level: "medium"}]->(hiking)
MATCH (emily:Person {name: "Emily"}), (music:Interest {name: "Music"}),
(photography:Interest {name: "Photography"})
CREATE (emily)-[:INTERESTED_IN {level: "medium"}]->(music),
(emily)-[:INTERESTED_IN {level: "high"}]->(photography)
Creating Bidirectional Relationships
In some cases, you want to create relationships in both directions. This can be done with two CREATE statements.
// Create mutual friendship between David and Laura
MATCH (david:Person {name: "David"}), (laura:Person {name: "Laura"})
CREATE (david)-[:FRIENDS_WITH {since: 2021}]->(laura)
// The reverse relationship already exists from our previous queries
Retrieving Relationships
Now that we've created relationships, let's explore different ways to retrieve and query them.
Basic Relationship Retrieval
// Find all friendships
MATCH (p1:Person)-[r:FRIENDS_WITH]->(p2:Person)
RETURN p1.name, p2.name, r.since
Filtering Relationships by Properties
// Find friendships established in 2020 or later
MATCH (p1:Person)-[r:FRIENDS_WITH]->(p2:Person)
WHERE r.since >= 2020
RETURN p1.name, p2.name, r.since
Finding Specific Relationship Patterns
// Find friends of John
MATCH (john:Person {name: "John"})-[:FRIENDS_WITH]->(friend)
RETURN friend.name
// Find people who are interested in hiking
MATCH (p:Person)-[:INTERESTED_IN]->(:Interest {name: "Hiking"})
RETURN p.name
Using Logical Operators with Relationships
// Find people who are interested in both music and photography
MATCH (p:Person)
WHERE (p)-[:INTERESTED_IN]->(:Interest {name: "Music"})
AND (p)-[:INTERESTED_IN]->(:Interest {name: "Photography"})
RETURN p.name
Using IN with Relationship Types
// Find any kind of relationship between people and interests
MATCH (p:Person)-[r:INTERESTED_IN|CREATED|CONTRIBUTED_TO]->(i:Interest)
RETURN p.name, type(r), i.name
Finding Paths with Variable Length Relationships
// Find friends of friends (people connected to John through 2 FRIENDS_WITH relationships)
MATCH (john:Person {name: "John"})-[:FRIENDS_WITH*2]->(fof)
RETURN john.name, fof.name
Modifying Relationships
Relationships can be modified after creation. Let's look at how to update relationship properties and change relationship structure.
Updating Relationship Properties
// Update a friendship's 'since' property
MATCH (john:Person {name: "John"})-[r:FRIENDS_WITH]->(sarah:Person {name: "Sarah"})
SET r.since = 2018
RETURN john.name, sarah.name, r.since
// Add a new property to all INTERESTED_IN relationships
MATCH (p:Person)-[r:INTERESTED_IN]->(i:Interest)
SET r.updated = date()
RETURN p.name, i.name, r.level, r.updated
Deleting Relationships
// Remove a specific friendship
MATCH (mike:Person {name: "Mike"})-[r:FRIENDS_WITH]->(john:Person {name: "John"})
DELETE r
// Remove all high-level interest relationships
MATCH (:Person)-[r:INTERESTED_IN {level: "high"}]->(:Interest)
DELETE r
Replacing Relationships
Sometimes you may want to replace a relationship with a different one.
// Replace an INTERESTED_IN relationship with EXPERT_IN
MATCH (mike:Person {name: "Mike"})-[r:INTERESTED_IN]->(:Interest {name: "Coding"})
WHERE r.level = "high"
DELETE r
CREATE (mike)-[:EXPERT_IN {since: 2015}]->(:Interest {name: "Coding"})
Merging Relationships
The MERGE command can be used to either match existing relationships or create them if they don't exist.
// Create a friendship between Emily and Mike if it doesn't exist
MATCH (emily:Person {name: "Emily"}), (mike:Person {name: "Mike"})
MERGE (emily)-[r:FRIENDS_WITH]->(mike)
ON CREATE SET r.since = 2022, r.through = "Work"
ON MATCH SET r.updated = date()
RETURN emily.name, mike.name, r
Practical Use Cases
Let's explore some practical use cases that combine everything we've learned about operators and relationships.
Recommendation Engine
// Recommend new interests to John based on what his friends like
MATCH (john:Person {name: "John"})-[:FRIENDS_WITH]->(friend)-[:INTERESTED_IN]->(interest)
WHERE NOT (john)-[:INTERESTED_IN]->(interest)
RETURN interest.name, count(friend) as frequency
ORDER BY frequency DESC
Finding Mutual Connections
// Find mutual friends between Sarah and Emily
MATCH (sarah:Person {name: "Sarah"})<-[:FRIENDS_WITH]-(mutualFriend)-[:FRIENDS_WITH]->(emily:Person {name: "Emily"})
RETURN mutualFriend.name
Complex Pattern Recognition
// Find people who live in the same city, share at least one interest, but aren't friends yet
MATCH (p1:Person)-[:INTERESTED_IN]->(i:Interest)<-[:INTERESTED_IN]-(p2:Person)
WHERE p1.city = p2.city AND p1.name < p2.name
AND NOT (p1)-[:FRIENDS_WITH]-(p2)
RETURN p1.name, p2.name, p1.city, collect(i.name) as sharedInterests
Advanced Techniques
Here are some advanced techniques for working with relationships in Neo4j.
Using FOREACH for Conditional Relationship Creation
// Create friendships between all people from Chicago
MATCH (p1:Person), (p2:Person)
WHERE p1.city = "Chicago" AND p2.city = "Chicago" AND p1 <> p2
FOREACH (ignoreMe IN CASE WHEN NOT (p1)-[:FRIENDS_WITH]->(p2) THEN [1] ELSE [] END |
CREATE (p1)-[:FRIENDS_WITH {since: 2022, reason: "Same city"}]->(p2)
)
Using UNWIND for Batch Relationship Creation
// Create multiple interest relationships at once
MATCH (david:Person {name: "David"})
UNWIND ["Coding", "Hiking", "Cooking"] as interestName
MATCH (interest:Interest {name: interestName})
CREATE (david)-[:INTERESTED_IN {level: "medium"}]->(interest)
Relationship Uniqueness
// Ensure no duplicate FRIENDS_WITH relationships exist
MATCH (p1:Person)-[r:FRIENDS_WITH]->(p2:Person)
WITH p1, p2, COLLECT(r) as rels
WHERE SIZE(rels) > 1
UNWIND TAIL(rels) as extraRel
DELETE extraRel
Best Practices for Working with Relationships
Design relationships carefully: Consider the directionality, type, and properties that best model your domain.
Use meaningful relationship types: Names like
FRIENDS_WITH
,BELONGS_TO
, orPURCHASED
clearly communicate the nature of the connection.Optimize relationship traversal: The power of Neo4j comes from efficient relationship traversal. Structure your queries to take advantage of this.
Be cautious with variable-length paths: While powerful, queries like
[:FRIENDS_WITH*1..5]
can become expensive on large databases.Index properties used in filters: For properties frequently used in WHERE clauses, create appropriate indexes.
Consider bidirectional relationships: For relationships like friendships, where traversal in both directions is common, consider creating relationships in both directions.
Use parameters in queries: Instead of hardcoding values, use parameters for better security and query plan caching.
Conclusion
In this tutorial, we've explored the power of logical operators (IN, AND, OR) in Cypher and how to create, retrieve, and modify relationships in Neo4j. Relationships are what make graph databases special, allowing you to model and query connected data in a way that's both intuitive and powerful.
By mastering these concepts, you're well on your way to leveraging the full potential of Neo4j for your applications. The ability to express complex patterns and relationships in simple, readable queries is one of the most compelling reasons to use a graph database.