Use Neo4j for Graph Queries
Use Neo4j for graph queries — Applied AI engineering.
Focus: use neo4j for graph queries
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 (
7687for 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
EXPLAINorPROFILEin the browser.
Driver session management
- Always close the driver when done (or use a context manager) to free connections.
- Use
session.execute_read/execute_writefor proper transaction handling; don't callrun()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
moviedatabase 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_readandexecute_writeincorrectly — usingsession.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
- Use the
cypherpackage for async/await support in Python if you need concurrent queries. - Use Neo4j AuraDB cloud service for a zero-maintenance managed graph database — great for production.
- 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_readandexecute_writesession 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.