Neo4j stores data as nodes and relationships — the natural representation for social networks, supply chains, recommendation engines, and fraud detection. Cypher is the declarative query language: MATCH patterns in the graph, RETURN computed results. The Graph Data Science (GDS) library runs PageRank, Louvain community detection, and shortest-path algorithms at scale. Claude Code generates Cypher queries, schema/constraint definitions, GDS pipeline configurations, and the Python/Node.js driver code for production graph applications.
CLAUDE.md for Neo4j Projects
## Neo4j Stack
- Version: Neo4j 5.x Community or Enterprise
- Driver: neo4j (Python) or neo4j (Node.js) — official drivers
- GDS: Graph Data Science 2.x library (algorithms)
- APOC: APOC Procedures for extended utilities
- Connection: Bolt protocol (bolt://) with connection pooling
- Constraints: unique constraint on all node IDs (CREATE CONSTRAINT ... IS UNIQUE)
- Indexes: composite indexes for common query patterns
- Schema: enforce node labels + mandatory properties via constraints
Schema and Constraints
-- Node constraints (run once at startup)
CREATE CONSTRAINT user_id IF NOT EXISTS
FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE CONSTRAINT product_id IF NOT EXISTS
FOR (p:Product) REQUIRE p.id IS UNIQUE;
CREATE CONSTRAINT order_id IF NOT EXISTS
FOR (o:Order) REQUIRE o.id IS UNIQUE;
-- Composite indexes for common queries
CREATE INDEX user_email IF NOT EXISTS
FOR (u:User) ON (u.email);
CREATE INDEX product_category IF NOT EXISTS
FOR (p:Product) ON (p.category, p.price);
-- Node property types (Neo4j 5+)
CREATE CONSTRAINT user_email_type IF NOT EXISTS
FOR (u:User) REQUIRE u.email IS :: STRING;
Node and Relationship Creation
-- Create user
CREATE (u:User {
id: $userId,
email: $email,
name: $name,
createdAt: datetime(),
tier: 'standard'
})
RETURN u;
-- Create product
CREATE (p:Product {
id: $productId,
name: $name,
category: $category,
price: $price,
tags: $tags -- list property
})
RETURN p;
-- Create order with relationships in one query
MATCH (u:User {id: $userId})
CREATE (o:Order {
id: $orderId,
totalCents: $totalCents,
status: 'pending',
createdAt: datetime()
})
CREATE (u)-[:PLACED_ORDER {at: datetime()}]->(o)
-- 3. Add items
WITH o
UNWIND $items AS item
MATCH (p:Product {id: item.productId})
CREATE (o)-[:CONTAINS {
quantity: item.quantity,
priceCents: item.priceCents
}]->(p)
RETURN o;
-- Merge pattern — create or match existing node
MERGE (u:User {email: $email})
ON CREATE SET
u.id = randomUUID(),
u.name = $name,
u.createdAt = datetime()
ON MATCH SET
u.lastSeen = datetime()
RETURN u;
Relationship Traversal
-- Find all products a user has ordered (1-hop via order)
MATCH (u:User {id: $userId})-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p:Product)
WHERE o.status <> 'cancelled'
RETURN DISTINCT p.id, p.name, p.category, COUNT(o) AS orderCount
ORDER BY orderCount DESC;
-- Find users who bought similar products (co-purchase graph)
MATCH (u1:User {id: $userId})-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p:Product)
MATCH (p)<-[:CONTAINS]-(o2:Order)<-[:PLACED_ORDER]-(u2:User)
WHERE u2 <> u1
WITH u2, COUNT(DISTINCT p) AS sharedProducts
WHERE sharedProducts >= 3
RETURN u2.id, u2.name, sharedProducts
ORDER BY sharedProducts DESC
LIMIT 10;
-- Variable-length path: find products connected through shared buyers
-- (customers who bought A also bought B)
MATCH path = (p1:Product {id: $productId})<-[:CONTAINS]-(:Order)<-[:PLACED_ORDER]-(u:User)
-[:PLACED_ORDER]->(:Order)-[:CONTAINS]->(p2:Product)
WHERE p2 <> p1
AND NOT EXISTS {
MATCH (u)-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p1)
WHERE (u)-[:PLACED_ORDER]->(:Order {id: o.id})
}
RETURN p2.id, p2.name, COUNT(DISTINCT u) AS cobuyers
ORDER BY cobuyers DESC
LIMIT 20;
-- Shortest path between two users through shared orders
MATCH (u1:User {id: $userId1}), (u2:User {id: $userId2})
MATCH p = shortestPath((u1)-[*..6]-(u2))
RETURN p, length(p) AS distance;
Recommendation Engine
-- Collaborative filtering: recommend products
-- Based on users with similar purchase history
MATCH (targetUser:User {id: $userId})-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p:Product)
WITH targetUser, COLLECT(DISTINCT p) AS targetProducts
-- Find similar users
MATCH (similar:User)-[:PLACED_ORDER]->(so:Order)-[:CONTAINS]->(sp:Product)
WHERE similar <> targetUser
AND sp IN targetProducts
WITH targetUser, targetProducts, similar, COUNT(DISTINCT sp) AS overlap
WHERE overlap >= 2
ORDER BY overlap DESC
LIMIT 20
-- Get products similar users bought that target hasn't
MATCH (similar)-[:PLACED_ORDER]->(so:Order)-[:CONTAINS]->(rec:Product)
WHERE NOT rec IN targetProducts
AND rec.category IN ['electronics', 'accessories'] -- Category filter
WITH rec, SUM(overlap) AS score, COUNT(DISTINCT similar) AS supporters
ORDER BY score DESC
LIMIT 10
RETURN rec.id, rec.name, rec.price, score, supporters;
-- Content-based: recommend by category + tag similarity
MATCH (u:User {id: $userId})-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p:Product)
WITH u, COLLECT(DISTINCT p.category) AS categories, COLLECT(DISTINCT p.tags) AS allTags
MATCH (candidate:Product)
WHERE candidate.category IN categories
AND NOT EXISTS {
MATCH (u)-[:PLACED_ORDER]->(:Order)-[:CONTAINS]->(candidate)
}
WITH candidate, SIZE([t IN candidate.tags WHERE t IN REDUCE(acc = [], x IN allTags | acc + x)]) AS tagOverlap
WHERE tagOverlap > 0
RETURN candidate.id, candidate.name, candidate.category, tagOverlap
ORDER BY tagOverlap DESC
LIMIT 10;
Fraud Detection Patterns
-- Find users sharing payment method (potential fraud ring)
MATCH (u1:User)-[:USED_PAYMENT]->(pm:PaymentMethod)<-[:USED_PAYMENT]-(u2:User)
WHERE u1 <> u2
AND pm.type = 'credit_card'
WITH pm, COLLECT(DISTINCT u1) + COLLECT(DISTINCT u2) AS users
WHERE SIZE(users) >= 3
MATCH (pm)<-[:USED_PAYMENT]-(u:User)-[:PLACED_ORDER]->(o:Order)
WHERE o.createdAt > datetime() - duration('P30D')
WITH pm, COLLECT(DISTINCT u) AS fraudRingUsers, SUM(o.totalCents) AS totalSpend
WHERE SIZE(fraudRingUsers) >= 3
RETURN
pm.last4 AS cardLast4,
SIZE(fraudRingUsers) AS usersSharing,
totalSpend / 100.0 AS totalSpendDollars,
[u IN fraudRingUsers | u.email] AS userEmails
ORDER BY totalSpend DESC;
-- Velocity check: same device, multiple accounts
MATCH (d:Device {id: $deviceId})<-[:USED_DEVICE]-(u:User)
MATCH (u)-[:PLACED_ORDER]->(o:Order)
WHERE o.createdAt > datetime() - duration('PT1H') -- Last hour
WITH d, COUNT(DISTINCT u) AS accountCount, SUM(o.totalCents) AS hourlySpend
WHERE accountCount > 2 OR hourlySpend > 100000 -- > $1000 in 1 hour
RETURN d.id, accountCount, hourlySpend / 100.0 AS hourlySpendDollars;
GDS Graph Algorithms
-- Create an in-memory graph projection for algorithms
CALL gds.graph.project(
'user-product-graph',
['User', 'Product'],
{
PURCHASED: {
type: 'CONTAINS',
orientation: 'UNDIRECTED',
properties: 'quantity'
}
}
)
-- Run PageRank to find influential products
CALL gds.pageRank.stream('user-product-graph', {
maxIterations: 20,
dampingFactor: 0.85
})
YIELD nodeId, score
WITH gds.util.asNode(nodeId) AS node, score
WHERE node:Product
RETURN node.name AS product, score
ORDER BY score DESC
LIMIT 10;
-- Louvain community detection — find customer segments
CALL gds.louvain.stream('user-product-graph')
YIELD nodeId, communityId
WITH gds.util.asNode(nodeId) AS node, communityId
WHERE node:User
RETURN communityId, COUNT(*) AS members, COLLECT(node.id)[..5] AS sampleUsers
ORDER BY members DESC;
-- Drop the projection when done
CALL gds.graph.drop('user-product-graph');
Python Driver Integration
# lib/graph_db.py — Neo4j connection with session management
from neo4j import AsyncGraphDatabase, AsyncSession
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator
class GraphDB:
def __init__(self):
self.driver = AsyncGraphDatabase.driver(
os.environ["NEO4J_URI"],
auth=(os.environ["NEO4J_USER"], os.environ["NEO4J_PASSWORD"]),
max_connection_pool_size=50,
)
async def close(self):
await self.driver.close()
@asynccontextmanager
async def session(self, database: str = "neo4j") -> AsyncIterator[AsyncSession]:
async with self.driver.session(database=database) as session:
yield session
async def get_recommendations(self, user_id: str, limit: int = 10) -> list[dict]:
"""Get product recommendations using collaborative filtering."""
async with self.session() as session:
result = await session.run("""
MATCH (u:User {id: $userId})-[:PLACED_ORDER]->(o:Order)-[:CONTAINS]->(p:Product)
WITH u, COLLECT(DISTINCT p) AS purchased
MATCH (similar:User)-[:PLACED_ORDER]->(:Order)-[:CONTAINS]->(sp:Product)
WHERE similar <> u AND sp IN purchased
WITH u, purchased, similar, COUNT(DISTINCT sp) AS overlap
ORDER BY overlap DESC LIMIT 20
MATCH (similar)-[:PLACED_ORDER]->(:Order)-[:CONTAINS]->(rec:Product)
WHERE NOT rec IN purchased
WITH rec, SUM(overlap) AS score
RETURN rec.id AS id, rec.name AS name, rec.price AS price, score
ORDER BY score DESC LIMIT $limit
""", userId=user_id, limit=limit)
return [dict(r) async for r in result]
async def create_order_graph(self, order_data: dict) -> None:
"""Create order with all relationships atomically."""
async with self.session() as session:
async with await session.begin_transaction() as tx:
await tx.run("""
MATCH (u:User {id: $userId})
CREATE (o:Order {id: $orderId, totalCents: $totalCents, status: 'pending', createdAt: datetime()})
CREATE (u)-[:PLACED_ORDER]->(o)
""", **order_data)
for item in order_data["items"]:
await tx.run("""
MATCH (o:Order {id: $orderId}), (p:Product {id: $productId})
CREATE (o)-[:CONTAINS {quantity: $quantity, priceCents: $priceCents}]->(p)
""", orderId=order_data["orderId"], **item)
await tx.commit()
db = GraphDB()
For the PostgreSQL relational database that often complements Neo4j for transactional data, see the PostgreSQL performance guide for index strategies. For the vector database patterns used alongside graph databases for semantic search, the vector databases guide covers embedding-based similarity search. The Claude Skills 360 bundle includes Neo4j skill sets covering Cypher queries, graph algorithms, and recommendation patterns. Start with the free tier to try graph database schema generation.