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("SupportCase")
.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("SupportCase")
.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("Manufacturer", "Apple")
.Out("Device")
.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("Manufacturer", "Apple")
.Out("Device")
.Out("SupportCase")
.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("SupportCase");
request.TargetUIDs = Q().StartAt("Customer", customerId)
.Out("Account")
.Out("SupportCase")
.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 Q().StartAtSimilarText("screen flicker after sleep",
nodeTypes: new[] { "SupportCase" },
count: 500)
.IsRelatedTo(Node.GetUID("Manufacturer", "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("SupportCase");
request.VectorSearchTypes = new[] { "SupportCase" };
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("SupportCase", caseId) };
return Q().StartAt("SupportCase")
.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("SupportCase");
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("Customer", customerId)
.Out("SupportCase")
.EmitSummary();
12. Neighbor summary (schema discovery)
Useful during development to see what edges exist on a type.
return Q().StartAt("SupportCase").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("Engineer")
.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("SupportCase")
.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("Product", productId)
.In("Viewed") // users who viewed this product
.Out("Viewed") // other products those users viewed
.Where(n => n.UID != Node.GetUID("Product", productId))
.SortByConnectivity()
.Take(10)
.Emit("N");
Tips
- Always bound traversal. Queries default to 50 nodes for safety. Use
Take(n)to raise the cap deliberately, neverTakeAll()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.