DS DevShelfHub Projects · AI tools
Cheatsheets / Neo4j
Cheatsheet · Dev tooling

Neo4j: Cypher, APOC and GDS Algorithms Reference Guide

By DevShelfHub

Cypher, MATCH/MERGE, node and relationship patterns, indexes, constraints, APOC, vector search (Neo4j 5.13+), and the Python driver — Neo4j 5 graph database reference for connected-data applications and GraphRAG pipelines.

94 items 9 min Cypher Graphs APOC

Start hereQuick start · 6 you’ll reach for daily

Connectcypher-shell -a bolt://host -u neo4j
CreateCREATE (u:User { name: 'Ada' })
MatchMATCH (u:User) RETURN u
UpsertMERGE (u:User { email: $e })
Relate(a)-[:FOLLOWS]->(b)
ProfilePROFILE MATCH ... RETURN ...

Target versions · paceVersions

Targets: neo4j ≥ 5.20 Cypher (Neo4j 5.x dialect) neo4j-python ≥ 5.20

Neo4j 5.x added relationship-property indexes, point + text indexes, an EXISTS sub-clause for patterns, and the consolidated CREATE INDEX / CREATE CONSTRAINT FOR ... REQUIRE grammar. APOC and GDS ship as official plugins. Neo4j 5 LTS is the safe target for production through 2027.

Docker · browser · AuraSetup

bash
# Docker — single-node Neo4j 5 with APOC + GDS bundled
docker run -d --name neo4j \
    -p 7474:7474 -p 7687:7687 \
    -e NEO4J_AUTH=neo4j/devpassword \
    -e NEO4J_PLUGINS='["apoc","graph-data-science"]' \
    -e NEO4J_dbms_security_procedures_unrestricted='apoc.*,gds.*' \
    neo4j:5

# Neo4j Browser — visual query UI
open http://localhost:7474

# Shell — bundled in the image
docker exec -it neo4j cypher-shell -a bolt://localhost:7687 -u neo4j -p devpassword

# Aura / hosted — connect string includes scheme + auth
cypher-shell -a neo4j+s://xxxxx.databases.neo4j.io -u neo4j -p ****

# Python driver
pip install "neo4j>=5.20"

python - <<'PY'
from neo4j import GraphDatabase
with GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "devpassword")) as drv:
    drv.verify_connectivity()
    with drv.session() as s:
        print(s.run("CALL dbms.components() YIELD name, versions").data())
PY

MATCH · MERGE · DELETECypher essentials

MATCH (n) RETURN n LIMIT 25Read pattern. Always cap exploratory queries.
MATCH (u:User { name: 'Ada' }) RETURN uInline property predicate — equivalent to WHERE.
CREATE (u:User { name: 'Ada', age: 30 })Insert a labelled node.
CREATE (a)-[:FOLLOWS { since: date() }]->(b)Insert a typed, directional relationship.
MERGE (u:User { email: $e }) ON CREATE SET u.created = datetime() ON MATCH SET u.seen = datetime()Preferred Idempotent upsert with conditional sets.
SET n.age = 31, n.tags = ['x','y']Mutate properties.
REMOVE n.tags · REMOVE n:TagDrop a property / a label.
DELETE nDelete a node. Errors if any relationships remain.
DETACH DELETE nDelete the node and all its relationships.
RETURN n.name AS name, n.age AS ageProjection — ALWAYS alias columns you reference downstream.
EXPLAIN MATCH ... RETURN ...Plan only. Free — doesn’t touch the data.
PROFILE MATCH ... RETURN ...Plan + actual rows / db hits per op.

Labels · types · directionNodes & relationships

(n:Label)Node with one label.
(n:Person:Author)Multi-label node.
(n:User { name: 'Ada' })Inline property predicate.
[r:KNOWS]Relationship of one type. Types are UPPER_CASE.
(a)-[r:KNOWS]->(b)Directed (left to right).
(a)-[r:KNOWS]-(b)Undirected — either direction matches.
(a)--(b)Anonymous relationship.
(a)-[r:KNOWS|FRIEND]->(b)Union of relationship types.
(a)-[r:KNOWS]->(b) WHERE r.weight > 0.5Predicates on relationship properties.
id(n) · elementId(n)Internal identifiers. elementId is the modern, stable form.

Variable length · shortest pathPatterns

(a)-[*1..3]->(b)Variable length: 1 to 3 hops.
(a)-[*..5]->(b)Up to 5 hops; lower bound defaults to 1.
(a)-[*0..1]->(b)Optional — matches at-self or 1 hop.
shortestPath((a)-[*..6]-(b))Shortest path with a depth cap. Always cap!
allShortestPaths((a)-[*..6]-(b))All ties at the same shortest length.
MATCH p = (a)-[*..3]->(b) RETURN nodes(p), relationships(p), length(p)Capture the path; introspect it.
OPTIONAL MATCH (n)-[:HAS]->(m)Left-join in graph form — nullable.
(:User)-[:LIKES]->()<-[:LIKES]-(:User)Co-occurrence pattern — building block for recommendations.
sql
// Variable-length paths + shortest path + property aggregation along the path.

// 1. People reachable in 1–3 hops via FOLLOWS
MATCH path = (me:User {name: 'Ada'})-[:FOLLOWS*1..3]->(other:User)
WHERE other <> me
RETURN other.name AS name, length(path) AS hops
ORDER BY hops, name
LIMIT 25;

// 2. Shortest path (any relationship, ≤ 6 hops) between two users
MATCH (a:User {name: 'Ada'}), (b:User {name: 'Ben'})
MATCH p = shortestPath((a)-[*..6]-(b))
RETURN length(p) AS hops, [n IN nodes(p) | n.name] AS chain;

// 3. Aggregate weights along the path
MATCH p = (a:User {name: 'Ada'})-[r:FOLLOWS*]->(b:User {name: 'Ben'})
WITH p, reduce(s = 0.0, rel IN r | s + rel.weight) AS total
RETURN length(p) AS hops, total ORDER BY total DESC LIMIT 3;

// 4. OPTIONAL MATCH — left-join in graph form
MATCH (u:User {name: 'Ada'})
OPTIONAL MATCH (u)-[:WROTE]->(p:Post)
RETURN u.name, count(p) AS posts;

Predicates · regex · pattern existenceFiltering & WHERE

WHERE n.age > 18 AND n.country IN ['US','EU']Standard predicates.
WHERE n.name STARTS WITH 'A'Prefix match. Hits a TEXT index.
WHERE n.email ENDS WITH '@x.io'Suffix match.
WHERE n.title CONTAINS 'redis'Substring. TEXT index speeds it up.
WHERE n.name =~ '(?i)^ada.*'Regex match. Use full-text indexes for big corpora instead.
WHERE n.deleted IS NULLNull check.
WHERE EXISTS { MATCH (n)-[:OWNS]->(:Post) }Pattern existence sub-clause.
WHERE NOT (n)-[:BLOCKED]->()Anti-pattern — row is dropped if the pattern exists.
WHERE any(t IN n.tags WHERE t =~ 'a.*')Predicate over a list property.

Pipeline stagesWITH & UNWIND

WITH n, count(*) AS c WHERE c > 5 RETURN nAggregate then filter (Cypher’s HAVING).
WITH n ORDER BY n.created DESC LIMIT 10 MATCH (n)-[:WROTE]->(p) RETURN pSlice a result mid-pipeline.
WITH DISTINCT n RETURN nDe-dup rows.
WITH collect(n) AS users UNWIND users AS u RETURN uCollect then expand — common ordering trick.
UNWIND [1, 2, 3] AS x RETURN xExpand a literal list to rows.
UNWIND $rows AS row MERGE (:User { id: row.id }) SET ...Preferred Batch upsert from a parameter list.

count · collect · percentilesAggregations

count(n) · count(DISTINCT n)Cardinality. count(*) counts rows; count(n) ignores nulls.
sum(x) · avg(x) · min(x) · max(x)Standard numeric aggs.
collect(n.name)Aggregate into a list.
collect(DISTINCT n.name)Distinct list.
percentileDisc(latency, 0.95)p95 over a numeric column.
stDev(x) · stDevP(x)Sample / population standard deviation.

RANGE · TEXT · POINT · FULLTEXTIndexes & constraints

CREATE INDEX user_email FOR (n:User) ON (n.email)RANGE index — equality + range queries.
CREATE TEXT INDEX user_name FOR (n:User) ON (n.name)Optimised for CONTAINS / STARTS WITH.
CREATE FULLTEXT INDEX posts_text FOR (n:Post|Comment) ON EACH [n.title, n.body]Full-text. Query via db.index.fulltext.queryNodes.
CREATE POINT INDEX places_geom FOR (n:Place) ON (n.location)Spatial. point.distance & range filters.
CREATE INDEX follows_since FOR ()-[r:FOLLOWS]-() ON (r.since)Relationship-property index (5.x).
CREATE CONSTRAINT user_email_unique FOR (u:User) REQUIRE u.email IS UNIQUEUNIQUE constraint — auto-builds a backing index.
CREATE CONSTRAINT user_name_not_null FOR (u:User) REQUIRE u.name IS NOT NULLNOT NULL (Enterprise).
CREATE CONSTRAINT user_key FOR (u:User) REQUIRE (u.tenant, u.email) IS NODE KEYComposite key (Enterprise).
SHOW INDEXES · SHOW CONSTRAINTSInspect everything.
CALL db.awaitIndexes()Block until pending indexes are ONLINE.
sql
// Constraints come first — they automatically create the backing index.
CREATE CONSTRAINT user_email_unique IF NOT EXISTS
FOR (u:User) REQUIRE u.email IS UNIQUE;

CREATE CONSTRAINT post_id_unique IF NOT EXISTS
FOR (p:Post) REQUIRE p.id IS UNIQUE;

// Composite NODE KEY (Enterprise) — multi-property unique + NOT NULL.
CREATE CONSTRAINT user_tenant_email IF NOT EXISTS
FOR (u:User) REQUIRE (u.tenant, u.email) IS NODE KEY;

// Range index on a non-unique property — speeds up equality + range queries.
CREATE INDEX user_created IF NOT EXISTS
FOR (u:User) ON (u.created);

// TEXT index — picks the right operator class for CONTAINS / STARTS WITH / ENDS WITH.
CREATE TEXT INDEX user_name IF NOT EXISTS
FOR (u:User) ON (u.name);

// Full-text index across multiple properties on multiple labels.
CREATE FULLTEXT INDEX posts_text IF NOT EXISTS
FOR (n:Post|Comment) ON EACH [n.title, n.body];

// Relationship-property index (5.x).
CREATE INDEX follows_since IF NOT EXISTS
FOR ()-[r:FOLLOWS]-() ON (r.since);

// Block until everything is ONLINE — safe to call from app boot.
CALL db.awaitIndexes();

// Inspect what you've got.
SHOW INDEXES;
SHOW CONSTRAINTS;

Extended proceduresAPOC

CALL apoc.help('text')Search APOC catalog by keyword.
CALL apoc.load.json('https://...') YIELD valueStream JSON rows from a URL or file.
CALL apoc.load.csv('file:///rows.csv', { header: true })Stream CSV rows. Place files under the import/ dir.
CALL apoc.periodic.iterate("MATCH (n:User) RETURN n", "SET n.flag = true", { batchSize: 5000 })Preferred Chunked writes with progress reporting.
CALL apoc.export.cypher.all('dump.cypher', { format: 'plain' })Schema + data export to a replay-able Cypher script.
CALL apoc.meta.schema()Auto-discovered schema — great for new codebases.
CALL apoc.refactor.mergeNodes(nodes, { properties: 'discard' })Programmatic node merging — preserves relationships.

Graph Data ScienceGraph algorithms

CALL gds.graph.project('g', ['User','Post'], { LIKES: { type:'LIKES', orientation:'NATURAL' } })Project nodes + rels into an in-memory graph.
CALL gds.pageRank.stream('g', { maxIterations: 20 }) YIELD nodeId, scoreStream PageRank scores.
CALL gds.pageRank.write('g', { writeProperty: 'pagerank' })Persist scores back onto the nodes.
CALL gds.louvain.stream('g')Community detection.
CALL gds.nodeSimilarity.stream('g', { topK: 10 })KNN over the graph — foundation for similarity recs.
CALL gds.graph.list() · gds.graph.drop('g')Inspect / clean up projected graphs.

neo4j-pythonPython driver

from neo4j import GraphDatabaseSync client; AsyncGraphDatabase for asyncio.
driver = GraphDatabase.driver("bolt://host", auth=("user","****"))One driver per process. Reuse it.
driver.verify_connectivity()Smoke-test on boot.
with driver.session(database="neo4j") as s: ...Sessions are cheap; create them per request.
s.run("MATCH (u:User {id:$id}) RETURN u", id=42).single()Run + single record. Use named parameters — not f-strings.
s.execute_read(work_fn)Preferred Managed read txn — auto-retry on transient errors.
s.execute_write(work_fn)Same for writes.
for record in tx.run("..."): handle(record)Stream a large result. Records are consumed lazily.
python
from neo4j import GraphDatabase

URI  = "neo4j+s://xxxxx.databases.neo4j.io"
AUTH = ("neo4j", "****")

# One driver per process; sessions are cheap, drivers are not.
driver = GraphDatabase.driver(URI, auth=AUTH)

# Unit-of-work functions get automatic retries on transient errors
# (DEADLOCK_DETECTED, LeaderSwitch, NotALeader, etc.).
def create_follow(tx, follower_id: str, followee_id: str):
    tx.run(
        """
        MATCH (a:User {id: $a}), (b:User {id: $b})
        MERGE (a)-[r:FOLLOWS]->(b)
        ON CREATE SET r.since = datetime()
        """,
        a=follower_id, b=followee_id,
    )

def fanout(tx, user_id: str):
    result = tx.run(
        "MATCH (u:User {id: $id})-[:FOLLOWS]->(f) RETURN f.id AS id, f.name AS name",
        id=user_id,
    )
    return [{"id": r["id"], "name": r["name"]} for r in result]   # consume in-txn

with driver.session(database="neo4j") as s:
    s.execute_write(create_follow, "u1", "u2")
    rows = s.execute_read(fanout, "u1")
    print(rows)

driver.close()

PROFILE · plannerPerformance

EXPLAIN MATCH ... RETURN ...Plan only. Free.
PROFILE MATCH ... RETURN ...Plan + per-op rows / db hits / cache hits.
Watch for "Eager" in PROFILEUsually means a write-read-write conflict — rewrite with WITH boundaries.
USING INDEX n:User(email)Hint the planner to use a specific index.
CALL apoc.periodic.iterate(...)For big writes — replaces the old USING PERIODIC COMMIT.
SHOW TRANSACTIONS · TERMINATE TRANSACTION 'tx-id'Find and kill long-running queries.

Full pipeline · ~25 linesEnd-to-end · Friend-of-friend recs

Defines constraints, seeds Users, Posts, FOLLOWS + LIKES relationships, then runs a 2-hop recommendation: posts liked by friends-of-friends, ordered by how many friends overlap, with self-likes excluded.

sql
// End-to-end: friend-of-friend recommendations.
// "Show me posts liked by people my friends like — that I haven't liked yet."

// 1. Schema
CREATE CONSTRAINT user_id_unique IF NOT EXISTS
FOR (u:User) REQUIRE u.id IS UNIQUE;
CREATE CONSTRAINT post_id_unique IF NOT EXISTS
FOR (p:Post) REQUIRE p.id IS UNIQUE;

// 2. Seed a tiny graph
UNWIND [
    {id:'u1', name:'Ada'}, {id:'u2', name:'Ben'},
    {id:'u3', name:'Cory'}, {id:'u4', name:'Dru'}
] AS u MERGE (:User {id:u.id}) SET u.name = u.name;

UNWIND [
    {id:'p1', title:'Vector search 101'},
    {id:'p2', title:'TWCS tuning'},
    {id:'p3', title:'Graph algos'}
] AS p MERGE (:Post {id:p.id}) SET p.title = p.title;

MATCH (a:User {id:'u1'}), (b:User {id:'u2'}) MERGE (a)-[:FOLLOWS]->(b);
MATCH (b:User {id:'u2'}), (c:User {id:'u3'}) MERGE (b)-[:FOLLOWS]->(c);
MATCH (u:User), (p:Post)
WHERE (u.id='u2' AND p.id='p2') OR (u.id='u3' AND p.id='p3')
MERGE (u)-[:LIKES]->(p);

// 3. Recommendation query — 2-hop, exclude self-likes
MATCH (me:User {id:'u1'})-[:FOLLOWS*1..2]->(friend:User)-[:LIKES]->(p:Post)
WHERE NOT (me)-[:LIKES]->(p)
RETURN p.id AS id, p.title AS title, count(DISTINCT friend) AS score
ORDER BY score DESC LIMIT 10;

Best practiceGood to know

Always cap variable-length paths. [*] with no upper bound is a graph version of SELECT * with no LIMIT — the planner happily traverses the entire connected component. [*..6] is a sane default.
Constraints are the indexes you actually want. A UNIQUE constraint creates the backing index for free, and MERGE on a constrained property is the only safe upsert. Always create the constraint before the first write that depends on it.
Use apoc.periodic.iterate for big writes. It chunks, reports progress, and rolls back per-batch. Without it, a million-row mutation can OOM the JVM before commit.

Common trapsWatch out for

Cartesian product in MATCH is silent. Multiple disconnected patterns in one MATCH (e.g. MATCH (a:User), (b:User)) join every a with every b. The planner only warns — it doesn’t stop you. Add a relationship or split into two MATCH clauses.
String concatenation is not parameterisation. f"MATCH (n:User {{ id: '{id}' }})" is the Cypher version of SQL injection. Use named parameters ($id) — the driver handles escaping and the planner caches the prepared query.
Don’t MERGE on a non-indexed property. MERGE without a constraint scans the label and serialises writes — throughput collapses as the graph grows. Always create the UNIQUE constraint first.

Go deeperSee also

Neo4j FAQ

What is Neo4j used for?

Neo4j is a native property graph database used for fraud detection, recommendation engines, identity and access management, knowledge graphs, and network topology analysis. Its Cypher query language makes it easier to express and traverse relationships than SQL joins across many tables.

What is Cypher in Neo4j?

Cypher is the declarative query language for Neo4j. It describes graph patterns using ASCII-art notation — (node)-[:RELATIONSHIP]->(node) — and supports MATCH, MERGE, CREATE, SET, DELETE, WITH, UNWIND, and aggregation. Cypher is the official GQL standard query language.

What is APOC in Neo4j?

APOC (Awesome Procedures On Cypher) is the official Neo4j plugin library providing over 450 procedures for data import/export, text processing, graph algorithms, schema introspection, and periodic batched commits. Install it from the Neo4j Plugin Manager or via the APOC plugin env var.

What is Neo4j GDS?

The Graph Data Science (GDS) library provides graph algorithm implementations — PageRank, community detection (Louvain, Label Propagation), shortest paths (Dijkstra, A*), link prediction, and node embeddings (FastRP, GraphSAGE). Algorithms project a named graph and run in-memory for speed.

Is Neo4j free to use?

Neo4j Community Edition is free and open source under the GPL license. Enterprise Edition adds clustering, hot backups, and advanced security under a commercial license. AuraDB is the cloud-managed service with a free tier for small graphs.

How do I use Neo4j with Python?

Install the official driver (pip install neo4j) and connect with driver = GraphDatabase.driver(uri, auth=(user, password)). Run Cypher with driver.session().run("MATCH (n:Person {name: $name}) RETURN n", name="Alice"). For vector search (GraphRAG), use Neo4jVector.from_existing_index() from langchain-community or neo4j-graphrag.