Cypher Reference
Purple8 Graph implements a subset of the openCypher specification. The engine covers ~80% of everyday Cypher use cases. The sections below document exactly what is and is not supported.
Supported clauses
| Clause | Since | Notes |
|---|---|---|
CREATE | v0.1 | Node and relationship creation |
MATCH | v0.1 | Pattern matching with optional predicates |
OPTIONAL MATCH | v0.5 | Returns null for non-matching patterns |
MERGE | v0.4 | Create if not exists |
SET | v0.1 | Update node/edge properties |
DELETE | v0.2 | Remove nodes and edges |
DETACH DELETE | v0.3 | Delete node + all its relationships |
RETURN | v0.1 | Project results |
WITH | v0.5 | Pipeline between query steps |
WHERE | v0.1 | Filter predicates |
ORDER BY | v0.3 | Sort results |
LIMIT / SKIP | v0.3 | Pagination |
UNWIND | v0.6 | Expand a list into rows |
CALL | v0.8 | Procedure calls (see below) |
Pattern syntax
cypher
-- Node pattern
MATCH (n)
MATCH (n:Person)
MATCH (n:Person {name: "Alice"})
-- Relationship pattern
MATCH (a)-[:KNOWS]->(b)
MATCH (a)-[r:KNOWS {since: 2020}]->(b)
-- Undirected
MATCH (a)-[:KNOWS]-(b)
-- Variable-length path (v0.22+)
MATCH (a)-[:KNOWS*1..4]->(b)
-- Any relationship type
MATCH (a)-[r]->(b)WHERE predicates
cypher
-- Comparison
WHERE n.age > 30
WHERE n.name = "Alice"
WHERE n.name <> "Bob"
-- Null checks
WHERE n.email IS NOT NULL
WHERE n.deleted_at IS NULL
-- String operators
WHERE n.name STARTS WITH "Al"
WHERE n.email ENDS WITH "@acme.com"
WHERE n.bio CONTAINS "engineer"
-- Boolean operators
WHERE n.age > 30 AND n.active = true
WHERE n.role = "admin" OR n.role = "owner"
WHERE NOT n.deleted = true
-- IN list
WHERE n.status IN ["active", "pending"]
-- Range (shorthand)
WHERE 18 <= n.age <= 65Aggregation functions
cypher
MATCH (n:Document)
RETURN
COUNT(n) AS total,
COUNT(DISTINCT n.author) AS unique_authors,
AVG(n.word_count) AS avg_words,
SUM(n.word_count) AS total_words,
MIN(n.published_at) AS earliest,
MAX(n.published_at) AS latest,
COLLECT(n.title) AS titlesWITH — multi-step pipelines
cypher
MATCH (p:Person)-[:AUTHORED]->(d:Document)
WHERE d.published_at > "2024-01-01"
WITH p, COUNT(d) AS doc_count
WHERE doc_count > 5
RETURN p.name, doc_count
ORDER BY doc_count DESC
LIMIT 10MERGE — upsert pattern
cypher
-- Create the node if it doesn't exist, match if it does
MERGE (p:Person {email: "alice@acme.com"})
ON CREATE SET p.created_at = timestamp()
ON MATCH SET p.last_seen = timestamp()
RETURN pParameters
Always use parameters for user-supplied values — they prevent injection and enable query plan caching:
python
engine.query(
"MATCH (p:Person {email: $email}) RETURN p",
email="alice@acme.com",
)cypher
-- In raw Cypher, parameters are referenced with $name
MATCH (p:Person {email: $email})
WHERE p.age > $min_age
RETURN p.name, p.ageCALL — built-in procedures
db.vector.search
cypher
CALL db.vector.search('Label', $embedding, k)
YIELD node, score
-- Example: top-5 documents similar to a query vector
CALL db.vector.search('Document', $vec, 5)
YIELD node AS doc, score
RETURN doc.title, doc.author, score
ORDER BY score DESC| Argument | Type | Description |
|---|---|---|
Label | STRING | Node label to search within |
$embedding | LIST<FLOAT> | Query vector (must match index dimensionality) |
k | INTEGER | Number of nearest neighbours to return |
With filters:
cypher
CALL db.vector.search('Document', $vec, 20)
YIELD node AS doc, score
WHERE doc.status = "published" AND doc.year >= 2023
RETURN doc.title, score
ORDER BY score DESC
LIMIT 5db.bm25.search
cypher
CALL db.bm25.search('Document', 'transformer attention mechanism', 10)
YIELD node AS doc, score
RETURN doc.title, score
ORDER BY score DESCdb.hybrid.search
cypher
CALL db.hybrid.search('Document', $vec, 'transformer attention', 10, {alpha: 0.6})
YIELD node AS doc, score
MATCH (doc)-[:AUTHORED_BY]->(author:Person)
RETURN doc.title, author.name, score
ORDER BY score DESCalpha controls the vector/BM25 blend — 1.0 = pure vector, 0.0 = pure BM25.
Named embedding slots
When a node has multiple vector properties (e.g., title_vec + body_vec), specify the slot name:
cypher
CALL db.vector.search('Document', $vec, 10, {slot: 'body_vec'})
YIELD node AS doc, score
RETURN doc.title, scoreFunctions
| Function | Description |
|---|---|
id(n) | Internal node ID |
labels(n) | List of node labels |
type(r) | Relationship type string |
properties(n) | All properties as a map |
keys(n) | List of property keys |
size(list) | Length of a list |
toString(x) | Cast to string |
toInteger(x) | Cast to integer |
toFloat(x) | Cast to float |
toBoolean(x) | Cast to boolean |
toLower(s) | Lowercase string |
toUpper(s) | Uppercase string |
trim(s) | Strip whitespace |
split(s, delim) | Split string to list |
substring(s, start, len) | Extract substring |
timestamp() | Current UNIX epoch (ms) |
date() | Current date string (YYYY-MM-DD) |
coalesce(a, b, ...) | First non-null value |
head(list) | First element |
tail(list) | All but first element |
last(list) | Last element |
range(start, end) | Integer range list |
Not supported
The following openCypher features are not implemented in the current release:
| Feature | Workaround |
|---|---|
FOREACH | UNWIND + WITH |
List comprehensions [x IN list WHERE ...] | UNWIND + WHERE + COLLECT |
Pattern comprehensions [(a)-->(b) | b.name] | MATCH + COLLECT |
CALL { ... } (subquery) | Split into WITH steps |
CALL { ... } IN TRANSACTIONS | Use engine.batch_query() |
EXISTS { ... } subquery | OPTIONAL MATCH + null check |
| APOC procedures | No APOC support — use Python SDK |
shortestPath() / allShortestPaths() | Planned for v0.26 |
| Full-text index synonyms | Use BM25 + custom tokenizer |
Cypher vs SQL at a glance
cypher
-- SQL: SELECT p.name FROM people p JOIN authored a ON p.id=a.person_id
-- JOIN documents d ON d.id=a.doc_id WHERE d.year=2024
-- Cypher equivalent:
MATCH (p:Person)-[:AUTHORED]->(d:Document {year: 2024})
RETURN p.name