Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cookbook: Cypher

Practical recipes for querying and writing Fluree with openCypher. Cypher runs on the same engine as JSON-LD and SPARQL, and Cypher relationships are stored as edge annotations — so data written here is readable from every surface.

Examples assume a ledger whose default @context maps the bare names (Person, KNOWS, name, …) — see IRI mapping. For how to send these statements (Rust / CLI / HTTP), see Running Cypher.

Model a property graph

Create nodes with labels and properties, and a relationship that carries its own properties:

CREATE (alice:Person {name: "Alice", age: 34})
CREATE (acme:Org {name: "Acme"})
MATCH (a:Person {name: "Alice"}), (o:Org {name: "Acme"})
CREATE (a)-[:WORKS_FOR {role: "Engineer", since: 2024}]->(o)

The relationship’s properties (role, since) live on the edge, not on a hand-modeled intermediate node.

Query relationships

MATCH (p:Person)-[r:WORKS_FOR]->(o:Org)
RETURN p.name, r.role, o.name

Plain vs. reified edges. Binding a relationship variable (-[r:T]->) or filtering on its properties reads the reified edge (bag semantics, one row per relationship). Without the variable (-[:T]->) you get the plain triple (set semantics) and see every base edge, including ones not written through Cypher/@annotation. See relationship lowering.

Optional matches and filters:

MATCH (p:Person)
OPTIONAL MATCH (p)-[:WORKS_FOR]->(o:Org)
WHERE p.age > 30
RETURN p.name, o.name

Shape rows as maps

Build a structured row with a map literal, or dump a node’s properties:

MATCH (p:Person {name: "Alice"})
RETURN {name: p.name, age: p.age} AS person

MATCH (p:Person {name: "Alice"})
RETURN properties(p) AS props, keys(p) AS fields

Map projection is shorthand for building a map from a node — .key selectors, a computed entry, or .* for every property:

MATCH (p:Person {name: "Alice"})
RETURN p{.name, .age, nextYear: p.age + 1} AS person

MATCH (p:Person {name: "Alice"})
RETURN p{.*} AS allProps

In the default cypher-json output a map is a native JSON object ({"name": "Alice", "age": 30}). properties(n) returns only data properties (not labels or relationships); keys(n) is their names. An object parameter is a map value too — $filter = {city: "NYC"} can be passed and returned as-is.

Find-or-create with MERGE

MERGE creates the pattern only if it doesn’t already exist, with optional ON CREATE / ON MATCH branches:

MERGE (p:Person {email: "alice@example.com"})
ON CREATE SET p.created = 2024
ON MATCH SET p.lastSeen = 2024

(A Cypher write returns a commit receipt, not query rows — there is no row-returning RETURN on a write statement.)

MERGE also works on a relationship. Standalone, the whole path is the match key and missing endpoints are created:

MERGE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})

With a leading MATCH binding the endpoints it is a per-row find-or-create — the edge is added only for matched pairs that don’t already have it (idempotent on re-run):

MATCH (a:Person), (b:Person) WHERE a.name <> b.name
MERGE (a)-[:KNOWS]->(b)

Update and delete

MATCH (p:Person {name: "Alice"}) SET p.age = 35, p:Verified
MATCH (p:Person {name: "Alice"}) REMOVE p.age
MATCH (p:Person {name: "Bob"}) DETACH DELETE p

SET p += {a: 1, b: 2} merges a map of properties; SET p:Label adds a label; DETACH DELETE removes a node together with its relationships.

Compute or filter before a write with WITH

A WITH between the match and the write can carry a computed value into the write or gate which rows are written:

MATCH (a:Person {name: "Alice"})
WITH a, a.birthYear + 30 AS adultAt
SET a.adultAt = adultAt

MATCH (p:Person)
WITH p, p.age AS age WHERE age >= 30
SET p.adult = true

WITH narrows scope to its projection (Cypher semantics) — only the listed names are visible to the write. Aggregation, DISTINCT, and ORDER BY / SKIP / LIMIT on a write-side WITH are not yet supported.

Transform lists with comprehensions, reduce, and predicates

// Project + filter a list inline.
MATCH (p:Person)
RETURN [x IN range(1, 10) WHERE x % 2 = 0 | x * x] AS evenSquares

// Fold a list to a scalar.
RETURN reduce(total = 0, x IN [3, 5, 7] | total + x) AS sum

// Collect nodes, then map a property over them (loop-local property access).
MATCH (p:Person)
RETURN [x IN collect(p) | x.name] AS names

// Quantify over a list.
MATCH (p:Person)
WHERE all(t IN p.tags WHERE t <> "banned")
RETURN p.name

The loop variable is scoped to the body, and property access on it works for both node elements (x.name scans the graph) and map elements (row.email for [row IN $people | row.email]). A null or non-list input yields null.

A pattern comprehension collects a projection over a correlated pattern — e.g. each person with the list of their friends’ names:

MATCH (p:Person)
RETURN p.name, [(p)-[:KNOWS]->(f:Person) WHERE f.age > 30 | f.name] AS adultFriends

Subqueries with CALL

CALL { … } runs a read-only subquery as a pipeline step. A scope clause imports outer variables (correlated execution); without one, the subquery runs once and its result is broadcast to every row:

// Per-person friend count (grouped per imported `p`).
MATCH (p:Person)
CALL (p) { MATCH (p)-[:KNOWS]->(f:Person) RETURN count(f) AS friends }
RETURN p.name, friends

// Uncorrelated: one count, broadcast to every row.
CALL { MATCH (x:Person) RETURN count(x) AS total }
MATCH (p:Person)
RETURN p.name, total

A correlated aggregating CALL is grouped per import, so a person with no matches drops out. Wrap the inner MATCH in OPTIONAL MATCH to keep them as 0:

MATCH (p:Person)
CALL (p) { OPTIONAL MATCH (p)-[:KNOWS]->(f:Person) RETURN count(f) AS friends }
RETURN p.name, friends

The body may also be a UNION of branches sharing a column shape — handy for “try these alternatives, then continue”:

MATCH (p:Person)
CALL (p) {
  MATCH (p)-[:KNOWS]->(f:Person)   RETURN f.name AS contact
  UNION
  MATCH (p)-[:WORKS_FOR]->(o:Org)  RETURN o.name AS contact
}
RETURN p.name, contact

The body must end in RETURN with explicit columns. Scope is strict: a returned name can’t re-bind an outer one, and the body can’t reuse an outer variable’s name without importing it — or use CALL (*) { … } to import the whole outer scope. Writes inside CALL aren’t supported yet.

Paths

Variable-length traversal (name the relationship type):

MATCH (a:Person {name: "Alice"})-[:KNOWS*1..3]->(b:Person)
RETURN DISTINCT b.name

Untyped traversal — follow any relationship type per hop (handy for “who is reachable from Alice within 3 hops, over any edge”):

MATCH (a:Person {name: "Alice"})-[*1..3]->(b:Person)
RETURN DISTINCT b.name

Untyped paths follow only node→node relationships — they skip data properties, :Label membership, and the edge-annotation sidecar — and use reachability semantics (each node reachable within the hop range). Give the path a direction (-[*]-> or <-[*]-); undirected untyped paths aren’t supported.

Shortest path between two people, and its length:

MATCH (a:Person {name: "Alice"}), (b:Person {name: "Dan"})
MATCH p = shortestPath((a)-[:KNOWS*]->(b))
RETURN length(p), nodes(p)

allShortestPaths(...) returns one row per minimal-length path; nodes(p) and pathPairs(p) are list-valued, so they feed UNWIND and the list functions.

Aggregate and collect

MATCH (p:Person)-[:WORKS_FOR]->(o:Org)
RETURN o.name, count(p) AS headcount, collect(p.name) AS people
ORDER BY headcount DESC

UNWIND expands a list back into rows — e.g. the nodes along a path:

MATCH p = shortestPath((a:Person {name: "Alice"})-[:KNOWS*]->(b:Person {name: "Dan"}))
UNWIND nodes(p) AS person
RETURN person.name

Cross-surface round-trip

Because Cypher relationships are edge annotations, a relationship written in Cypher is visible to JSON-LD and SPARQL, and vice versa:

CREATE (a:Person {name: "Alice"})-[:WORKS_FOR {role: "Engineer"}]->(o:Org {name: "Acme"})

reads back through the SPARQL 1.2 annotation tail or the JSON-LD @annotation surface as the same edge with the same role metadata — see Edge annotations.

See also