Syntax & Clauses

The Cypher clauses, operators, and path syntax WhisperGraph supports, with a working example for each.

Updated July 2026

Syntax & Clauses Documentation

WhisperGraph implements a read-only Cypher dialect. You send Cypher over HTTP and get back columns and rows. Write clauses (CREATE, MERGE, SET, DELETE, REMOVE, FOREACH) are not supported — the parser recognizes them and rejects them.

Two rules make every query fast: anchor the starting node by its name (an indexed lookup), and add a LIMIT. This page walks each clause with a runnable example. For the labels and edge directions you traverse, see the Graph Schema; for the procedures you call with CALL, the Procedures reference. For the golden rules and pitfalls, see Best Practices.

MATCH

MATCH finds patterns in the graph. The fastest form anchors a node by its name, which is an indexed lookup.

MATCH (h:HOSTNAME {name: "google.com"}) RETURN h.name

Chain a relationship to reach the node on the other end:

MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:ANNOUNCED_PREFIX)
RETURN p.name LIMIT 5

A label-only match with no {name: ...} scans every node of that label. That is fine on small labels like CATEGORY, but it never finishes on billion-node labels like HOSTNAME or IPV4 — always anchor those.

OPTIONAL MATCH

OPTIONAL MATCH keeps the driving row even when the optional pattern has no match, filling the missing columns with null. Use it for sparse fields like WHOIS contacts or geolocation, where a plain MATCH would drop the whole row.

MATCH (h:HOSTNAME {name: "google.com"})
OPTIONAL MATCH (h)-[:HAS_EMAIL]->(e:EMAIL)
OPTIONAL MATCH (h)-[:HAS_REGISTRAR]->(r:REGISTRAR)
RETURN h.name, collect(DISTINCT e.name) AS emails, collect(DISTINCT r.name) AS registrars

WHERE

WHERE filters bound rows. The supported operators:

  • Comparison: =, <>, <, >, <=, >=
  • Logical: AND, OR, NOT, XOR
  • Null checks: IS NULL, IS NOT NULL
  • List membership: IN
  • String predicates: STARTS WITH, ENDS WITH, CONTAINS
MATCH (h:HOSTNAME)
WHERE h.name STARTS WITH "cloudflare."
RETURN h.name LIMIT 5
MATCH (a:ASN)
WHERE a.name IN ["AS13335", "AS15169"]
RETURN a.name

STARTS WITH and a selective, leading-dot ENDS WITH (for example ENDS WITH ".cloudflare.com") are indexed on .name and fast. CONTAINS and a broad ENDS WITH fall back to a scan, so anchor the rest of the query and add a LIMIT.

RETURN

RETURN selects what comes back. Use AS for aliases and DISTINCT to deduplicate.

MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p:PREFIX)
RETURN DISTINCT a.name AS asn, count(p) AS prefixes

WITH

WITH pipes results from one part of a query to the next. It is how you aggregate or narrow a set before traversing further, and you can filter after it with WHERE.

MATCH (h:HOSTNAME {name: "google.com"})<-[:NAMESERVER_FOR]-(ns:HOSTNAME)
WITH ns LIMIT 3
MATCH (ns)-[:NAMESERVER_FOR]->(sibling:HOSTNAME)
RETURN ns.name AS nameserver, collect(DISTINCT sibling.name)[0..8] AS domains

This anchor-then-narrow-then-expand shape is the single most useful pattern in the language. Bounding the intermediate set with WITH ... LIMIT keeps a two-stage query from exploding.

ORDER BY, LIMIT, SKIP

ORDER BY sorts, LIMIT caps the row count, and SKIP offsets for pagination. Always include a LIMIT, and use literal numbers in SKIP / LIMIT.

MATCH (sub:HOSTNAME)-[:CHILD_OF]->(:HOSTNAME {name: "google.com"})
RETURN sub.name AS subdomain
ORDER BY sub.name SKIP 0 LIMIT 15

To page, keep a stable ORDER BY and walk SKIP forward: SKIP 0 LIMIT 15, then SKIP 15 LIMIT 15, and so on.

UNWIND

UNWIND turns a list into rows, one per element. It is the right pattern for batch lookups: each element becomes its own anchored query.

UNWIND ["185.220.101.1", "104.16.123.96", "8.8.8.8"] AS addr
MATCH (ip:IPV4 {name: addr})
RETURN ip.name AS ip, ip.threatLevel AS level, ip.isThreat AS isThreat

The MATCH after UNWIND is still anchored — each row binds ip on its indexed name.

UNION

UNION combines results from multiple queries and deduplicates; UNION ALL keeps duplicates. Every branch must return the same column names.

MATCH (a:ASN {name: "AS13335"})-[:ROUTES]->(p) RETURN p.name AS n
UNION
MATCH (a:ASN {name: "AS13335"})-[:PEERS_WITH]->(peer) RETURN peer.name AS n

CALL procedures

CALL runs a procedure. Standalone, or with YIELD to name the columns you want and feed the rest of the query. See Procedures for the full set.

CALL explain("185.220.101.1")
CALL whisper.variants("paypal.com")
YIELD variant, method, exists
WHERE exists
RETURN variant, method LIMIT 10

A CALL placed after UNWIND, WITH, or MATCH runs once per incoming row, so you can score a whole list in one query:

UNWIND ["1.1.1.1", "8.8.8.8"] AS ip
CALL explain(ip) YIELD indicator, score, level
RETURN indicator, score, level

Schema-introspection and quota procedures don't count against your quota and are the fastest way to confirm a label or edge exists before you anchor on it:

CALL db.labels() YIELD label RETURN label ORDER BY label LIMIT 20

CALL subqueries

CALL { ... } scopes a subquery, importing outer variables with WITH. Bounding each branch inside its own CALL {} is the reliable way to run several per-branch aggregations from one anchor.

MATCH (a:ASN {name: "AS13335"})
CALL {
  WITH a
  MATCH (a)-[:ROUTES]->(p:ANNOUNCED_PREFIX)
  RETURN count(p) AS pc
}
RETURN a.name, pc

CASE

CASE is a conditional expression, in both the simple form (match a value) and the searched form (evaluate conditions).

MATCH (a:ASN {name: "AS13335"})
RETURN CASE a.name WHEN "AS13335" THEN "cloudflare" ELSE "other" END AS label
UNWIND [1, 5, 10] AS x
RETURN CASE WHEN x < 3 THEN "low" WHEN x < 8 THEN "mid" ELSE "high" END AS bucket

Patterns & paths

A pattern is a sequence of node and relationship descriptions. The forms:

  • Directed(:HOSTNAME {name: "google.com"})-[:RESOLVES_TO]->(ip)
  • Undirected(:ASN {name: "AS13335"})-[:PEERS_WITH]-(peer) (PEERS_WITH is symmetric)
  • Multi-type(:HOSTNAME {name: "google.com"})-[:RESOLVES_TO|HAS_EMAIL]->(x) matches either edge
  • Variable-length(:HOSTNAME {name: "www.google.com"})-[:CHILD_OF*1..3]->(parent) walks one to three hops
  • Named paths — bind the path with p = (...) to use nodes(), relationships(), and length()
  • Anonymous nodes(:ASN {name: "AS13335"})-[:ROUTES]->() when you don't need the far node bound
MATCH p = (h:HOSTNAME {name: "github.com"})-[:RESOLVES_TO]->(:IPV4)-[:ANNOUNCED_BY]->(:ANNOUNCED_PREFIX)
RETURN length(p) AS hops, [n IN nodes(p) | n.name] AS chain LIMIT 5

Variable-length relationships

Repeat an edge between a lower and upper bound. Always set an upper bound — the range must stay within your plan's hop cap (anonymous 2, free key 3), and unbounded [*] is rejected.

MATCH (h:HOSTNAME {name: "api.v2.github.com"})-[:CHILD_OF*1..3]->(parent)
RETURN parent.name LIMIT 5

Variable-length expansion walks only stored edges like CHILD_OF and LINKS_TO. It does not follow the synthesized edges (ROUTES, ANNOUNCED_BY, LISTED_IN, HAS_NAME, CONFLICTS_WITH); write those as explicit single hops. See Best Practices.

shortestPath

shortestPath finds the minimum-hop path between two anchored nodes. It requires a bounded path length, so always cap the variable-length range (for example [*1..6]).

MATCH p = shortestPath(
  (h:HOSTNAME {name: "www.google.com"})-[:CHILD_OF*1..5]->(t:TLD {name: "com"})
)
RETURN length(p) AS hops

EXPLAIN

EXPLAIN returns the query plan without executing it. Use it to confirm an anchored lookup hits the index rather than scanning.

EXPLAIN MATCH (h:HOSTNAME {name: "google.com"})-[:RESOLVES_TO]->(ip) RETURN ip.name

A NodeLookup (or index seek) at the leaf of the plan means the anchor is indexed. A bare label scan there is a warning that the query will be slow — anchor it before you run it.