xrayGraphDB Documentation
Install the engine, connect a client, learn Cypher and GFQL, then explore the complete interactive reference of 395 functions and 125 procedures. Version 1, generated 2026-06-27.
Installation: Docker
Docker is the fastest way to start xrayGraphDB. The image ships with sensible defaults — storage path, ports, and encryption at rest. Pull it, bootstrap an admin on the first run, and you're up.
Before you run anything: raise the host's memory-map limit so the database can start: sudo sysctl -w vm.max_map_count=262144 (persist it in /etc/sysctl.d/).
First run — bootstrap an admin
The daemon refuses connections without an admin user. Pass --init-admin-user and --init-admin-password on the first start to create one. These flags are bootstrap-only — they only create a user when no users exist, and become silent no-ops on every subsequent boot once the admin record is on disk.
# Pull (or `docker load < xraygraphdb-4.9.4-docker.tar.gz` for the offline tarball) docker pull xraygraphdb.emtailabs.com/xraygraphdb:v4.9.4 # First run — creates admin/<your-password> on initial boot docker run -d \ --name xraygraphdb \ --restart unless-stopped \ -p 7689:7689 \ -v xraygraphdb-data:/var/lib/xraygraphdb \ -v xraygraphdb-logs:/var/log/xraygraphdb \ xraygraphdb.emtailabs.com/xraygraphdb:v4.9.4 \ --license-acknowledge-saved=true \ --init-admin-user=admin \ --init-admin-password=YourStrongPassword!23 # Wait ~5s, then confirm the daemon is listening docker logs xraygraphdb | grep "xrayProtocol listening" # Expected: "xrayProtocol listening on 0.0.0.0:7689 with 4 workers"
Security: rotate the bootstrap flags off after the first boot. Once the daemon confirms it's listening, the admin record is persisted in /var/lib/xraygraphdb/auth/ and the --init-admin-* flags do nothing on restart. But they remain visible in three places — docker inspect xraygraphdb (full Args[]), /var/lib/docker/containers/<id>/config.v2.json on the host, and ps aux inside the container. Recreate the container without the bootstrap flags so the password isn't sitting in process args:
docker stop xraygraphdb && docker rm xraygraphdb docker run -d \ --name xraygraphdb \ --restart unless-stopped \ -p 7689:7689 \ -v xraygraphdb-data:/var/lib/xraygraphdb \ -v xraygraphdb-logs:/var/log/xraygraphdb \ xraygraphdb.emtailabs.com/xraygraphdb:v4.9.4 \ --license-acknowledge-saved=true
The named volumes xraygraphdb-data and xraygraphdb-logs persist the database state and logs across container recreations. The data path inside the image is /var/lib/xraygraphdb (matches the daemon's default --data-directory).
Enterprise license (optional)
If you have a license file, mount it read-only at /etc/xraygraphdb/license.xglicense. The daemon auto-detects and validates it on startup:
docker run -d \ --name xraygraphdb \ -p 7689:7689 \ -v xraygraphdb-data:/var/lib/xraygraphdb \ -v /path/to/license.xglicense:/etc/xraygraphdb/license.xglicense:ro \ xraygraphdb.emtailabs.com/xraygraphdb:v4.9.4 \ --license-acknowledge-saved=true docker logs xraygraphdb | grep "License loaded" # Expected: "License loaded: xg-ent-... tier=enterprise org=..."
Without a license, the community-tier graph algorithms (PageRank, BFS, triangle count, betweenness, community detection, and more) run unrestricted. An enterprise license unlocks the additional commercial procedure namespaces.
Docker Compose
services: xraygraphdb: image: xraygraphdb.emtailabs.com/xraygraphdb:v4.9.4 restart: unless-stopped ports: - "7689:7689" volumes: - xraygraphdb-data:/var/lib/xraygraphdb - xraygraphdb-logs:/var/log/xraygraphdb # Optional: Enterprise license # - ./license.xglicense:/etc/xraygraphdb/license.xglicense:ro command: - --license-acknowledge-saved=true # --- FIRST RUN ONLY: uncomment for the first `docker compose up`, # --- then comment back out and `docker compose up -d --force-recreate`. # - --init-admin-user=admin # - --init-admin-password=YourStrongPassword!23 volumes: xraygraphdb-data: xraygraphdb-logs:
docker compose up -d docker compose logs -f xraygraphdb | grep "xrayProtocol listening"
Verify the Server
# Container status docker ps --filter name=xraygraphdb # Daemon ready check — xrayProtocol on 7689 (Bolt is OFF by default; opt-in via --bolt-server-name) docker logs xraygraphdb | grep "xrayProtocol listening" # Expected: "xrayProtocol listening on 0.0.0.0:7689 with 4 workers" # Test connection (Python xray_protocol_client; HELLO must carry a database name) python3 -c 'import xray_protocol_client as xg conn = xg.connect(host="localhost", port=7689, auth_token="admin:YourStrongPassword!23", database="xraygraphdb") print(conn.execute_query("MATCH (n) RETURN count(n) AS n_count"))'
Installation: Linux
Two install paths on Ubuntu 24.04 LTS: the .deb package (recommended — auto-resolves runtime deps via apt) or the portable .tar.gz (everything bundled, runs against the system glibc).
.deb — Ubuntu / Debian
# 1. Download wget https://xraygraphdb.emtailabs.com/downloads/xraygraphdb_4.9.4_amd64.deb # 2. Install — use `apt install -f` so apt auto-resolves the libgdal34t64 + python3 deps. # `dpkg -i` directly will FAIL the first time with "depends on libgdal34t64; however ...". sudo apt install -f ./xraygraphdb_4.9.4_amd64.deb
.tar.gz — portable build (any glibc ≥ 2.39)
The tarball ships every C/C++ runtime we need (libstdc++, libLLVM, libsolclient, libssl, libcrypto, libxraygraphdb_module_support) under lib/. The install.sh wrapper drops them at /usr/lib/xraygraphdb/lib, registers the path with ldconfig, installs the systemd unit, and apt-installs the one external dep (libgdal34t64).
wget https://xraygraphdb.emtailabs.com/downloads/xraygraphdb-4.9.4-linux-x86_64.tar.gz tar xzf xraygraphdb-4.9.4-linux-x86_64.tar.gz cd xraygraphdb-4.9.4-linux-x86_64 sudo ./install.sh
Configure — required systemd drop-in
The default /lib/systemd/system/xraygraphdb.service ships an empty ExecStart sentinel so you can layer site-local flags via a drop-in without forking the unit file. Create the drop-in before the first systemctl start:
sudo mkdir -p /etc/systemd/system/xraygraphdb.service.d sudo tee /etc/systemd/system/xraygraphdb.service.d/local.conf >/dev/null <<'EOF' [Service] ExecStart= ExecStart=/usr/lib/xraygraphdb/xraygraphdb \ --data-directory=/var/lib/xraygraphdb \ --bolt-port=7687 \ --xray-port=7689 \ --storage-properties-on-edges=true \ --log-level=WARNING \ --license-acknowledge-saved=true \ --bolt_listen_mode=off EOF sudo systemctl daemon-reload sudo systemctl start xraygraphdb sudo systemctl status xraygraphdb ss -tlnp | grep :7689 # xrayProtocol
Enterprise multi-tenant deployments can encrypt each tenant's data at rest using your own key management system (HashiCorp Vault, AWS KMS, or a compatible KMS). Setup is covered in the licensed admin guide. Single-tenant and evaluation installs work out of the box with no additional configuration.
SIGTERM (graceful shutdown). Never use kill -9. A forced kill skips the final snapshot and may result in data loss on next recovery.
Installation: macOS
macOS is supported for development only. For production workloads use Linux or Docker.
# Download macOS binary (Apple Silicon or Intel) curl -LO https://releases.emtailabs.com/xraygraphdb/xraygraphdb-v4.9.4-macos-arm64.tar.gz # Extract and run tar xzf xraygraphdb-v4.9.4-macos-arm64.tar.gz cd xraygraphdb-v4.9.4 ./bin/xraygraphdb-wrapper
On macOS you may also use Docker Desktop, which is the recommended approach for local development.
First Boot: Bootstrap Admin
Fresh installs ship with no users in the auth store. The daemon will not let you create the first admin user over the wire (chicken-and-egg), so you must run the one-time bootstrap helper before the database is usable. This step does not apply to replica nodes — replicas inherit the admin user from the cluster's MAIN; see Cluster Setup.
sudo xraygraphdb-bootstrap-admin
The helper:
- Generates a 24-character random password.
- Displays it once on your terminal in red.
- Asks you to retype it to confirm you copied it.
- Writes the username/password to
/run/xraygraphdb/bootstrap.env— this is a tmpfs file, never on disk — and starts the daemon. - Schedules an
ExecStartPosthook that wipes the env file ~15 seconds after the daemon comes up.
/var/lib/xraygraphdb and re-run the bootstrap — the existing user data is unrecoverable.
Loading datasets — where to put files
The default systemd unit ships with PrivateTmp=true for sandbox hardening. That gives the daemon its own private /tmp/ namespace, separate from the host's /tmp/. Anything you place in the host's /tmp/ is invisible to the daemon, and bulk-import calls against those paths fail-fast with 0 vertices / 0 edges in under a second — no error in the journal.
/tmp/. Even though the dataset file is world-readable, the daemon cannot see it. Use /var/lib/xraygraphdb/import/ instead — this path is in the unit's ReadWritePaths=, owned by the xraygraphdb user, and shares the daemon's namespace.
# Right way — daemon can read it: sudo mkdir -p /var/lib/xraygraphdb/import sudo mv ~/com-friendster.ungraph.txt /var/lib/xraygraphdb/import/ sudo chown -R xraygraphdb:xraygraphdb /var/lib/xraygraphdb/import # Then in your bench/import script: client.bulk_import_file("/var/lib/xraygraphdb/import/com-friendster.ungraph.txt") # Wrong way — daemon's PrivateTmp namespace makes this invisible: # client.bulk_import_file("/tmp/com-friendster.ungraph.txt") ← returns 0/0 in 1s
If you need a different dataset path (e.g. a large NVMe mount at /data/), add it to the systemd unit's ReadWritePaths= via a drop-in:
[Service] ReadWritePaths=/data
Then systemctl daemon-reload && systemctl restart xraygraphdb, and the daemon can read/write under /data/.
If the host is going to join an existing cluster as a replica, skip this section and use xraygraphdb-cluster-join instead.
Connect
First-party clients speak xrayProtocol on port 7689; the Bolt port 7689's companion 7687 is provided for triage compatibility. Pick your language below.
First Connection: Python
xrayGraphDB is compatible with the official Neo4j Python driver. Install it with pip and connect over the Bolt protocol.
pip install neo4j
from neo4j import GraphDatabase driver = GraphDatabase.driver( "bolt://localhost:7687", auth=("admin", "<your-password>") ) with driver.session() as session: # Create a node session.run( "CREATE (n:Person {name: $name, age: $age})", name="Alice", age=30 ) # Read it back result = session.run( "MATCH (n:Person {name: $name}) RETURN n.name, n.age", name="Alice" ) record = result.single() print(record["n.name"], record["n.age"]) # Output: Alice 30 driver.close()
First Connection: JavaScript
npm install neo4j-driver
const neo4j = require('neo4j-driver'); const driver = neo4j.driver( 'bolt://localhost:7687', neo4j.auth.basic('admin', '<your-password>') ); const session = driver.session(); try { // Create a node await session.run( 'CREATE (n:Person {name: $name, age: $age})', { name: 'Bob', age: 25 } ); // Read it back const result = await session.run( 'MATCH (n:Person {name: $name}) RETURN n', { name: 'Bob' } ); console.log(result.records[0].get('n').properties); } finally { await session.close(); await driver.close(); }
First Connection: Java
Add the Neo4j Java driver to your Maven or Gradle project.
<!-- Maven dependency --> <dependency> <groupId>org.neo4j.driver</groupId> <artifactId>neo4j-java-driver</artifactId> <version>5.x</version> </dependency>
import org.neo4j.driver.*; public class XRayExample { public static void main(String[] args) { var driver = GraphDatabase.driver( "bolt://localhost:7687", AuthTokens.basic("admin", "<your-password>") ); try (var session = driver.session()) { session.run( "CREATE (n:Person {name: $name})", Values.parameters("name", "Carol") ); var result = session.run( "MATCH (n:Person) RETURN n.name" ); while (result.hasNext()) { System.out.println(result.next().get("n.name").asString()); } } driver.close(); } }
First Connection: Go
go get github.com/neo4j/neo4j-go-driver/v5
package main import ( "context" "fmt" "github.com/neo4j/neo4j-go-driver/v5/neo4j" ) func main() { ctx := context.Background() driver, err := neo4j.NewDriverWithContext( "bolt://localhost:7687", neo4j.BasicAuth("admin", "<your-password>", ""), ) if err != nil { panic(err) } defer driver.Close(ctx) session := driver.NewSession(ctx, neo4j.SessionConfig{}) defer session.Close(ctx) _, err = session.Run(ctx, "CREATE (n:Person {name: $name})", map[string]any{"name": "Dave"}, ) if err != nil { panic(err) } result, err := session.Run(ctx, "MATCH (n:Person) RETURN n.name", nil, ) if err != nil { panic(err) } for result.Next(ctx) { fmt.Println(result.Record().Values[0]) } }
First Connection: .NET
dotnet add package Neo4j.Driver
using Neo4j.Driver; var driver = GraphDatabase.Driver( "bolt://localhost:7687", AuthTokens.Basic("admin", "<your-password>") ); await using var session = driver.AsyncSession(); await session.RunAsync( "CREATE (n:Person {name: $name})", new { name = "Eve" } ); var result = await session.RunAsync( "MATCH (n:Person) RETURN n.name" ); var records = await result.ToListAsync(); foreach (var record in records) { Console.WriteLine(record["n.name"].As<string>()); } await driver.DisposeAsync();
Quick Start Tutorial
This tutorial walks through creating a small graph, querying it, and cleaning up. It assumes you have a running xrayGraphDB instance and a Python driver installed.
// Step 1: Create some nodes CREATE (alice:Person {name: "Alice", age: 30}) CREATE (bob:Person {name: "Bob", age: 25}) CREATE (carol:Person {name: "Carol", age: 35}) CREATE (proj:Project {name: "xrayGraphDB"}) RETURN alice, bob, carol, proj; // Step 2: Create relationships MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"}) CREATE (a)-[:KNOWS]->(b) RETURN a, b; MATCH (a:Person {name: "Alice"}), (p:Project {name: "xrayGraphDB"}) CREATE (a)-[:WORKS_ON {role: "lead"}]->(p) RETURN a, p; MATCH (b:Person {name: "Bob"}), (p:Project {name: "xrayGraphDB"}) CREATE (b)-[:WORKS_ON {role: "contributor"}]->(p) RETURN b, p; // Step 3: Query the graph MATCH (p:Person)-[:WORKS_ON]->(proj:Project) RETURN p.name, proj.name; // Step 4: Update properties MATCH (a:Person {name: "Alice"}) SET a.email = "alice@example.com" RETURN a; // Step 5: Clean up MATCH (n) DETACH DELETE n;
$name, $age, etc.) to prevent injection and improve plan cache hit rates.
MATCH
The MATCH clause is the primary read operation. It describes a pattern to find in the graph and binds matching subgraphs to variables.
// Match all nodes with a specific label MATCH (n:Person) RETURN n; // Match a relationship pattern MATCH (a:Person)-[:KNOWS]->(b:Person) RETURN a.name, b.name; // Match with relationship variable MATCH (a:Person)-[r:WORKS_ON]->(p:Project) RETURN a.name, r.role, p.name; // Match any direction MATCH (a:Person)-[:KNOWS]-(b:Person) RETURN a.name, b.name; // Match with multiple labels MATCH (n:Person:Employee) RETURN n;
Patterns can include any combination of nodes, relationships, and directions. Nodes are enclosed in parentheses (), relationships in square brackets [], and direction is indicated by arrows -> or <-.
WHERE
The WHERE clause filters results from MATCH patterns. It supports comparison operators, boolean logic, string matching, list predicates, and null checks.
// Comparison operators MATCH (n:Person) WHERE n.age > 25 AND n.age <= 40 RETURN n.name, n.age; // String matching MATCH (n:Person) WHERE n.name STARTS WITH "A" RETURN n; // Regular expression MATCH (n:Person) WHERE n.email =~ ".*@example\\.com" RETURN n; // Null checks MATCH (n:Person) WHERE n.email IS NOT NULL RETURN n; // IN list MATCH (n:Person) WHERE n.name IN ["Alice", "Bob", "Carol"] RETURN n; // Pattern predicates (exists) MATCH (n:Person) WHERE (n)-[:WORKS_ON]->() RETURN n.name;
| Operator | Description | Example |
|---|---|---|
| = | Equal | n.age = 30 |
| <> | Not equal | n.name <> "Alice" |
| <, >, <=, >= | Comparison | n.age >= 18 |
| AND, OR, NOT | Boolean logic | n.age > 20 AND n.active = true |
| IN | List membership | n.status IN ["active", "pending"] |
| STARTS WITH | String prefix | n.name STARTS WITH "Al" |
| ENDS WITH | String suffix | n.name ENDS WITH "ice" |
| CONTAINS | String contains | n.name CONTAINS "li" |
| =~ | Regex match | n.email =~ ".*@example\\.com" |
| IS NULL | Null check | n.deleted IS NULL |
| IS NOT NULL | Not null | n.email IS NOT NULL |
RETURN
RETURN specifies which values to include in the result set. You can return nodes, relationships, properties, expressions, or aggregations.
// Return specific properties MATCH (n:Person) RETURN n.name, n.age; // Alias with AS MATCH (n:Person) RETURN n.name AS person_name, n.age AS years; // Return all properties as a map MATCH (n:Person) RETURN properties(n); // Return distinct values MATCH (n:Person)-[:WORKS_ON]->(p:Project) RETURN DISTINCT p.name; // Expressions in RETURN MATCH (n:Person) RETURN n.name, n.age * 12 AS age_in_months;
ORDER BY / LIMIT / SKIP
Control the ordering and pagination of results.
// Order by a property MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC; // Limit results MATCH (n:Person) RETURN n.name ORDER BY n.name LIMIT 10; // Pagination with SKIP and LIMIT MATCH (n:Person) RETURN n.name ORDER BY n.name SKIP 20 LIMIT 10; // Multiple sort keys MATCH (n:Person) RETURN n.name, n.age ORDER BY n.age DESC, n.name ASC;
WITH
WITH acts as a pipeline separator, allowing you to chain query stages together. Variables not listed in WITH are not available in subsequent clauses.
// Filter intermediate results MATCH (p:Person)-[:WORKS_ON]->(proj:Project) WITH proj, count(p) AS team_size WHERE team_size > 3 RETURN proj.name, team_size ORDER BY team_size DESC; // Chain queries MATCH (n:Person) WITH n ORDER BY n.age DESC LIMIT 5 MATCH (n)-[:KNOWS]->(friend) RETURN n.name, collect(friend.name) AS friends;
UNWIND
UNWIND expands a list into individual rows. Useful for bulk operations and working with list parameters.
// Expand a list UNWIND [1, 2, 3] AS x RETURN x; // Bulk create from parameters UNWIND $people AS person CREATE (n:Person {name: person.name, age: person.age}); // Combine with MATCH UNWIND ["Alice", "Bob"] AS name MATCH (n:Person {name: name}) RETURN n;
OPTIONAL MATCH
OPTIONAL MATCH works like MATCH but returns null for missing parts of the pattern instead of excluding the row entirely. Equivalent to a left outer join.
// Return all people, even those without projects MATCH (p:Person) OPTIONAL MATCH (p)-[:WORKS_ON]->(proj:Project) RETURN p.name, proj.name;
CREATE
CREATE adds new nodes and relationships to the graph. It always creates new elements (use MERGE to avoid duplicates).
// Create a single node CREATE (n:Person {name: "Frank", age: 28}) RETURN n; // Create multiple nodes CREATE (a:Person {name: "Grace"}), (b:Person {name: "Hank"}); // Create a node with multiple labels CREATE (n:Person:Developer {name: "Ivy"}); // Create a relationship between existing nodes MATCH (a:Person {name: "Grace"}), (b:Person {name: "Hank"}) CREATE (a)-[:KNOWS {since: 2024}]->(b) RETURN a, b; // Create a full path in one statement CREATE (a:Module {name: "auth"})-[:IMPORTS]->(b:Module {name: "crypto"}) RETURN a, b;
MERGE
MERGE ensures a pattern exists in the graph. If the pattern is found, it is bound. If not found, it is created. Use ON CREATE SET and ON MATCH SET to conditionally set properties.
// Merge a node (create if not exists) MERGE (n:Person {name: "Alice"}) ON CREATE SET n.created = timestamp() ON MATCH SET n.lastSeen = timestamp() RETURN n; // Merge a relationship MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"}) MERGE (a)-[r:KNOWS]->(b) ON CREATE SET r.since = 2024 RETURN r;
MERGE matches the entire pattern. If you merge on (a)-[:KNOWS]->(b) and the relationship does not exist, it creates only the relationship, not the nodes (they must already be bound by a preceding MATCH or MERGE).
SET
SET updates properties on nodes and relationships, or adds labels to nodes.
// Set a property MATCH (n:Person {name: "Alice"}) SET n.age = 31 RETURN n; // Set multiple properties MATCH (n:Person {name: "Alice"}) SET n.age = 31, n.email = "alice@example.com" RETURN n; // Replace all properties with a map MATCH (n:Person {name: "Alice"}) SET n = {name: "Alice", age: 31, active: true} RETURN n; // Merge properties (add without removing existing) MATCH (n:Person {name: "Alice"}) SET n += {department: "engineering"} RETURN n; // Add a label MATCH (n:Person {name: "Alice"}) SET n:Employee RETURN n;
REMOVE
REMOVE deletes properties from nodes/relationships and removes labels from nodes.
// Remove a property MATCH (n:Person {name: "Alice"}) REMOVE n.email RETURN n; // Remove a label MATCH (n:Person:Employee {name: "Alice"}) REMOVE n:Employee RETURN labels(n);
DELETE / DETACH DELETE
DELETE removes nodes and relationships. A node cannot be deleted if it still has relationships. Use DETACH DELETE to delete a node and all its relationships in one operation.
// Delete a relationship MATCH (a:Person)-[r:KNOWS]->(b:Person) WHERE a.name = "Alice" AND b.name = "Bob" DELETE r; // Delete a node (must have no relationships) MATCH (n:Person {name: "Frank"}) DELETE n; // Detach delete (node + all relationships) MATCH (n:Person {name: "Alice"}) DETACH DELETE n; // Delete all nodes and relationships in the database MATCH (n) DETACH DELETE n;
MATCH (n) DETACH DELETE n removes the entire graph. There is no undo. Make a snapshot before running destructive queries on production data.
Variable-length Paths
Variable-length path patterns match paths of varying depth using the * syntax inside relationship brackets.
// Paths of exactly 2 hops MATCH (a:Person)-[:KNOWS*2]->(c:Person) RETURN a.name, c.name; // Paths of 1 to 5 hops MATCH (a:Person)-[:KNOWS*1..5]->(c:Person) RETURN a.name, c.name; // Paths of any length (use with caution) MATCH (a:Person {name: "Alice"})-[:KNOWS*]->(c:Person) RETURN DISTINCT c.name; // Capture the path MATCH path = (a:Person {name: "Alice"})-[:KNOWS*1..3]->(c:Person) RETURN path, length(path) AS hops;
* without limits) can be expensive on large graphs. Always set an upper bound when possible.
shortestPath / allShortestPaths
Find the shortest path(s) between two nodes.
// Find one shortest path MATCH (a:Person {name: "Alice"}), (b:Person {name: "Eve"}) MATCH p = shortestPath((a)-[*..10]-(b)) RETURN p, length(p) AS hops; // Find all shortest paths (same length) MATCH (a:Person {name: "Alice"}), (b:Person {name: "Eve"}) MATCH p = allShortestPaths((a)-[*..10]-(b)) RETURN p; // With relationship type filter MATCH (a:Person {name: "Alice"}), (b:Person {name: "Eve"}) MATCH p = shortestPath((a)-[:KNOWS|WORKS_WITH*..10]-(b)) RETURN p;
BFS Traversal
Breadth-first search traversal is available for exploring graphs level by level. BFS guarantees that nodes are visited in order of increasing distance from the start node.
// BFS with upper bound MATCH (start:Person {name: "Alice"}) MATCH path = (start)-[:KNOWS BFS]->(target) RETURN target.name, length(path) AS distance ORDER BY distance;
BFS traversal is the default algorithm used by shortestPath. Use the explicit BFS syntax when you need to enumerate all reachable nodes by distance layer.
Aggregation
Aggregation functions operate on groups of rows. Non-aggregated columns in RETURN act as implicit group keys (similar to SQL GROUP BY).
// Count MATCH (n:Person) RETURN count(n) AS total_people; // Group and count MATCH (p:Person)-[:WORKS_ON]->(proj:Project) RETURN proj.name, count(p) AS team_size ORDER BY team_size DESC; // Sum, average, min, max MATCH (n:Person) RETURN sum(n.age) AS total_age, avg(n.age) AS avg_age, min(n.age) AS youngest, max(n.age) AS oldest; // Collect into a list MATCH (p:Person)-[:WORKS_ON]->(proj:Project) RETURN proj.name, collect(p.name) AS members; // Standard deviation and percentile MATCH (n:Person) RETURN stDev(n.age) AS std_dev, percentileCont(n.age, 0.5) AS median;
| Function | Description | Example |
|---|---|---|
| count(expr) | Number of non-null values | count(n) |
| sum(expr) | Sum of numeric values | sum(n.salary) |
| avg(expr) | Average of numeric values | avg(n.age) |
| min(expr) | Minimum value | min(n.created) |
| max(expr) | Maximum value | max(n.score) |
| collect(expr) | Collect values into a list | collect(n.name) |
| percentileCont(expr, p) | Continuous percentile (interpolated) | percentileCont(n.age, 0.5) |
| percentileDisc(expr, p) | Discrete percentile (nearest value) | percentileDisc(n.age, 0.9) |
| stDev(expr) | Standard deviation (sample) | stDev(n.score) |
| stDevP(expr) | Standard deviation (population) | stDevP(n.score) |
Indexing & Constraints
Indexes accelerate lookups by property value. Constraints enforce data integrity rules.
// Create a label-property index CREATE INDEX ON :Person(name); // Neo4j-compatible named index syntax CREATE INDEX person_name_idx FOR (n:Person) ON (n.name); // Composite index CREATE INDEX ON :Person(name, age); // Drop an index DROP INDEX ON :Person(name); // Unique constraint CREATE CONSTRAINT ON (n:Person) ASSERT n.email IS UNIQUE; // Existence constraint CREATE CONSTRAINT ON (n:Person) ASSERT EXISTS (n.name); // Show index info SHOW INDEX INFO; // List all constraints SHOW CONSTRAINT INFO;
WHERE clauses and MATCH patterns. Without an index, the engine must scan all nodes of a given label.
Transactions
xrayGraphDB supports both auto-commit transactions (single query) and explicit transactions (multi-query).
Auto-commit Transactions
Every query sent via session.run() runs in its own auto-commit transaction. If the query succeeds, it is committed. If it fails, it is rolled back.
Explicit Transactions
Use explicit transactions when you need to execute multiple queries atomically.
with driver.session() as session: tx = session.begin_transaction() try: tx.run("CREATE (a:Account {id: $id, balance: $bal})", id="A001", bal=1000) tx.run("CREATE (a:Account {id: $id, balance: $bal})", id="A002", bal=500) tx.commit() except Exception: tx.rollback() raise
// Explicit transaction commands (Bolt protocol) BEGIN; CREATE (n:Temp {data: "test"}); COMMIT; // Or rollback BEGIN; CREATE (n:Temp {data: "test"}); ROLLBACK;
CALL xg.builtin_functions() and available in the Community edition at no cost.
GFQL Overview
GFQL (Graph Frame Query Language) is a dataframe-native query language for graph traversal and analysis. It is designed for data scientists and developers who prefer chainable, functional-style operations over declarative pattern matching.
GFQL queries run natively inside the xrayGraphDB engine alongside Cypher. They operate on the same in-memory graph as Cypher queries, with the same transaction isolation guarantees.
SET GFQL_CONTEXT
Before executing GFQL operations, set a query context that defines the working scope (labels, edge types, or property filters).
// Set context to all Function nodes SET GFQL_CONTEXT label='Function'; // Set context with edge filter SET GFQL_CONTEXT label='Module', edge_type='IMPORTS'; // Set context with property filter SET GFQL_CONTEXT label='Person', WHERE age > 25;
chain(), n(), e_forward(), e_reverse()
GFQL operations are chained together using a fluent API. The core primitives are:
| Function | Description | Example |
|---|---|---|
| chain() | Start a GFQL operation chain | chain() |
| n() | Select nodes (optionally filtered) | n(label='Person') |
| e_forward() | Traverse outgoing edges | e_forward(type='CALLS') |
| e_reverse() | Traverse incoming edges | e_reverse(type='CALLS') |
| .filter() | Filter current frame | .filter(complexity > 5) |
| .hop() | Multi-hop traversal | .hop(edge_type='CALLS', depth=3) |
| .select() | Project specific columns | .select('name', 'module') |
| .aggregate() | Group and aggregate | .aggregate(by='module', count='name') |
// Find high-complexity functions and their callees chain() .n(label='Function') .filter(complexity > 10) .e_forward(type='CALLS') .select('source.name', 'target.name'); // Multi-hop traversal chain() .n(label='Module', name='auth') .hop(edge_type='IMPORTS', depth=3) .select('name', '_hop_depth'); // Aggregate by module chain() .n(label='Function') .aggregate(by='module', count='name', avg_complexity='complexity');
Filter Predicates
GFQL supports the following predicates inside .filter() expressions:
| Operator | Description | Example |
|---|---|---|
| =, != | Equality / inequality | .filter(status = 'active') |
| >, <, >=, <= | Comparison | .filter(age >= 18) |
| AND, OR | Logical operators | .filter(age > 18 AND active = true) |
| NOT | Logical negation | .filter(NOT deleted) |
| IN | List membership | .filter(status IN ['active', 'pending']) |
| LIKE | Pattern matching (% wildcard) | .filter(name LIKE 'auth%') |
| IS NULL | Null check | .filter(email IS NULL) |
| IS NOT NULL | Not null | .filter(email IS NOT NULL) |
Function Reference
395 built-in functions. Click a category in the sidebar to filter, or use the search box. Click any card to expand its full signature and details.
Procedure Reference
125 callable procedures invoked with CALL.
GFQL Operators
Graph operators available through the GFQL chain syntax.