Lesson 05

Patterns & Path Finding

DifficultyIntermediate Time

Learning objectives

After this lesson you will be able to:

  1. Reason about relationship direction versus undirected patterns.
  2. Bind path variables with MATCH p = (...).
  3. Write bounded variable-length patterns such as *1..3.
  4. Use shortestPath() and allShortestPaths() for minimum-hop routes.
  5. Explain relationship uniqueness (isomorphism) in classic Cypher expansions.
  6. Note how Cypher 25 quantified path patterns modernize multi-hop syntax (version-sensitive).
  7. Apply path techniques to Movie-graph questions like co-actor distance.

Core concepts

Direction, path variables, and variable-length patterns

Relationships in Neo4j are stored with a direction. Pattern arrows must respect that storage unless you deliberately write an undirected pattern. Directed patterns answer oriented questions (“who directed this film?”). Undirected patterns answer connectivity questions where orientation is irrelevant (“are these people linked through shared films in either modeled direction?”). Mixing them up is the number-one cause of empty path results for intermediate learners.

A path variable captures an entire matched walk: nodes and relationships together. Write MATCH p = (a)-[:ACTED_IN]->(m)<-[:ACTED_IN]-(b) and you can RETURN p, length(p), nodes(p), or relationships(p). Paths are values—great for visualization and for feeding path functions.

Variable-length relationship patterns expand a type repeatedly: -[:ACTED_IN*1..2]- means one or two ACTED_IN hops in an undirected sense when written without arrows (or follow the arrows you draw). Bounds are not optional in careful systems work: unbounded * can explode on dense social graphs. Always prefer an upper bound that matches the business question (for example “within two collaborations”).

When paths shine: recommendations, impact analysis, fraud rings, dependency depth, and any UI that draws a route. Prefer fixed two-hop patterns when the shape is known; use variable length when the hop count is a range; use shortest-path functions when the goal is a minimum route between two fixed ends.

CYPHER
MATCH p = (a:Person {name: "Tom Hanks"})-[:ACTED_IN*1..2]-(b:Person)
WHERE a <> b
// path variable p captures the walk; length(p) is hop count
RETURN b.name AS other, length(p) AS hops
ORDER BY hops, other
LIMIT 10
Variable-length path diagram from Tom Hanks through movies to other Person nodes within two hops
Bounded *1..2 expansion finds nearby people through shared ACTED_IN links.

shortestPath, allShortestPaths, uniqueness, and Cypher 25

shortestPath((a)-[*..5]-(b)) finds one path with minimum relationship count (among paths matching the pattern and optional max depth). When several routes share that same minimum length, which one you get is not uniquely determined—Neo4j may return any equal-length path. Use allShortestPaths(...) to enumerate every path that shares that minimum length, for example when multiple equally short collaboration chains exist between two actors. Always supply a reasonable upper bound in the pattern to protect the database.

Classic Cypher expansions obey relationship uniqueness (relationship-isomorphism): a single MATCH path does not reuse the same relationship twice. That prevents many trivial cycles from spinning forever, but it is a semantic rule you must remember when interpreting counts. Node uniqueness is different—nodes can reappear depending on the pattern and functions used. Read plans carefully when results surprise you.

Cypher 25 quantified path patterns introduce a more expressive, modern syntax for repeated path segments (quantifiers on parenthesized path patterns). If your Neo4j version supports Cypher 25 features, prefer the manual’s quantified-path forms for new multi-hop work—they make grouping, predicates on path segments, and intent clearer than older *n..m alone. This course still shows classic variable-length syntax because it remains widely deployed; treat Cypher 25 as the forward-looking note to check against your server version before rewriting production queries.

Practical path design on the Movie graph

Co-actor questions are natural: people share ACTED_IN edges through movies. A two-hop undirected pattern (a:Person)-[:ACTED_IN]-(m:Movie)-[:ACTED_IN]-(b:Person) is often clearer than a variable-length pattern when the hop count is fixed. Use variable length when the hop budget varies. Use shortestPath when endpoints are known and you want the tightest collaboration chain.

Filter with WHERE to exclude the start person, require specific movies, or cap path length via length(p). Project names—not entire paths—when you only need a table; return the path when Browser visualization helps teaching.

Worked examples

Dataset note: Expected results use the classic Neo4j Movie graph from the Getting Started tutorial (37 movies, 133 people). Your results can differ if you change or extend the data.

Directed versus undirected ACTED_IN

Both queries start from Tom Hanks. The directed form follows stored ACTED_IN orientation into movies; co-actor discovery uses undirected or mirrored patterns through the movie.

CYPHER
MATCH (tom:Person {name: "Tom Hanks"})-[:ACTED_IN]->(m:Movie)
RETURN m.title AS movie
ORDER BY movie
LIMIT 5

Expected result

movie
A League of Their Own
Apollo 13
Cast Away
Charlie Wilsons War
Cloud Atlas

Bounded variable-length co-actors

Find other people within 1..2 undirected ACTED_IN hops of Meg Ryan, ordered by name.

CYPHER
MATCH (meg:Person {name: "Meg Ryan"})-[:ACTED_IN*1..2]-(other:Person)
WHERE meg <> other
RETURN DISTINCT other.name AS person
ORDER BY person
LIMIT 5

Expected result

person
Anthony Edwards
Bill Pullman
Billy Crystal
Bruno Kirby
Carrie Fisher

shortestPath between two actors

Compute one shortest collaboration path between Tom Hanks and Keanu Reeves through ACTED_IN links (people and movies alternate along the path). shortestPath may return any one of several equal-length routes; the table below is one valid example of hop length 4—use allShortestPaths if you need every tied path, not a single representative.

CYPHER
MATCH (a:Person {name: "Tom Hanks"}), (b:Person {name: "Keanu Reeves"})
MATCH p = shortestPath((a)-[:ACTED_IN*..6]-(b))
RETURN length(p) AS hops,
       [n IN nodes(p) | coalesce(n.name, n.title)] AS route

Expected result (one possible equal-length path)

hops route
4 ["Tom Hanks", "Cloud Atlas", "Hugo Weaving", "The Matrix", "Keanu Reeves"]

Pitfalls

  • Unbounded variable-length expansion

    Patterns like -[*]- without upper bounds can be extremely expensive. Always bound hops for exploratory and production queries.

  • Wrong direction on path searches

    shortestPath with a directed pattern only follows that orientation. Use undirected patterns when collaboration is symmetric in your question.

  • Misreading uniqueness rules

    Classic MATCH will not reuse the same relationship in one path. If you expected a cyclic walk that reuses an edge, Cypher will not produce it under those rules.

  • Ignoring version differences for quantified paths

    Cypher 25 quantified path syntax may not exist on older servers. Check neo4j version and the Cypher Manual before deploying new path syntax.

Exercises

  1. 1. Path variable for co-actors

    MATCH p = (tom)-[:ACTED_IN]->(m)<-[:ACTED_IN]-(co) for Tom Hanks; RETURN length(p), co.name, m.title LIMIT 10.

    STARTER
    MATCH (tom:Person {name: 'Tom Hanks'})
    // MATCH p = (tom)-[:ACTED_IN]->(m)<-[:ACTED_IN]-(co)
    RETURN tom
  2. 2. Variable-length *2..2

    Find people exactly two undirected ACTED_IN hops from Carrie-Anne Moss; RETURN distinct names.

    STARTER
    MATCH (a:Person {name: 'Carrie-Anne Moss'})-[:ACTED_IN*2..2]-(b:Person)
    // filter a <> b, return distinct b.name
    RETURN b
  3. 3. allShortestPaths

    Between Tom Hanks and Meg Ryan, use allShortestPaths over ACTED_IN undirected up to 6 hops. RETURN length and the name/title route list.

    STARTER
    MATCH (a:Person {name: 'Tom Hanks'}), (b:Person {name: 'Meg Ryan'})
    // MATCH p = allShortestPaths((a)-[:ACTED_IN*..6]-(b))
    RETURN a, b
  4. 4. Version check note

    Run CALL dbms.components() YIELD name, versions RETURN * (or your deployment’s version query). In comments, note whether you would use classic *n..m or look up Cypher 25 quantified paths for new code.

    STARTER
    // Check server version, then draft a quantified-path or *1..3 pattern you prefer
    MATCH (n)
    RETURN count(n) AS nodes

Lesson quiz

Five questions. Submit once to see your score and the full answer key.

Loading quiz…

Resources

Video

Watch on YouTube (opens in a new tab).