Finding Patterns in Graphs

Introduction

Welcome back to Advanced Queries! You've now completed the first lesson, where we explored how to aggregate data and extract summary insights from graph databases. In this second unit, we're taking our querying skills further by learning how to find complex patterns within our graphs.

Pattern matching is where graph databases truly shine. While aggregations help us summarize data, pattern matching allows us to discover hidden connections, recommend new relationships, and uncover insights that would be difficult or impossible to find in traditional databases. Think about how social networks suggest new friends, how e-commerce sites recommend products, or how fraud detection systems identify suspicious activity patterns.

In this lesson, we'll learn how to construct sophisticated pattern-matching queries that traverse multiple relationships, filter results based on specific conditions, and rank recommendations by relevance. By the end, you'll understand how to build queries that power real-world recommendation systems and analytical tools.

Understanding Pattern Matching

Pattern matching is the process of describing a graph structure we want to find and letting Neo4j search for all instances of that pattern. We've already used simple patterns in previous work, like (user:User) to find users or (person)-[:FRIENDS_WITH]->(friend) to find friendships. Now, we'll learn to chain these patterns together to find more complex structures.

The power of pattern matching lies in its ability to traverse multiple hops through the graph in a single query. Instead of finding direct connections, we can search for indirect relationships, such as friends of friends, colleagues of colleagues, or products purchased by similar customers. These multi-hop patterns reveal the hidden fabric of connections within our data.

Cypher makes this intuitive through its visual syntax. We simply describe the path we want to find using familiar node and relationship notation, and the database engine handles the complex traversal logic behind the scenes.

The Challenge of Recommendations

One of the most common applications of pattern matching is building recommendation systems. Consider a social network: when we log in, the platform suggests people we might know. How does it determine these recommendations? The answer often lies in mutual connections.

If Alice is friends with Bob, and Bob is friends with Carol, but Alice and Carol aren't friends yet, then Carol might be a good recommendation for Alice. This is a "friend of a friend" pattern. The more friends Alice and Carol have in common, the stronger the recommendation becomes.

This type of analysis is challenging in traditional databases because it requires joining tables multiple times and tracking indirect relationships. In a graph database, however, we can express this pattern naturally and efficiently using Cypher's pattern-matching syntax.

Building Recommendation Queries

Let's start by finding people whom a specific user might know. We'll use Alice as our example and search for friends of her friends.

Consider this example graph structure to understand the concept. Alice is directly connected to Bob and Carol. But notice Emma—she's not directly connected to Alice, yet she's reachable through two hops: Alice → Bob → Emma and Alice → Carol → Emma. This is exactly the pattern we want to capture.

Note: This diagram illustrates the conceptual pattern we're building. When you run the queries below, you'll see outputs from our actual database, which contains different names and relationships. The pattern-matching logic, however, works exactly the same way.

We can express this pattern in Cypher:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->()-[:FRIENDS_WITH]->(rec)
RETURN rec.name

This pattern describes a path starting from Alice, following a FRIENDS_WITH relationship to someone (we don't need to name this intermediate person), then following another FRIENDS_WITH relationship to a potential recommendation. The middle node uses () without a variable name because we only care about the endpoints—Alice and the potential recommendation.

Sample Output:

rec.name
"Bob"
"Frank"
"Grace"
"Diana"
"Alice"
"Frank"
"Eve"
"Diana"
"Alice"

Notice that this output includes some problems: Alice herself appears twice (through circular paths), Bob appears even though he's already her friend, and Diana and Frank appear multiple times. We'll address these issues in the next steps.

The arrows form a chain: one relationship from Alice to her friend, then another relationship from that friend to someone new. This two-hop traversal finds candidates who are connected to Alice through mutual friends, even though they're not directly connected to her.

The pattern reads almost like a sentence: "Match Alice, who is friends with someone, who is friends with rec." This visual clarity makes Cypher patterns easy to understand and maintain.

Excluding Existing Connections

Our initial pattern finds all friends of Alice's friends, but it has two problems. First, it might return Alice herself if the pattern loops back to her through mutual connections. Second, it includes people Alice is already friends with, which aren't useful recommendations.

We solve both issues using the WHERE clause to filter unwanted results:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->()-[:FRIENDS_WITH]->(rec)
WHERE me <> rec AND NOT (me)-[:FRIENDS_WITH]->(rec)
RETURN rec.name

The WHERE clause adds two important filters. First, me <> rec ensures we don't recommend Alice to herself. Second, NOT (me)-[:FRIENDS_WITH]->(rec) excludes anyone who is already Alice's friend. The NOT keyword checks that a pattern does not exist, making it perfect for filtering out existing relationships.

Sample Output:

rec.name
"Frank"
"Grace"
"Diana"
"Frank"
"Eve"
"Diana"

Much better! Now we see only genuine new recommendations—no Alice, no Bob, no Charlie. However, notice that Diana and Frank still appear twice because they're reachable through multiple mutual friends. We'll handle that next.

Together, these conditions ensure we only get genuine new recommendations.

Understanding the Pattern Flow

Let's refine our recommendation query by ensuring each person appears only once:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->()-[:FRIENDS_WITH]->(rec)
WHERE me <> rec AND NOT (me)-[:FRIENDS_WITH]->(rec)
RETURN DISTINCT rec.name

The DISTINCT keyword is crucial here because the same person might appear multiple times through different mutual friends. If both Bob and Charlie are friends with Alice and both are also friends with David, David would appear twice in our results without DISTINCT. Using it ensures each recommendation appears only once.

Sample Output:

rec.name
"Frank"
"Grace"
"Diana"
"Eve"

This query gives us a basic list of people Alice might know, but it doesn't tell us how well she might know them. That's where our next refinement comes in.

Adding Intelligence with Mutual Friends

A recommendation becomes more valuable when we know how many mutual connections exist. If Alice shares five friends with someone versus just one, that person is likely a stronger recommendation. To add this intelligence, we need to count the mutual connections.

The key insight is naming our intermediate node so we can reference it in our aggregation:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->(m)-[:FRIENDS_WITH]->(rec)
WHERE me <> rec AND NOT (me)-[:FRIENDS_WITH]->(rec)

Now, instead of using () for the middle node, we use (m) to give it a variable name. This allows us to count how many different values of m lead to each rec, effectively counting mutual friends between Alice and each recommendation.

Counting Mutual Connections

With our intermediate node named, we can use aggregation functions to count mutual friends:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->(m)-[:FRIENDS_WITH]->(rec)
WHERE me <> rec AND NOT (me)-[:FRIENDS_WITH]->(rec)
RETURN rec.name, count(m) AS mutualFriends

As you may recall from our previous unit on aggregation, count(m) creates an implicit grouping. Cypher groups results by rec.name (the non-aggregated expression) and counts how many different mutual friends m exist for each group. This gives us exactly what we need: a count of shared connections for each recommendation.

Sample Output:

rec.name, mutualFriends
"Frank", 2
"Grace", 1
"Diana", 2
"Eve", 1

The result shows not just whom Alice might know, but how strongly connected they are through mutual friends. Diana and Frank each have 2 mutual friends with Alice, making them stronger recommendations than Eve and Grace, who each have only 1 mutual friend.

Ordering and Limiting Results

Raw recommendations are useful, but for a real application, we want to show the best suggestions first. We can rank recommendations by the number of mutual friends and limit our results:

MATCH (me:User {name: 'Alice'})-[:FRIENDS_WITH]->(m)-[:FRIENDS_WITH]->(rec)
WHERE me <> rec AND NOT (me)-[:FRIENDS_WITH]->(rec)
RETURN rec.name, count(m) AS mutualFriends
ORDER BY mutualFriends DESC
LIMIT 3

The ORDER BY mutualFriends DESC clause sorts our results with the highest counts first, ensuring the strongest recommendations appear at the top. The LIMIT 3 clause restricts our output to just the top three recommendations, making the results manageable and focused.

Sample Output:

rec.name, mutualFriends
"Frank", 2
"Diana", 2
"Grace", 1

Notice how changing the LIMIT value affects the output—we now see only the top 3 recommendations instead of all 4. This combination of pattern matching, filtering, aggregation, ordering, and limiting creates a sophisticated recommendation engine in just a few lines of Cypher.

Real-World Applications

The pattern-matching techniques we've explored extend far beyond friend recommendations. These same principles power numerous real-world applications across different domains.

In e-commerce, product recommendation engines use similar patterns to suggest items: "customers who bought product A also bought product B." Fraud detection systems identify suspicious patterns by looking for unusual relationship structures, such as multiple accounts sharing the same phone number or address. Professional networking platforms use mutual connection counts to rank potential contacts and suggest relevant business connections.

Content streaming services analyze viewing patterns across similar users to recommend movies or shows. Supply chain management systems trace product paths through multiple suppliers and distributors to identify bottlenecks or risks. Each of these applications relies on the ability to match complex patterns across multiple relationships and filter results based on specific criteria.

Conclusion and Next Steps

In this lesson, we've discovered how pattern matching transforms graph databases into powerful analytical tools. We learned to construct multi-hop patterns that traverse friend-of-friend relationships, use the WHERE clause to filter unwanted results, combine pattern matching with aggregation to count mutual connections, and rank recommendations using ORDER BY and LIMIT.

These techniques form the foundation of recommendation systems, fraud detection, and many other real-world applications. The ability to describe complex graph structures naturally in Cypher and have Neo4j efficiently find all matching instances is what makes graph databases so powerful for connected data.

Now it's your turn to apply these concepts! The practice exercises ahead will challenge you to build your own pattern-matching queries, helping you develop the skills to tackle real-world graph analysis problems. Get ready to discover hidden patterns in data!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal