MATCH Basics Tutorial

Learn fundamental pattern matching with GQL in this hands-on 10-minute tutorial.

Prerequisites

  • Geode server running (geode serve)
  • Access to Geode shell (geode shell) or a client library
  • No prior graph database experience required

Connect with Client Libraries

import (
    "database/sql"
    _ "geodedb.com/geode"
)

db, err := sql.Open("geode", "localhost:3141")
if err != nil {
    log.Fatal(err)
}
defer db.Close()
from geode_client import Client

client = Client(host="localhost", port=3141, skip_verify=True)
# Use: async with client.connection() as conn:
use geode_client::Client;

let client = Client::new("127.0.0.1", 3141).skip_verify(true);
let mut conn = client.connect().await?;
import { createClient } from '@geodedb/client';

const client = await createClient('quic://localhost:3141');
const geode = @import("geode_client");

var client = geode.GeodeClient.init(allocator, "localhost", 3141, true);
try client.connect();
try client.sendHello("match-tutorial", "1.0.0");
_ = try client.receiveMessage(30000);

Tutorial Overview

Time: 10 minutes Difficulty: Beginner Topics: Graph creation, node creation, relationships, basic queries

By the end of this tutorial, you’ll be able to:

  • Create a graph and add nodes
  • Create relationships between nodes
  • Query nodes with pattern matching
  • Filter results with WHERE clauses
  • Return specific properties

Step 1: Create a Graph

Start by creating a new graph for this tutorial:

CREATE GRAPH SocialNetwork;

Expected output:

Graph 'SocialNetwork' created successfully

Switch to using the graph:

USE SocialNetwork;

Expected output:

Using graph 'SocialNetwork'

What You Learned

  • CREATE GRAPH creates a new named graph
  • USE switches your session to work with a specific graph
  • Graph names are case-sensitive

Step 2: Create Nodes

Create three person nodes with properties:

CREATE
  (:Person {name: "Alice", age: 30}),
  (:Person {name: "Bob", age: 25}),
  (:Person {name: "Charlie", age: 35});

Expected output:

Created 3 nodes

Node Syntax Breakdown

(:Label {property: value, ...})
  • () - Node pattern
  • :Label - Node label (type)
  • {...} - Property map
  • property: value - Key-value pairs

What You Learned

  • Nodes have labels (like :Person)
  • Nodes can have multiple properties
  • CREATE can create multiple nodes in one statement
  • Properties use JSON-like syntax

Step 3: Create Relationships

Connect Alice and Bob with a KNOWS relationship:

MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"})
CREATE (a)-[:KNOWS {since: 2020}]->(b);

Expected output:

Created 1 relationship

Relationship Syntax Breakdown

(a)-[:TYPE {property: value}]->(b)
  • (a) and (b) - Start and end nodes
  • -[:TYPE]-> - Directed relationship with type
  • {...} - Relationship properties

What You Learned

  • MATCH finds existing nodes
  • Relationships connect two nodes
  • Relationships have types (like :KNOWS)
  • Relationships can have properties
  • -> indicates direction

Step 4: Query All Nodes

Retrieve all person nodes:

MATCH (p:Person)
RETURN p.name, p.age;

Execute with Client Libraries

rows, err := db.QueryContext(ctx, "MATCH (p:Person) RETURN p.name, p.age")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var name string
    var age int
    if err := rows.Scan(&name, &age); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s: %d\n", name, age)
}
async with client.connection() as conn:
    page, _ = await conn.query("MATCH (p:Person) RETURN p.name, p.age")
    for row in page.rows:
        name = row["p.name"].as_string
        age = row["p.age"].as_int
        print(f"{name}: {age}")
let (page, _) = conn.query("MATCH (p:Person) RETURN p.name, p.age").await?;
for row in &page.rows {
    let name = row.get("p.name").unwrap().as_string()?;
    let age = row.get("p.age").unwrap().as_int()?;
    println!("{}: {}", name, age);
}
const rows = await client.queryAll('MATCH (p:Person) RETURN p.name, p.age');
for (const row of rows) {
    const name = row.get('p.name')?.asString;
    const age = row.get('p.age')?.asNumber;
    console.log(`${name}: ${age}`);
}
try client.sendRunGql(1, "MATCH (p:Person) RETURN p.name, p.age", null);
const schema = try client.receiveMessage(30000);
defer allocator.free(schema);

try client.sendPull(1, 1000);
const result = try client.receiveMessage(30000);
defer allocator.free(result);
// Parse JSON response
std.debug.print("Result: {s}\n", .{result});

Expected output:

name     | age
---------|----
Alice    | 30
Bob      | 25
Charlie  | 35

Query Breakdown

  • MATCH (p:Person) - Find all Person nodes, bind to variable p
  • RETURN p.name, p.age - Return specific properties
  • Variables (p) are used to reference matched elements

What You Learned

  • MATCH patterns find nodes in the graph
  • Variables bind matched elements for later use
  • RETURN specifies which data to output
  • Property access uses dot notation (p.name)

Step 5: Filter with WHERE

Find people older than 26:

MATCH (p:Person)
WHERE p.age > 26
RETURN p.name, p.age
ORDER BY p.age DESC;

Expected output:

name     | age
---------|----
Charlie  | 35
Alice    | 30

Query Breakdown

  • WHERE p.age > 26 - Filter condition
  • ORDER BY p.age DESC - Sort results (descending)
  • Only nodes matching the condition are returned

Comparison Operators

=   -- Equal
<>  -- Not equal
<   -- Less than
<=  -- Less than or equal
>   -- Greater than
>=  -- Greater than or equal

What You Learned

  • WHERE filters matched nodes
  • Comparison operators work like SQL
  • ORDER BY sorts results
  • DESC = descending, ASC = ascending (default)

Step 6: Query Relationships

Find all KNOWS relationships:

MATCH (a:Person)-[k:KNOWS]->(b:Person)
RETURN a.name AS from, b.name AS to, k.since AS year;

Execute with Client Libraries

rows, _ := db.QueryContext(ctx, `
    MATCH (a:Person)-[k:KNOWS]->(b:Person)
    RETURN a.name AS from, b.name AS to, k.since AS year
`)
defer rows.Close()

for rows.Next() {
    var from, to string
    var year int
    rows.Scan(&from, &to, &year)
    fmt.Printf("%s knows %s since %d\n", from, to, year)
}
page, _ = await conn.query("""
    MATCH (a:Person)-[k:KNOWS]->(b:Person)
    RETURN a.name AS from, b.name AS to, k.since AS year
""")
for row in page.rows:
    print(f"{row['from'].as_string} knows {row['to'].as_string} since {row['year'].as_int}")
let (page, _) = conn.query(r#"
    MATCH (a:Person)-[k:KNOWS]->(b:Person)
    RETURN a.name AS from, b.name AS to, k.since AS year
"#).await?;

for row in &page.rows {
    println!("{} knows {} since {}",
        row.get("from").unwrap().as_string()?,
        row.get("to").unwrap().as_string()?,
        row.get("year").unwrap().as_int()?);
}
const rows = await client.queryAll(`
    MATCH (a:Person)-[k:KNOWS]->(b:Person)
    RETURN a.name AS from, b.name AS to, k.since AS year
`);
for (const row of rows) {
    console.log(`${row.get('from')?.asString} knows ${row.get('to')?.asString} since ${row.get('year')?.asNumber}`);
}
try client.sendRunGql(1,
    \\MATCH (a:Person)-[k:KNOWS]->(b:Person)
    \\RETURN a.name AS from, b.name AS to, k.since AS year
, null);
const schema = try client.receiveMessage(30000);
defer allocator.free(schema);

try client.sendPull(1, 1000);
const result = try client.receiveMessage(30000);
defer allocator.free(result);
std.debug.print("Result: {s}\n", .{result});

Expected output:

from   | to  | year
-------|-----|------
Alice  | Bob | 2020

Relationship Pattern Breakdown

(a:Person)-[k:KNOWS]->(b:Person)
  • (a:Person) - Source node
  • -[k:KNOWS]-> - Relationship with type, bound to variable k
  • (b:Person) - Target node
  • Matches only relationships from Person to Person

Column Aliases

RETURN a.name AS from
  • AS creates column alias
  • Makes output more readable
  • Useful for complex expressions

What You Learned

  • Relationships are matched with -[variable:TYPE]->
  • Relationship properties are accessed like node properties
  • AS creates column aliases for output
  • Relationship patterns specify source and target nodes

Step 7: Bidirectional Matching

Create a bidirectional relationship:

MATCH (b:Person {name: "Bob"}), (c:Person {name: "Charlie"})
CREATE (b)-[:KNOWS]->(c);

Now query without direction:

MATCH (p1:Person)-[:KNOWS]-(p2:Person)
WHERE p1.name = "Bob"
RETURN p1.name, p2.name;

Expected output:

name | name
-----|--------
Bob  | Alice
Bob  | Charlie

Undirected vs Directed

-[:KNOWS]-   -- Matches either direction
-[:KNOWS]->  -- Matches only left-to-right
<-[:KNOWS]-  -- Matches only right-to-left

What You Learned

  • Omit > or < for bidirectional matching
  • Same relationship can be traversed in either direction
  • WHERE can filter on matched variables

Step 8: Multiple Relationships

Create more connections:

MATCH (a:Person {name: "Alice"}), (c:Person {name: "Charlie"})
CREATE (a)-[:KNOWS]->(c);

MATCH (c:Person {name: "Charlie"}), (a:Person {name: "Alice"})
CREATE (c)-[:LIKES]->(a);

Query multiple relationship types:

MATCH (p1:Person)-[r]->(p2:Person)
RETURN p1.name, type(r) AS relationship_type, p2.name
ORDER BY p1.name, relationship_type;

Expected output:

name    | relationship_type | name
--------|-------------------|--------
Alice   | KNOWS             | Bob
Alice   | KNOWS             | Charlie
Bob     | KNOWS             | Charlie
Charlie | LIKES             | Alice

Relationship Type Functions

type(r)  -- Returns relationship type as string

What You Learned

  • [r] without type matches any relationship
  • type(r) function returns relationship type
  • Multiple relationship types can exist between same nodes
  • Results can be sorted by multiple columns

Step 9: Count Aggregation

Count nodes and relationships:

-- Count all people
MATCH (p:Person)
RETURN count(p) AS total_people;

-- Count relationships
MATCH ()-[r:KNOWS]->()
RETURN count(r) AS total_knows;

-- Count per person
MATCH (p:Person)-[r:KNOWS]->()
RETURN p.name, count(r) AS connections
ORDER BY connections DESC;

Expected output:

total_people
------------
3

total_knows
-----------
3

name    | connections
--------|------------
Alice   | 2
Bob     | 1
Charlie | 0

Aggregation Functions

count(x)  -- Count non-null values
sum(x)    -- Sum numeric values
avg(x)    -- Average
min(x)    -- Minimum
max(x)    -- Maximum

What You Learned

  • count() aggregates matched elements
  • Aggregations collapse multiple rows
  • Can combine aggregations with grouping
  • ORDER BY works with aggregated values

Step 10: Pattern Variables

Match multi-hop paths:

-- Find friends of friends
MATCH (a:Person {name: "Alice"})-[:KNOWS]->()-[:KNOWS]->(fof:Person)
WHERE fof <> a
RETURN DISTINCT fof.name AS friend_of_friend;

Expected output:

friend_of_friend
----------------
Charlie

Pattern Breakdown

  • ()-[:KNOWS]->() - Anonymous intermediate node
  • fof <> a - Exclude Alice herself
  • DISTINCT - Remove duplicates

What You Learned

  • Anonymous nodes () match any node
  • Multi-hop patterns traverse multiple relationships
  • <> is “not equal” operator
  • DISTINCT removes duplicate results

Complete Example: Social Network Analysis

Put it all together:

-- Create a larger network
CREATE
  (:Person {name: "David", age: 28}),
  (:Person {name: "Emma", age: 32});

MATCH (a:Person {name: "Alice"}), (d:Person {name: "David"})
CREATE (a)-[:KNOWS {since: 2021}]->(d);

MATCH (b:Person {name: "Bob"}), (e:Person {name: "Emma"})
CREATE (b)-[:KNOWS {since: 2019}]->(e);

-- Find most connected people
MATCH (p:Person)
OPTIONAL MATCH (p)-[r:KNOWS]-()
RETURN p.name, p.age, count(r) AS connections
ORDER BY connections DESC, p.age ASC;

Expected output:

name    | age | connections
--------|-----|------------
Alice   | 30  | 3
Bob     | 25  | 2
Charlie | 35  | 0
David   | 28  | 0
Emma    | 32  | 0

What You Learned

  • OPTIONAL MATCH returns null if no match (like LEFT JOIN)
  • Combine multiple ORDER BY criteria
  • Build complex queries incrementally

Common Patterns Reference

Find Node by Property

MATCH (n:Label {property: value})
RETURN n;
MATCH (a:Label1)-[:REL_TYPE]->(b:Label2)
RETURN a, b;

Filter with Multiple Conditions

MATCH (n:Label)
WHERE n.prop1 > 10 AND n.prop2 = 'value'
RETURN n;

Count Relationships

MATCH (n)-[r:TYPE]-()
RETURN n, count(r) AS rel_count;

Find Paths

MATCH path = (a)-[:TYPE*1..3]->(b)
RETURN path;

Practice Exercises

Exercise 1: Create Your Own Network

Create a network of movies and actors:

CREATE GRAPH MovieNetwork;
USE MovieNetwork;

-- Create movies
CREATE
  (:Movie {title: "The Matrix", year: 1999}),
  (:Movie {title: "John Wick", year: 2014});

-- Create actors
CREATE
  (:Actor {name: "Keanu Reeves", born: 1964}),
  (:Actor {name: "Laurence Fishburne", born: 1961});

-- Connect actors to movies
MATCH (k:Actor {name: "Keanu Reeves"}), (m1:Movie {title: "The Matrix"})
CREATE (k)-[:ACTED_IN {role: "Neo"}]->(m1);

MATCH (k:Actor {name: "Keanu Reeves"}), (m2:Movie {title: "John Wick"})
CREATE (k)-[:ACTED_IN {role: "John Wick"}]->(m2);

MATCH (l:Actor {name: "Laurence Fishburne"}), (m:Movie {title: "The Matrix"})
CREATE (l)-[:ACTED_IN {role: "Morpheus"}]->(m);

Questions:

  1. Find all actors in “The Matrix”
  2. Find all movies featuring Keanu Reeves
  3. Count how many movies each actor has been in
  4. Find actors who appeared in the same movie

Exercise 2: Add More Relationships

Extend the social network:

USE SocialNetwork;

-- Add workplace
MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"})
CREATE (a)-[:WORKS_WITH]->(b);

-- Add location
MATCH (p:Person)
WHERE p.name IN ["Alice", "Bob"]
SET p.city = "Seattle";

MATCH (p:Person)
WHERE p.name = "Charlie"
SET p.city = "Portland";

Questions:

  1. Find people who both know each other AND work together
  2. Find people in the same city
  3. Count relationships per person (all types)

Exercise 3: Complex Queries

Challenge yourself:

-- Find triangles (3 people who all know each other)
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)-[:KNOWS]->(a)
RETURN a.name, b.name, c.name;

-- Find people with no connections
MATCH (p:Person)
WHERE NOT EXISTS ((p)-[:KNOWS]-())
RETURN p.name;

-- Calculate average age by city
MATCH (p:Person)
RETURN p.city, avg(p.age) AS avg_age, count(p) AS population
ORDER BY avg_age DESC;

Solutions

Exercise 1 Solutions
-- 1. Find all actors in "The Matrix"
MATCH (a:Actor)-[:ACTED_IN]->(m:Movie {title: "The Matrix"})
RETURN a.name, a.born;

-- 2. Find all movies featuring Keanu Reeves
MATCH (k:Actor {name: "Keanu Reeves"})-[r:ACTED_IN]->(m:Movie)
RETURN m.title, m.year, r.role;

-- 3. Count movies per actor
MATCH (a:Actor)-[:ACTED_IN]->(m:Movie)
RETURN a.name, count(m) AS movie_count
ORDER BY movie_count DESC;

-- 4. Find co-actors
MATCH (a1:Actor)-[:ACTED_IN]->(m:Movie)<-[:ACTED_IN]-(a2:Actor)
WHERE a1 <> a2
RETURN a1.name, a2.name, m.title;
Exercise 2 Solutions
-- 1. People who know each other AND work together
MATCH (a:Person)-[:KNOWS]->(b:Person)
WHERE EXISTS ((a)-[:WORKS_WITH]->(b))
RETURN a.name, b.name;

-- 2. People in same city
MATCH (p1:Person), (p2:Person)
WHERE p1.city = p2.city AND p1 <> p2
RETURN DISTINCT p1.name, p2.name, p1.city;

-- 3. Count all relationships per person
MATCH (p:Person)
OPTIONAL MATCH (p)-[r]-()
RETURN p.name, count(r) AS total_connections
ORDER BY total_connections DESC;

Troubleshooting

Common Errors

Error: “Graph ‘SocialNetwork’ not found”

  • Solution: Run CREATE GRAPH SocialNetwork first

Error: “Variable ‘p’ not defined”

  • Solution: Ensure variable is defined in MATCH clause before using in RETURN

Error: “Node with label ‘Person’ and properties {name: ‘Alice’} not found”

  • Solution: Check node exists with MATCH (p:Person) RETURN p.name

Performance Tips

  • Use specific labels instead of matching all nodes
  • Filter early with WHERE clauses
  • Create indexes on frequently queried properties (see Indexing Tutorial )
  • Limit results with LIMIT when testing large datasets

Next Steps

Now that you understand MATCH basics, continue learning:

  1. Indexing Tutorial - Optimize query performance
  2. Graph Algorithms Tutorial - PageRank, community detection
  3. GQL Guide - Complete language reference
  4. Data Model - Property graph concepts

Quick Reference

Node Patterns

()                    -- Any node
(:Label)              -- Node with label
({prop: value})       -- Node with property
(:Label {prop: val})  -- Both label and property
(n)                   -- Bind to variable n

Relationship Patterns

-[:TYPE]->            -- Directed relationship
-[:TYPE]-             -- Any direction
<-[:TYPE]-            -- Reverse direction
-[r:TYPE]->           -- Bind to variable r
-[{prop: val}]->      -- With properties

Clauses

CREATE                -- Create nodes/relationships
MATCH                 -- Find patterns
WHERE                 -- Filter conditions
RETURN                -- Output results
ORDER BY              -- Sort results
LIMIT                 -- Restrict result count
DISTINCT              -- Remove duplicates

Functions

count(x)              -- Count elements
type(r)               -- Relationship type
EXISTS(pattern)       -- Check pattern exists

Tutorial Complete! You now understand the fundamentals of pattern matching in GQL.

Next: Indexing and Optimization Tutorial