Combine
Combine operations merge results from multiple queries using set operations. Use union() to combine
results, intersect() to find common results, and except() to exclude results.
Set Operations Overview
Section titled “Set Operations Overview”| Operation | Description | Duplicates |
|---|---|---|
union() | Combine results from both queries | Removed |
unionAll() | Combine results from both queries | Kept |
intersect() | Results that appear in both queries | Removed |
except() | Results in first query but not second | Removed |
union()
Section titled “union()”Combine results from multiple queries, removing duplicates:
const activeOrAdmin = await store .query() .from("Person", "p") .whereNode("p", (p) => p.status.eq("active")) .select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })) .union( store .query() .from("Person", "p") .whereNode("p", (p) => p.role.eq("admin")) .select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })) ) .execute();This returns all active users PLUS all admins, with duplicates removed (active admins appear once).
Selection Shape Must Match
Section titled “Selection Shape Must Match”Both queries must have the same selection shape:
// Valid: Same shapequery1.select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })) .union( query2.select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })) )
// Invalid: Different shapes - will cause an errorquery1.select((ctx) => ({ id: ctx.p.id })) .union( query2.select((ctx) => ({ id: ctx.p.id, name: ctx.p.name })) )unionAll()
Section titled “unionAll()”Combine results keeping duplicates:
const allMentions = await store .query() .from("Comment", "c") .whereNode("c", (c) => c.mentions.contains(userId)) .select((ctx) => ({ id: ctx.c.id, text: ctx.c.text })) .unionAll( store .query() .from("Post", "p") .whereNode("p", (p) => p.mentions.contains(userId)) .select((ctx) => ({ id: ctx.p.id, text: ctx.p.content })) ) .execute();Use unionAll() when:
- You want to preserve duplicates
- Performance matters (no deduplication overhead)
- You’re counting occurrences
intersect()
Section titled “intersect()”Find results that appear in both queries:
const activeAdmins = await store .query() .from("Person", "p") .whereNode("p", (p) => p.status.eq("active")) .select((ctx) => ({ id: ctx.p.id })) .intersect( store .query() .from("Person", "p") .whereNode("p", (p) => p.role.eq("admin")) .select((ctx) => ({ id: ctx.p.id })) ) .execute();This returns only users who are BOTH active AND admins.
Equivalent to AND
Section titled “Equivalent to AND”intersect() can often be replaced with combined predicates:
// Using intersectquery1.intersect(query2)
// Often equivalent to.whereNode("p", (p) => p.status.eq("active").and(p.role.eq("admin")))Use intersect() when the queries are complex or involve different traversal paths.
except()
Section titled “except()”Find results in the first query but not the second (set difference):
const nonAdminActive = await store .query() .from("Person", "p") .whereNode("p", (p) => p.status.eq("active")) .select((ctx) => ({ id: ctx.p.id })) .except( store .query() .from("Person", "p") .whereNode("p", (p) => p.role.eq("admin")) .select((ctx) => ({ id: ctx.p.id })) ) .execute();This returns active users who are NOT admins.
Order Matters
Section titled “Order Matters”Unlike union() and intersect(), the order of queries in except() matters:
// Active users who are NOT adminsactiveUsers.except(admins)
// Admins who are NOT active (different result!)admins.except(activeUsers)Chaining Set Operations
Section titled “Chaining Set Operations”Chain multiple set operations:
const complexSet = await store .query() .from("Person", "p") .whereNode("p", (p) => p.status.eq("active")) .select((ctx) => ({ id: ctx.p.id })) .union( store.query() .from("Person", "p") .whereNode("p", (p) => p.role.eq("admin")) .select((ctx) => ({ id: ctx.p.id })) ) .except( store.query() .from("Person", "p") .whereNode("p", (p) => p.suspended.eq(true)) .select((ctx) => ({ id: ctx.p.id })) ) .execute();
// (active OR admin) AND NOT suspendedOrdering and Limiting Combined Results
Section titled “Ordering and Limiting Combined Results”Apply ordering and limits after set operations:
const results = await query1 .union(query2) .orderBy("name", "asc") .limit(100) .execute();Real-World Examples
Section titled “Real-World Examples”Multi-Source Search
Section titled “Multi-Source Search”Search across different node types:
async function globalSearch(term: string) { const people = store .query() .from("Person", "p") .whereNode("p", (p) => p.name.ilike(`%${term}%`)) .select((ctx) => ({ id: ctx.p.id, type: "person" as const, title: ctx.p.name, }));
const companies = store .query() .from("Company", "c") .whereNode("c", (c) => c.name.ilike(`%${term}%`)) .select((ctx) => ({ id: ctx.c.id, type: "company" as const, title: ctx.c.name, }));
return people .union(companies) .limit(20) .execute();}Exclude Blocklist
Section titled “Exclude Blocklist”const eligibleUsers = await store .query() .from("User", "u") .whereNode("u", (u) => u.status.eq("active")) .select((ctx) => ({ id: ctx.u.id, email: ctx.u.email })) .except( store .query() .from("BlockedUser", "b") .traverse("blockedUser", "e") .to("User", "u") .select((ctx) => ({ id: ctx.u.id, email: ctx.u.email })) ) .execute();Find Common Connections
Section titled “Find Common Connections”async function mutualFriends(userId1: string, userId2: string) { const user1Friends = store .query() .from("Person", "p") .whereNode("p", (p) => p.id.eq(userId1)) .traverse("follows", "e") .to("Person", "friend") .select((ctx) => ({ id: ctx.friend.id, name: ctx.friend.name }));
const user2Friends = store .query() .from("Person", "p") .whereNode("p", (p) => p.id.eq(userId2)) .traverse("follows", "e") .to("Person", "friend") .select((ctx) => ({ id: ctx.friend.id, name: ctx.friend.name }));
return user1Friends .intersect(user2Friends) .execute();}Deduplicate Recursive Results
Section titled “Deduplicate Recursive Results”Remove duplicate nodes from recursive traversals:
// Get unique reachable nodes (recursive may return duplicates via different paths)const uniqueNodes = await store .query() .from("Node", "start") .traverse("linkedTo", "e") .recursive() .to("Node", "reachable") .select((ctx) => ({ id: ctx.reachable.id })) .union( // Union with empty set to deduplicate (hack) store .query() .from("Node", "n") .whereNode("n", (n) => n.id.eq("__nonexistent__")) .select((ctx) => ({ id: ctx.n.id })) ) .execute();