Query Cookbook
Patterns for combining graph traversal, search, and similarity in custom endpoints. Each recipe is self-contained and meant to be pasted into an endpoint and adapted.
For the method-by-method reference, see Graph query language. For the search request shape, see Search DSL.
1. List recent items of a type
The simplest possible query. Use it to verify schema and ingestion.
return Q().StartAt(N.SupportCase.Type)
.SortByTimestamp(oldestFirst: false)
.Take(20)
.Emit("N");
2. Paginated list
There's no built-in cursor — use Skip/Take. For very large pages, switch to a cursor (carry the last UID and exclude it).
int page = int.Parse(Query["page"] ?? "0");
int size = 20;
return Q().StartAt(N.SupportCase.Type)
.SortByTimestamp(oldestFirst: false)
.Skip(page * size)
.Take(size)
.Emit("N");
3. One-hop traversal
Use this whenever you'd write a JOIN in SQL.
return Q().StartAt(N.Manufacturer.Type, "Apple")
.Out(N.Device.Type)
.Take(50)
.Emit("N");
4. Multi-hop traversal
Each hop multiplies the working set. Keep paths short and bound with Take between hops if the fan-out is large.
return Q().StartAt(N.Manufacturer.Type, "Apple")
.Out(N.Device.Type)
.Out(N.SupportCase.Type)
.SortByTimestamp(oldestFirst: false)
.Take(50)
.Emit("N");
5. Filter by property
Property predicates run server-side. Start from a narrow set so the filter doesn't have to evaluate against the whole type.
return Q().StartAt(N.Device.Type)
.Where(n => n.GetString(N.Device.Status) == "Active")
.Take(50)
.Emit("N");
6. Search-within-context
The flagship pattern: a graph traversal computes the candidate set, then search runs against it. This is how you implement "find issues mentioning X in this customer's tickets."
var request = new SearchRequest("battery drain")
.WithTypesFacet(N.SupportCase.Type);
request.TargetUIDs = Q().StartAt(N.Customer.Type, customerId)
.Out(N.Account.Type)
.Out(N.SupportCase.Type)
.AsUIDEnumerable()
.ToArray();
return await Graph.CreateSearchAsync(request);
7. Permission-aware search
CreateSearchAsUserAsync applies the user's ACL graph before scoring. Use it for any user-facing surface — never call the unscoped variant from a logged-in user's request.
return await Graph.CreateSearchAsUserAsync(request, User.Id);
8. Semantic similarity with graph constraints
Vector retrieval as the start set, graph filter as the second stage. The graph filter is much cheaper than re-embedding, so always push hard constraints into the graph layer.
return (await Q().StartAtSimilarTextAsync("screen flicker after sleep",
nodeTypes: new[] { N.SupportCase.Type },
count: 500))
.IsRelatedTo(Node.GetUID(N.Manufacturer.Type, "Apple"))
.Take(20)
.EmitWithScores();
9. Hybrid search (keyword + vector)
For mixed lexical/semantic queries, let the search engine run both branches and merge.
var request = new SearchRequest("error 0x80004005 after update")
.WithTypesFacet(N.SupportCase.Type);
request.VectorSearchTypes = new[] { N.SupportCase.Type };
request.VectorSearchMode = VectorSearchMode.Hybrid;
return await Graph.CreateSearchAsync(request);
10. "More like this"
Use an explicit UID as the similarity seed when the user is already looking at a specific node.
var seed = new[] { Node.GetUID(N.SupportCase.Type, caseId) };
return Q().StartAt(N.SupportCase.Type)
.Where(n => !seed.Contains(n.UID))
.Take(500)
.EmitWithScores();
For higher-quality similarity, push the seed into SimilarityRanking on a SearchRequest:
var request = new SearchRequest("")
.WithTypesFacet(N.SupportCase.Type);
request.SimilarityRanking = new SimilarityRanking().WithSimilarTo(seed);
return await Graph.CreateSearchAsync(request);
11. Aggregate / count by type
Skip materialization — EmitSummary returns counts without loading nodes.
return Q().StartAt(N.Customer.Type, customerId)
.Out(N.SupportCase.Type)
.EmitSummary();
12. Neighbor summary (schema discovery)
Useful during development to see what edges exist on a type.
return Q().StartAt(N.SupportCase.Type).EmitNeighborsSummary();
13. Top contributors (sort by connectivity)
Surface "most connected" nodes — the engineers with the most resolved tickets, the parts that appear in the most cases, etc.
return Q().StartAt(N.Engineer.Type)
.SortByConnectivity()
.Take(10)
.Emit("N");
14. Time window
Combine a sort on the timestamp with a filter on the same property:
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
return Q().StartAt(N.SupportCase.Type)
.Where(n => n.GetTime(N.SupportCase.OpenedAt) >= cutoff)
.SortByTimestamp(oldestFirst: false)
.Take(100)
.Emit("N");
15. Recommend related items
Two-hop walk through a shared neighbor, often used for "people who viewed this also viewed."
return Q().StartAt(N.Product.Type, productId)
.Out(edgeType: "Viewed") // users who viewed this product
.Out(edgeType: "Viewed") // other products those users viewed
.Where(n => n.UID != Node.GetUID(N.Product.Type, productId))
.SortByConnectivity()
.Take(10)
.Emit("N");
16. Random sample
TakeRandom(count) draws a uniform sample with reservoir sampling — the result set is enumerated once, only the sample is held in memory, and results come back in random order. Unlike Take it is eager, and access filtering is applied before sampling.
return Q().StartAt(N.SupportCase.Type)
.TakeRandom(25)
.Emit("N");
Pair it with WithoutGraphMetadata() when the export should carry only the nodes' own fields and timestamp, with no Link / UID / Type / EdgeCount columns:
await Q().StartAt(N.SupportCase.Type)
.TakeRandom(1000)
.Emit("Sample")
.WithoutGraphMetadata()
.ToCsvAsync(stream);
Both are graph-side only — see Graph query language.
Tips
- Always bound traversal. Queries default to 50 nodes for safety. Use
Take(n)to raise the cap deliberately; avoid uncapped reads in user-facing endpoints. - Start narrow. A
StartAt(type, key)is orders of magnitude cheaper thanStartAt(type).Where(...). - Move computation into the graph. Don't fan out to a search just to filter — if there's an edge that represents the filter, use it.
- Validate with
EmitSummary. Stage 1 of a multi-hop query? Run anEmitSummaryfirst to see how many nodes you're carrying. - Use LLMs for narration, not computation. Aggregation, filtering, ranking all live in the graph; the LLM answers questions about the result set.