Skip to content

Cypher Graph Queries

Zatabase includes a built-in Cypher query engine for graph traversal and pattern matching. Nodes are stored as rows in labeled tables, and relationships (edges) are first-class objects with types, direction, and properties. This lets you model and query graph structures alongside your relational and vector data — all in one database.

All Cypher queries are executed via:

POST /v1/cypher
Authorization: Bearer <token>
Content-Type: application/json
{"cypher": "YOUR CYPHER QUERY HERE"}
{
"rows": [ ... ],
"affected": null
}
  • rows — Array of JSON objects for read queries (MATCH).
  • affected — Number of nodes/relationships created, deleted, or merged for write operations.

Zatabase’s graph model maps onto its existing table/row storage:

  • Nodes are rows in a table. The table name serves as the node label (e.g., Person, Movie, City).
  • Relationships (edges) are directed, typed connections between two nodes. Each edge has a type (e.g., KNOWS, LIKES), a direction, and optional properties stored as key-value pairs.
  • Properties on both nodes and edges can be integers, floats, strings, booleans, vectors, lists, or null.
TypeCypher LiteralExample
Integerbare number42, -7
Floatdecimal number3.14, 0.5
Stringsingle-quoted'hello'
Booleankeywordtrue, false
Nullkeywordnull
Vectorbracket array[0.1, 0.2, 0.3] (in ORDER BY distance)

MATCH finds patterns in the graph. Every MATCH requires at least one relationship hop.

Find all people who know other people:

MATCH (a:Person)-[r:KNOWS]->(b:Person)
RETURN a, b
  • (a:Person) — binds variable a to nodes in the Person table.
  • -[r:KNOWS]-> — traverses outgoing edges of type KNOWS, binding the relationship to r.
  • (b:Person) — binds the target node to b.
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, b LIMIT 10"}'

Chain multiple hops to traverse deeper into the graph:

MATCH (a:Person)-[r:KNOWS]->(b:Person)-[s:LIKES]->(c:Movie)
RETURN a, c

This finds people who know someone who likes a movie.

Use *min..max to match paths of variable length:

MATCH (a:Person)-[:KNOWS*1..3]->(b:Person)
WHERE a.name = 'Alice'
RETURN b

This finds all people reachable from Alice through 1 to 3 KNOWS hops.

Supported formats:

  • *1..3 — between 1 and 3 hops
  • *2 — exactly 2 hops
  • *1.. — 1 or more hops (no upper bound)
  • *..3 — up to 3 hops

OPTIONAL MATCH works like a left outer join: if no matching pattern is found, the unmatched variables are kept as null rather than filtering out the row.

MATCH (p:Person)-[:IN]->(c:City)
OPTIONAL MATCH (p:Person)-[:OWNS]->(pet:Pet)
RETURN p, pet

People without pets still appear in the results; their pet variable will be unbound.

The RETURN clause specifies what data to include in the results.

Return entire nodes:

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a, b

Return specific properties:

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name

Use AS to rename returned properties:

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name AS source, b.name AS target

Access properties on relationships by binding the edge to a variable:

MATCH (a:Person)-[r:KNOWS]->(b:Person)
WHERE r.since > 2020
RETURN a.name, b.name, r.since

Three aggregation functions are supported:

COUNT — count matching rows:

MATCH (p:Person)-[:IN]->(c:City)
RETURN COUNT(*) AS total
MATCH (p:Person)-[:IN]->(c:City)
RETURN COUNT(p) AS people_count

SUM — sum a numeric property:

MATCH (o:Order)-[:FOR]->(c:Customer)
RETURN SUM(o.amount) AS revenue

COLLECT — collect property values into a list:

MATCH (p:Person)-[:IN]->(g:Group)
RETURN COLLECT(p.name) AS members

All aggregation functions support AS aliases.

Filter results with WHERE clauses after the MATCH pattern.

OperatorDescription
=Equal to
<>Not equal to
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal
MATCH (a:Item)-[:HAS]->(t:Tag)
WHERE a.price > 20
RETURN a

Cross-type numeric comparisons work (integer vs. float):

WHERE a.price >= 9.99

Combine conditions with AND, OR, and NOT:

MATCH (a:N)-[:R]->(b:M)
WHERE a.x = 1 OR a.x = 3
RETURN a
MATCH (a:N)-[:R]->(b:M)
WHERE NOT a.x = 2
RETURN a
MATCH (a:N)-[:R]->(b:M)
WHERE (a.x = 1 OR a.x = 2) AND NOT a.x = 2
RETURN a

Parentheses are supported for grouping.

PredicateDescription
STARTS WITHString prefix match
ENDS WITHString suffix match
CONTAINSSubstring match
MATCH (p:Person)-[:LIVES_IN]->(c:City)
WHERE p.name STARTS WITH 'Ali'
RETURN p
WHERE p.name ENDS WITH 'son'
WHERE p.name CONTAINS 'li'
WHERE a.email IS NULL
WHERE a.email IS NOT NULL

Compare properties across nodes or edges:

MATCH (a:Item)-[:RELATED]->(b:Item)
WHERE a.price > b.price
RETURN a, b

Restrict the number of returned rows:

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a, b
LIMIT 10

Order results by vector similarity using the distance() function. This enables graph + vector hybrid queries — a unique Zatabase capability.

MATCH (a:docs)-[:similar]->(b:docs)
WHERE a.title = 'Introduction'
RETURN b
ORDER BY distance(b.embedding, [0.1, 0.2, 0.3], 'l2')
LIMIT 5

The distance() function takes three arguments:

  1. A property reference to a vector field (e.g., b.embedding)
  2. A query vector as a bracket-delimited array
  3. A distance metric as a string

Supported distance metrics:

MetricStringDescription
L2 (Euclidean)'l2'Euclidean distance (default)
Cosine'cosine'Cosine distance (1 - cosine similarity)
Dot Product'dot', 'inner', 'innerproduct'Negated dot product (for distance ordering)

This is how you combine graph traversal with vector search — traverse relationships to find related nodes, then rank them by embedding similarity.

Create new nodes and relationships.

CREATE (a:Person {name: 'Alice', age: 30})

This inserts a row into the Person table with the specified properties. The table is created automatically if it doesn’t exist.

Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "CREATE (a:Person {name: '\''Alice'\'', age: 30})"}'
CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})

Create a relationship between two nodes defined in the same statement:

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

Relationships can also carry properties:

CREATE (a:Person {name: 'Alice'})-[:KNOWS {since: 2020}]->(b:Person {name: 'Bob'})

MERGE is an idempotent create: it finds a matching node or creates one if none exists. This is useful for ensuring data is not duplicated.

MERGE (a:Person {name: 'Alice'})
  • If a node with label Person and name = 'Alice' exists, it returns the existing node.
  • If no match is found, it creates a new node with those properties.
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MERGE (a:Person {name: '\''Alice'\''})"}'

Note: ON MATCH SET / ON CREATE SET clauses are not yet supported. MERGE for relationships requires using a MATCH + CREATE pattern instead.

Delete nodes and relationships found by a MATCH pattern.

MATCH (a:Person)-[:TAGGED]->(t:Tag)
WHERE a.name = 'Alice'
DELETE a

DETACH DELETE removes the node and all its incoming and outgoing edges:

MATCH (a:Person)-[:TAGGED]->(t:Tag)
WHERE a.name = 'Alice'
DETACH DELETE a

Delete just the relationship by binding it to a variable:

MATCH (a:Person)-[r:KNOWS]->(b:Person)
WHERE a.name = 'Alice' AND b.name = 'Bob'
DELETE r
MATCH (a:Person)-[r:KNOWS]->(b:Person)
WHERE a.name = 'Alice'
DELETE a, r

Cypher queries can also be executed through the PostgreSQL wire protocol using special function syntax. Zatabase registers Cypher-related functions accessible from any PostgreSQL client:

FunctionDescription
cypher_query(query, params)Execute a Cypher query with parameters
cypher_match(pattern, where, return)Execute a MATCH query
cypher_create(pattern, properties)Execute a CREATE
cypher_merge(pattern, properties)Execute a MERGE
cypher_delete(match, detach)Execute a DELETE
cypher_path(start, end, max_depth)Find a path between nodes
cypher_shortest_path(start, end)Find shortest path

Graph analysis functions are also available:

FunctionDescription
graph_degree(node, direction)Get node degree
graph_neighbors(node, direction)Get neighboring nodes
graph_components(type)Find connected components
graph_centrality(type, nodes)Compute centrality metrics
Terminal window
psql "postgresql://admin:[email protected]/zatabase" -c \
"SELECT * FROM cypher_query('MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a, b', '{}')"

Zatabase’s Cypher implementation covers the core subset of Neo4j’s Cypher query language while adding unique capabilities like vector similarity search within graph traversals.

FeatureZatabaseNeo4j
MATCH with pattern matchingYesYes
OPTIONAL MATCHYesYes
Node labels and relationship typesYesYes
Property access (n.name)YesYes
WHERE with comparisonsYesYes
AND / OR / NOTYesYes
String predicates (STARTS WITH, ENDS WITH, CONTAINS)YesYes
IS NULL / IS NOT NULLYesYes
Variable-length paths (*1..3)YesYes
CREATE nodes and relationshipsYesYes
MERGE (idempotent create)YesYes
DELETE / DETACH DELETEYesYes
RETURN with property access and aliasesYesYes
COUNT, SUM, COLLECT aggregationsYesYes
ORDER BY and LIMITYesYes
Edge propertiesYesYes
Multi-hop patternsYesYes
Inline property constraints {name: 'Alice'}YesYes
FeatureZatabaseNeo4j
ORDER BY distance() for vector searchYesNo
Runs alongside SQL and vector searchYesNo (separate database)
ON MATCH SET / ON CREATE SET in MERGENot yetYes
SET clause for property updatesNot yetYes
WITH clause for query chainingNot yetYes
UNWIND clauseNot yetYes
GROUP BYNot yetYes
CASE expressionsNot yetYes
WHERE ... IN [list]Not yetYes
Multiple labels per nodeNot yetYes
Index hintsNot yetYes
FOREACHNot yetYes
CALL proceduresNot yetYes
Path functions (length(), nodes(), relationships())Not yetYes
  • Graph + Vector hybrid queries: Traverse graph patterns then rank results by vector similarity using ORDER BY distance(). No other graph database offers this natively.
  • Unified engine: Graph queries, SQL queries, and vector search all operate on the same underlying data — no data duplication or sync required.
  • Permission-aware traversal: Graph queries respect Zatabase’s RBAC permission system. Row-level and table-level deny rules are enforced during traversal.
  • Persistent edges: Edges are automatically persisted to the ZQL storage engine and survive server restarts.
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MATCH (me:Person)-[:KNOWS*1..2]->(friend:Person) WHERE me.name = '\''Alice'\'' RETURN friend.name"}'

Knowledge Graph: Traverse and Rank by Similarity

Section titled “Knowledge Graph: Traverse and Rank by Similarity”
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MATCH (a:Document)-[:RELATED]->(b:Document) WHERE a.title = '\''Introduction'\'' RETURN b ORDER BY distance(b.embedding, [0.1, 0.2, 0.3, 0.4], '\''cosine'\'') LIMIT 5"}'
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MATCH (o:Order)-[:FOR]->(c:Customer) WHERE c.name = '\''Acme'\'' RETURN SUM(o.amount) AS revenue"}'
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MERGE (u:User {email: '\''[email protected]'\'', name: '\''Alice'\''})"}'
Terminal window
curl -s -X POST https://your-project.zatabase.io/v1/cypher \
-H "Authorization: Bearer $ZATABASE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cypher": "MATCH (a:Person)-[:KNOWS]->(b:Person) WHERE a.name = '\''Alice'\'' DETACH DELETE a"}'
  • MATCH requires at least one relationship hop. You cannot MATCH (a:Person) RETURN a — a relationship pattern is required. Use the SQL endpoint for simple table scans.
  • MERGE for relationships is not supported directly. Use MATCH to find existing nodes, then CREATE the relationship via the API.
  • ON MATCH SET / ON CREATE SET clauses in MERGE are not yet implemented.
  • SET, REMOVE, WITH, UNWIND, FOREACH, CALL are not yet supported.
  • Multiple labels per node are not supported. Each node has exactly one label (its table name).
  • No CREATE INDEX — indexes are managed through Zatabase’s SQL interface or API.
  • Relationship creation in the HTTP handler currently only supports creating nodes via CREATE. Full relationship creation through the Cypher endpoint requires the relationship patterns to reference variables bound in the same CREATE statement.
  1. Always specify node labels. Unlabeled source nodes require prior variable binding, which is not possible in a single-statement query. Labels also constrain the scan to a single table, improving performance.

  2. Use LIMIT on large graphs. Without a limit, MATCH will return all matching patterns, which can be expensive on large datasets. The default server-side cap is 1000 rows.

  3. Prefer MERGE for idempotent writes. When ingesting data that may overlap, MERGE prevents duplicate nodes.

  4. Use DETACH DELETE for nodes with edges. Plain DELETE on a node that has edges will remove the node but the dangling edge references are also cleaned up. Use DETACH DELETE for clarity and to explicitly remove all connected edges.

  5. Combine graph + vector for semantic search. Zatabase’s unique strength is hybrid queries: traverse a knowledge graph to find candidate nodes, then rank them by embedding similarity using ORDER BY distance().

  6. Use edge properties for weighted graphs. Relationship properties (e.g., {weight: 0.8, since: 2020}) are accessible in WHERE and RETURN clauses, enabling weighted graph algorithms.

  7. Keep variable-length paths bounded. Open-ended paths (*1..) can be expensive. Set a reasonable max_hops (e.g., *1..5) to avoid unbounded exploration.