Skip to main content

Multi-Hop Queries Tutorial

Learn pattern matching with variable-length paths.

What You'll Build

A network where we explore multi-hop relationship patterns.

Setup

import { InMemoryGraphFactory } from 'grafio';
import { CypherEngine } from 'grafio/cypher';

const factory = new InMemoryGraphFactory();
const graph = factory.forGraph('default');
const engine = new CypherEngine(graph);

// Build a network
const alice = await graph.addNode('Person', { name: 'Alice' });
const bob = await graph.addNode('Person', { name: 'Bob' });
const carol = await graph.addNode('Person', { name: 'Carol' });
const dave = await graph.addNode('Person', { name: 'Dave' });
const eve = await graph.addNode('Person', { name: 'Eve' });

// Alice KNOWS Bob KNOWS Carol KNOWS Dave
await graph.addEdge(alice.id, bob.id, 'KNOWS');
await graph.addEdge(bob.id, carol.id, 'KNOWS');
await graph.addEdge(carol.id, dave.id, 'KNOWS');

// Alice also KNOWS Eve directly
await graph.addEdge(alice.id, eve.id, 'KNOWS');

1-Hop Queries (Direct Connections)

MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name
`);
// Alice->Bob, Bob->Carol, Carol->Dave, Alice->Eve

2-Hop Queries (Friends of Friends)

MATCH (a:Person)-[:KNOWS*2]->(c:Person)
RETURN a.name, c.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*2]->(c:Person)
RETURN a.name, c.name
`);
// Alice->Carol (via Bob), Bob->Dave (via Carol)

3-Hop Queries (Friends of Friends of Friends)

MATCH (a:Person)-[:KNOWS*3]->(d:Person)
RETURN a.name, d.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*3]->(d:Person)
RETURN a.name, d.name
`);
// Alice->Dave (via Bob->Carol)

Variable Length: 1 to 3 Hops

MATCH (a:Person)-[:KNOWS*1..3]->(reachable)
RETURN a.name, reachable.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*1..3]->(r:Person)
RETURN a.name, r.name
`);
// All people reachable within 1-3 hops

Bounded Length

MATCH (a:Person)-[:KNOWS*2..2]->(b:Person)
RETURN a.name, b.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*2..2]->(b:Person)
RETURN a.name, b.name
`);
// Exactly 2 hops only

Unbounded (Up to 100 Hops)

MATCH (a:Person)-[:KNOWS*]->(b:Person)
RETURN a.name, b.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*]->(b:Person)
RETURN a.name, b.name
`);
// All reachable (up to 100 hops default)

Capture Named Paths

MATCH p = (a:Person)-[:KNOWS*]->(b:Person)
WHERE length(p) > 1
RETURN p
const result = await engine.query(`
MATCH p = (a:Person)-[:KNOWS*]->(b:Person)
WHERE length(p) > 1
RETURN p
`);

DISTINCT to Avoid Duplicates

MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)
RETURN DISTINCT a.name, b.name
const result = await engine.query(`
MATCH (a:Person)-[:KNOWS*1..2]->(b:Person)
RETURN DISTINCT a.name, b.name
`);

Finding Shortest Path

BFS is used by default for shortest path:

MATCH (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(e:Person {name: 'Eve'})
RETURN a.name, e.name
const result = await engine.query(`
MATCH (a:Person {name: 'Alice'})-[:KNOWS*1..3]->(e:Person {name: 'Eve'})
RETURN a.name, e.name
`);
// Alice->Eve directly (shortest path)

Complete Example

import { InMemoryGraphFactory } from 'grafio';
import { CypherEngine } from 'grafio/cypher';

async function main() {
const factory = new InMemoryGraphFactory();
const graph = factory.forGraph();
const engine = new CypherEngine(graph);

// Build network
const [alice, bob, carol, dave, eve] = await Promise.all([
graph.addNode('Person', { name: 'Alice' }),
graph.addNode('Person', { name: 'Bob' }),
graph.addNode('Person', { name: 'Carol' }),
graph.addNode('Person', { name: 'Dave' }),
graph.addNode('Person', { name: 'Eve' }),
]);

await graph.addEdge(alice.id, bob.id, 'KNOWS');
await graph.addEdge(bob.id, carol.id, 'KNOWS');
await graph.addEdge(carol.id, dave.id, 'KNOWS');
await graph.addEdge(alice.id, eve.id, 'KNOWS');

// Multi-hop queries
const foaf = await engine.query(`
MATCH (me:Person {name: 'Alice'})-[:KNOWS*2]->(friend)
RETURN DISTINCT friend.name
`);

const reachable = await engine.query(`
MATCH (me:Person {name: 'Alice'})-[:KNOWS*1..3]->(reachable)
RETURN DISTINCT reachable.name
ORDER BY reachable.name
`);

console.log('Friends of friends:', foaf.rows);
console.log('All reachable:', reachable.rows);
}

main();

Next Steps