Use Neo4j for Graph Queries

Use Neo4j for graph queries — Applied AI engineering.

Focus: use neo4j for graph queries

Sponsored

You've mastered relational tables, but now your AI application needs to answer questions that are fundamentally about relationships: "Which users share mutual interests with this influencer?" "What's the shortest path between two concepts in a knowledge graph?" Relational databases stumble on multi-hop traversals, forcing slow JOIN chains and recursive CTEs. By the end of this lesson, you'll be able to use Neo4j for graph queries with Python — modeling your data as nodes and relationships, and using Cypher to extract insights in milliseconds that would take seconds in SQL.

The problem this lesson solves

Most developers default to relational databases for everything, but graph data — like social networks, recommendation engines, fraud detection, or knowledge graphs — becomes painfully awkward in SQL. Let's see why.

Consider a simple query: "Find friends of friends who also like the same movies as user 123." In SQL with a classic friendships table, you'd write a self-join:

SELECT DISTINCT f2.user_id
FROM friendships f1
JOIN friendships f2 ON f1.friend_id = f2.user_id
JOIN likes l1 ON f1.user_id = l1.user_id AND l1.movie_id = ...
JOIN likes l2 ON f2.user_id = l2.user_id AND l2.movie_id = ...
WHERE f1.user_id = 123;

That's four JOINs for a two-hop query. Now imagine “friends of friends of friends” (three hops) — the SQL explodes into a monster. Queries become unreadable, slow, and nearly impossible to optimize once your data grows.

Relational databases also struggle with variable-length paths. Finding all users within 3 hops of a person requires recursive CTEs or hundreds of lines of procedural code — neither is elegant nor efficient. And adding a new constraint (e.g., "only consider friendships older than 6 months") means rewriting large chunks of logic.

The pain is real: AI applications that build recommendation engines, search knowledge graphs, or detect fraud need relationship traversal at scale, and using a relational database ends up costing you debugging time, latency, and complexity.

Pro tip: If your queries are mostly "find rows where column = value," stick with Postgres. But if your queries are mostly "traverse relationships," you need a graph database.

Core concept / mental model

Think of a graph database as a colored map of cities and roads. Each city is a node, each road is a relationship. Both can have properties — the city might have a population, the road might have a distance or a speed limit.

In Neo4j, you write Cypher, a declarative query language designed for graphs. Instead of joining tables, you describe the pattern you want to find, and the engine does the heavy lifting of traversing nodes and relationships.

Here's the mental model:

  • Node = a person, product, movie, event, concept — anything with an identity.
  • Relationship = an edge that connects two nodes. It has a type (e.g., FRIENDS_WITH) and a direction (can be one-way or two-way).
  • Properties = key-value pairs on either nodes or relationships (like name, since, weight).
  • Labels = a way to group related nodes (e.g., :Person, :Movie).
  • Pattern = a combination of nodes and relationships that you want to match, written like (person)-[:LIKES]->(movie).
  • Traversal = the process of walking from one node to the next along relationships.

Diagram in words: (:User {name:'Alice'})-[:FRIENDS_WITH]->(:User {name:'Bob'})-[:LIKES]->(:Movie {title:'Inception'})

By using Neo4j for graph queries, you're treating relationships as first-class citizens. That's the key shift from the relational model.

How it works step by step

Let's break down how to get from zero to executing your first graph query in Python.

Step 1: Set up a Neo4j instance

You can either use Neo4j Desktop, a local server, or a cloud instance (like Neo4j AuraDB). For local development, the simplest is to run it in Docker:

docker run -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/testpassword -d neo4j:5

This gives you a browser UI on port 7474 and the Bolt protocol on port 7687.

Step 2: Install the Python driver

pip install neo4j

Step 3: Connect to the database

from neo4j import GraphDatabase

driver = GraphDatabase.driver(
    "bolt://localhost:7687",
    auth=("neo4j", "testpassword")
)

try:
    driver.verify_connectivity()
    print("Connected to Neo4j")
finally:
    driver.close()

Step 4: Write Cypher queries

Cypher is the language you'll use with the driver. It reads like ASCII art of the pattern you want.

Step 5: Run queries from Python

Use driver.execute_query() for simple cases, or manage sessions for transactions.

Hands-on walkthrough

Let's build a small social network graph and query it, step by step.

Create nodes and relationships

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "testpassword"))

def create_graph(tx):
    tx.run("""
    CREATE (a:Person {name: 'Alice', age: 30}),
           (b:Person {name: 'Bob', age: 25}),
           (c:Person {name: 'Carol', age: 35}),
           (m:Movie {title: 'Inception', year: 2010}),
           (n:Movie {title: 'Matrix', year: 1999}),
           (a)-[:FRIENDS_WITH]->(b),
           (b)-[:FRIENDS_WITH]->(c),
           (a)-[:LIKES]->(m),
           (b)-[:LIKES]->(n),
           (c)-[:LIKES]->(n)
    """)

with driver.session() as session:
    session.execute_write(create_graph)
    print("Graph created")

driver.close()

Query: find friends of friends

Now use Neo4j for graph queries to find friends of friends of Alice:

from neo4j import GraphDatabase

driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "testpassword"))

def find_fof(tx, name):
    result = tx.run("""
    MATCH (p:Person {name: $name})-[:FRIENDS_WITH]->()-[:FRIENDS_WITH]->(fof)
    WHERE NOT (p)-[:FRIENDS_WITH]->(fof)
    RETURN DISTINCT fof.name AS name
    """, name=name)
    return [record["name"] for record in result]

with driver.session() as session:
    fof = session.execute_read(find_fof, "Alice")
    print("Friends of friends of Alice:", fof)

driver.close()

Expected output:

Friends of friends of Alice: ['Carol']

Query: recommendation based on shared likes

Let's recommend movies to Alice that her friends liked but she hasn't seen:

def recommend_movies(tx, name):
    result = tx.run("""
    MATCH (p:Person {name: $name})-[:FRIENDS_WITH]->(friend)-[:LIKES]->(movie)
    WHERE NOT (p)-[:LIKES]->(movie)
    RETURN movie.title AS title, collect(DISTINCT friend.name) AS recommended_by
    """, name=name)
    return [(record["title"], record["recommended_by"]) for record in result]

with driver.session() as session:
    recs = session.execute_read(recommend_movies, "Alice")
    print("Recommendations for Alice:")
    for title, friends in recs:
        print(f"- {title} (by {', '.join(friends)})")

Expected output:

Recommendations for Alice:
- Matrix (by Bob, Carol)

Query: shortest path

Find the shortest path between two nodes:

def shortest_path(tx, name1, name2):
    result = tx.run("""
    MATCH p = shortestPath(
        (a:Person {name: $name1})-[:FRIENDS_WITH*..5]-(b:Person {name: $name2})
    )
    RETURN [node in nodes(p) | node.name] AS path
    """, name1=name1, name2=name2)
    return [record["path"] for record in result]

with driver.session() as session:
    path = session.execute_read(shortest_path, "Alice", "Carol")
    print("Shortest path:", path[0] if path else "No path within 5 hops")

Expected output:

Shortest path: ['Alice', 'Bob', 'Carol']

Gotcha: avoid cartesian products

A common pitfall is forgetting to anchor a MATCH to a known node. If you write MATCH (p:Person), (m:Movie), you'll do a cross product — terrible for performance. Always start from a specific node or use indexes.

Compare options / when to choose what

You don't always need Neo4j. Here's a quick comparison to help you decide when to use Neo4j for graph queries vs. a relational database:

Feature / Use case Neo4j (graph) PostgreSQL (relational)
Modeling relationships First-class, elegant Requires JOIN tables + foreign keys
Multi-hop queries Expressive, fast Slow, complex JOINs or recursive CTEs
Variable-length paths Native with *..n patterns Painful with recursive CTEs
Performance on deep traversals Excellent (index-free adjacency) Degrades with depth
When to use Social networks, recommendation, fraud, knowledge graphs Standard CRUD, transactional data, aggregated reporting
Learning curve New query language (Cypher) needed Familiar SQL

Troubleshooting & edge cases

Connection refused

  • Check that the Docker container is running: docker ps.
  • Verify the port mapping is correct (7687 for Bolt).
  • If using AuraDB, use the full connection URI from the console.
  • Ensure the auth credentials match NEO4J_AUTH.

Cypher syntax errors

  • Use single quotes inside Cypher for string literals (e.g., 'Alice').
  • Use parameter substitution ($name) always to avoid injection and improve performance.
  • Double-check arrow directions in patterns — <-[:FRIENDS_WITH]- vs -[:FRIENDS_WITH]-> are not the same.

Performance: slow queries

  • Always create an index on properties you filter by: CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name).
  • Avoid unbounded variable-length patterns in production; use an upper bound like [:FRIENDS_WITH*..4].
  • Profile with EXPLAIN or PROFILE in the browser.

Driver session management

  • Always close the driver when done (or use a context manager) to free connections.
  • Use session.execute_read/execute_write for proper transaction handling; don't call run() on the driver directly.

Edge case: empty result

Sometimes no path exists. Always check for None or empty lists in your Python code:

if not path:
    print("No connection found.")

What you learned & what's next

You now understand the core idea behind use Neo4j for graph queries: you model data as nodes and relationships, and you use Cypher to traverse paths naturally. You've completed a practical exercise — building a social graph and querying it for friends-of-friends, recommendations, and shortest paths. You know how to connect Python to Neo4j, create data, and run parameterized queries safely.

You've achieved the learning objectives: - Explain the core idea behind graph queries in Neo4j. - Complete a practical exercise using Neo4j with Python.

What's next: In the next lesson, you'll move beyond local graphs and explore how to integrate graph data into AI pipelines — for example, using graph embeddings for retrieval-augmented generation. You'll learn how to feed Neo4j results directly into LLM prompts for richer, context-aware AI responses.

Pro tip: Keep practicing with the movie database built into Neo4j Browser (type :play movies). It's the perfect playground to experiment with more complex patterns before building your own.

Practice recap

Now it's your turn: create a graph of 3–5 of your favorite movies and actors, with ACTED_IN relationships. Write a Cypher query that finds all actors within two hops of a given actor and returns the movies they've been in. Run it from Python using the driver, and make sure to parameterize the actor name.

Common mistakes

  • Forgetting to create an index on filtered properties (like name) — leads to full scans and terrible performance.
  • Mixing execute_read and execute_write incorrectly — using session.run() directly bypasses the session's transaction safety.
  • Writing Cypher that returns all nodes instead of filtering early, causing cartesian products and memory bloat.
  • Neglecting to close the driver after use — leaks connections and stalls your script.
  • Using unbounded variable-length paths like [:FRIENDS_WITH*] in production — can traverse the entire graph and hang.

Variations

  1. Use the cypher package for async/await support in Python if you need concurrent queries.
  2. Use Neo4j AuraDB cloud service for a zero-maintenance managed graph database — great for production.
  3. Use the GDS (Graph Data Science) library inside Neo4j for advanced algorithms like PageRank or community detection, then query results with Cypher.

Real-world use cases

  • Build a social recommendation engine that finds 'people you may know' by traversing mutual friendships.
  • Detect fraud rings by querying shared accounts, devices, and IP addresses across a graph in real time.
  • Power a knowledge-graph–based chatbot that answers questions about entity relationships using shortest-path queries.

Key takeaways

  • Graph data is best modeled as nodes and relationships, with properties on both — not as foreign-key-joined tables.
  • Cypher lets you express multi-hop traversals and variable-length paths in a few lines, unlike SQL's recursive CTEs.
  • Always use parameters in Cypher for safety and query caching, and create indexes on frequently filtered properties.
  • Use execute_read and execute_write session methods for proper transaction management.
  • Use shortestPath and bounded path patterns to keep traversal performance predictable.
  • Choose a graph database only when your primary query pattern is relationship traversal — not for standard CRUD.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.