Curiosity

Graph Query Language

The Curiosity Graph Query Language is a fluent C# interface for traversing and querying the knowledge graph. Two distinct IQuery interfaces exist depending on the context:

Context How to obtain Interface Results
Endpoints / Shell Q() / Query() global methods Mosaik.GraphDB.IQuery — full feature set In the Shell, use Emit(...). In endpoints, materialize with AsEnumerable() / ToList() and return a record.
Data Connectors Lambda in await graph.QueryAsync(q => q...) Curiosity.Library.IQuery — focused subset QueryResults returned by graph.QueryAsync; use GetEmitted() / GetEmittedCount().

Starting Points

Method Backend Library Notes
StartAt(string nodeType) All nodes of the given type.
StartAt(Node node) Start from a specific node. Library uses Node.FromKey().
StartAt(Node[] nodes) Start from a set of nodes.
StartAt(string nodeType, string[] keys) Start from nodes of a type identified by multiple keys.
StartAt(string type, string key) Start from a single node by type and key.
StartAt(UID128 uid) / StartAt(IEnumerable<UID128>) Start from one or more UIDs.
StartSearch(string nodeType, string field, ISearchExpression query) Full-text search on a specific field.
StartAtSimilarTextAsync(string text, int count, string[] nodeTypes, ...) Semantic similarity search.
StartNearTo(string nodeType, string field, GeoPoint point, double radius) Nodes within a geographic radius.
StartWhereString(string nodeType, string field, string value) Nodes where a string field matches a value.
StartWhereString(string nodeType, string field, values) Nodes where a string field matches any of a set. Values as string[], HashSet<string>, or IEnumerable<string>. An all-types StartWhereString(field, values) overload (no nodeType) is also available.
StartWhereNotString(string nodeType, string field, values) Nodes where a string field matches none of a set. Values as HashSet<string> or IEnumerable<string>; an all-types StartWhereNotString(field, values) overload is also available.
StartWhere(string nodeType, string field, Func<dynamic, bool> predicate) Nodes filtered by a field predicate.

Node references: In the backend, use Node.GetUID(type, key) to reference a node by key. In the library, use Node.FromKey(type, key).

// Backend (endpoint/shell)
Q().StartAt(N.Device.Type, "MacBook Air")
Q().StartAt(Node.GetUID(N.Device.Type, "MacBook Air"))

// Library (data connector)
await graph.QueryAsync(q => q.StartAt(Node.FromKey(nameof(Nodes.Device), "MacBook Air")).Emit("N"))
await graph.QueryAsync(q => q.StartAt(nameof(Nodes.Device), new[] { "MacBook Air", "MacBook Pro" }).Emit("N"))

Traversal

Method Backend Library Notes
Out() Traverse all outgoing edges.
Out(string nodeType) Traverse to a specific node type.
Out(string nodeType, string edgeType) Traverse via a specific edge type.
Out(string nodeType, string[] edgeTypes) Traverse via multiple edge types.
Out(string[] nodeTypes, string edgeType) Traverse to multiple node types via one edge type.
Out(string[] nodeTypes, string[] edgeTypes) Traverse to multiple types via multiple edge types.
OutMany(int levels, string[] nodeTypes, string[] edgeTypes, bool distinct) Multi-hop traversal.
OutWhere(string nodeType, string edgeType, Func<Edge, bool> predicate) Traverse with a predicate on the edge.
Search(string nodeType, string field, ISearchExpression query) Full-text search within the current result set.
PathBetween(UID128 from, UID128 to, int maxLevels) Find paths between two nodes.
// Backend
Q().StartAt(N.Manufacturer.Type, "Apple")
   .Out(N.Device.Type)
   .Out(N.SupportCase.Type)

// Library (data connector)
await graph.QueryAsync(q =>
    q.StartAt(Node.FromKey(nameof(Nodes.Manufacturer), "Apple"))
     .Out(nameof(Nodes.Device))
     .Out(nameof(Nodes.SupportCase))
     .Emit("Cases"))

Filtering

Method Backend Library Notes
OfType(string nodeType) Keep only nodes of the given type.
OfTypes(...) Keep only nodes of any of the given types.
ExceptType(string nodeType) Remove nodes of the given type.
ExceptTypes(...) Remove nodes of any of the given types.
IsRelatedTo(Node node) Keep nodes related to a specific node.
IsRelatedTo(Node[] nodes) Keep nodes related to any of the given nodes.
IsRelatedTo(string nodeType) Keep nodes related to any node of the given type.
IsRelatedTo(string[] nodeTypes) Keep nodes related to any of the given types.
IsRelatedToVia(Node node, string edgeType) Keep nodes related to a specific node via a specific edge.
IsRelatedToVia(string nodeType, string edgeType) Keep nodes related via matching node/edge type.
IsRelatedToVia(string[] nodeTypes, string[] edgeTypes) Keep nodes related via any matching combination.
IsNotRelatedTo(...) Remove nodes related to the given node(s)/type(s).
IsNotRelatedToVia(...) Remove nodes related via the given edge type.
WhereString(string nodeType, string field, string value) Keep nodes where a string field matches.
WhereString(string nodeType, string field, values) Keep nodes (of nodeType) where a string field matches any of a set. Values as string[], HashSet<string>, or IEnumerable<string>.
WhereString(string field, values, bool removeOtherTypes = false) All-types variant. Values as HashSet<string> or IEnumerable<string>. With removeOtherTypes: true, nodes whose type has no index for field are dropped; by default they are kept.
WhereNotString(string nodeType, string field, string value) Keep nodes where a string field does not match.
WhereNotString(string nodeType, string field, values, bool removeOtherTypes = false) Typed variant. Values as string[] or IEnumerable<string>.
WhereNotString(string field, values, bool removeOtherTypes = false) All-types variant. Values as HashSet<string> or IEnumerable<string>. With removeOtherTypes: true, nodes whose type has no index for field are dropped; by default they are kept.
WhereTimestamp(DateTimeOffset from, DateTimeOffset to, bool insideBoundary) Filter by timestamp (library uses DateTimeOffset).
WhereTimestamp(Time from, Time to, bool insideBoundary) Filter by timestamp (backend uses Time).
Where(Func<ReadOnlyNode, bool> predicate) Keep nodes matching a predicate.
Where(string field, Func<dynamic, bool> predicate) Keep nodes where a field satisfies a predicate.
Where<T>(string field, Func<T, bool> predicate) Strongly-typed field predicate — the field value is cast to T before the predicate runs.
Except(UID128 uid) / Except(IQuery other) Set difference.
Intersect(IQuery other) Set intersection.
Union(IQuery other) Set union.
Distinct() Deduplicate results.
// Library (data connector)
await graph.QueryAsync(q =>
    q.StartAt(nameof(Nodes.SupportCase))
     .IsRelatedTo(Node.FromKey(nameof(Nodes.Device), "MacBook Air"))
     .WhereString(nameof(Nodes.SupportCase), nameof(Nodes.SupportCase.Status), "Open")
     .WhereTimestamp(DateTimeOffset.UtcNow.AddDays(-7), DateTimeOffset.UtcNow, insideBoundary: true)
     .Emit("N"))

// Backend (endpoint/shell)
Q().StartAt(N.SupportCase.Type)
   .IsRelatedTo(Node.GetUID(N.Device.Type, "MacBook Air"))
   .WhereString(N.SupportCase.Type, N.SupportCase.Status, "Open")
   .WhereTimestamp(Time.Now().Add(TimeSpan.FromDays(-7)), Time.Now(), insideBoundary: true)

Sorting and Pagination

Method Backend Library Notes
SortByTimestamp(bool oldestFirst) Sort by creation time.
SortByLastUpdated(bool newestFirst) Sort by modification time.
SortByConnectivity(bool mostConnectedFirst) Sort by edge count.
SortByConnectivityTo(string[] nodeTypes, string[] edgeTypes, bool mostConnectedFirst) Sort by connections to specific types.
SortByDistanceFromNow() Sort by time distance from now.
SortByBoostValue() Sort by relevance score.
SortByRepetition(bool mostOftenFirst) Sort by repetition frequency.
Sort(Func<...> sorter) Custom in-memory sort.
Skip(int count) Skip the first N results.
Take(int count) Limit to N results.
Page(int pageIndex, int pageSize) One page of results. Equivalent to Skip(pageIndex * pageSize).Take(pageSize).
Reverse() Reverse the order of the current result set.

Output

Emit methods

The Emit family sends matched nodes to the output without materializing them into C# objects. It is the default choice in the Shell, where you explore interactively and want the raw nodes in the result panel. In data connectors it configures what graph.QueryAsync() includes in its QueryResults. It can also be returned directly from an endpoint, but endpoints more often materialize the query and project the nodes into a typed record (see In-memory materialization below).

Method Backend Library Notes
Emit(string key) Return matched nodes under a key.
Emit(string key, string[] fields) Return nodes, restricted to specific fields.
EmitCount(string key) Return only the count under a key.
EmitWithEdges(string key) Return nodes with their edge data.
EmitWithEdges(string key, string[] fields) Return nodes with edges, restricted to specific fields.
EmitWithScores(string name) Return nodes with relevance scores.
EmitUIDs(string name) Return only UIDs.
EmitUIDsWithScores(string name) Return UIDs with scores.
EmitSummary(string name) Return a structural graph summary (shell).
EmitNeighborsSummary(string name) Return an edge-type summary (shell).
Similar(string index, int count, float tolerance) Find similar nodes via a named index.
Similar(IndexTypes?, IndexUID, int count) Find similar nodes via backend index reference.
// Library (data connector) — results via QueryResults
var r = await graph.QueryAsync(q =>
    q.StartAt(nameof(Nodes.Device)).Take(10).Emit("N", [nameof(Nodes.Device.Name)]));
var nodes = r.GetEmitted("N");

var r2 = await graph.QueryAsync(q => q.StartAt(nameof(Nodes.Device)).EmitCount("C"));
var count = r2.GetEmittedCount("C");

// Backend (endpoint/shell) — return directly
return Q().StartAt(N.Part.Type).Skip(page * 50).Take(50).Emit();
return Q().StartAt(N.SupportCase.Type).EmitNeighborsSummary();
return Q().EmitSummary();

In-memory materialization (Backend / Endpoints and Shell only)

These methods execute the query and return C# objects. They are not available in the library IQuery. In endpoints this is the usual path: materialize with AsEnumerable() / ToList(), project each ReadOnlyNode into a record, and return that for a stable, typed JSON response.

Method Return Type Description
AsEnumerable() IEnumerable<ReadOnlyNode> Lazy enumerable.
AsEnumerableAsync(int concurrency, int chunkSize, bool stableOrder, CancellationToken) IAsyncEnumerable<ReadOnlyNode> Async lazy enumerable. The token flows automatically through await foreach (... .WithCancellation(token)).
AsEnumerableWithEdgesAsync(int concurrency, int chunkSize, bool stableOrder, CancellationToken) IAsyncEnumerable<ReadOnlyNodeWithEdges> Async lazy enumerable including edge data. Same cancellation behaviour as above.
AsUIDEnumerable() IEnumerable<UID128> Raw UIDs only.
AsTypedUIDEnumerable() IEnumerable<TypedUID128> UID + type pairs.
AsScoredUIDEnumerable() IEnumerable<ScoredUID> UIDs with relevance scores.
First() / FirstOrDefault() ReadOnlyNode First node in the set. First() throws when empty; FirstOrDefault() returns null.
Single() / SingleOrDefault() ReadOnlyNode The one node in the set. Both throw when more than one element is present; Single() also throws when empty, SingleOrDefault() returns null.
ToList() List<ReadOnlyNode> Materialize to a list.
ToListWithEdges() List<ReadOnlyNodeWithEdges> All results with edge data.
ToUIDList() List<UID128> All UIDs as a list.
ToUIDSet() HashSet<UID128> All UIDs as a set.
ToScoredUIDList() List<ScoredUID> All scored UIDs as a list.
ToDictionary<TKey>(keySelector, comparer = null) Dictionary<TKey, ReadOnlyNode> Keyed by a selector over each node.
ToDictionary<TKey, TValue>(keySelector, valueSelector, comparer = null) Dictionary<TKey, TValue> Built from key and value selectors over each node.
ToArrayAsync(int concurrency) Task<ReadOnlyNode[]> Async array materialization.
ToJsonAsync() Task<string> Serialize to JSON string.
ToWorkbookAsync() Task<IWorkbook> Export as a workbook.
ToCsvAsync(Stream stream) Task Write as CSV to a stream.
Count() int Total count.
Any() bool True if any results exist.
CollectUIDs(out List<TypedUID128> uids) IQuery Capture UIDs mid-pipeline.
CollectNodes(out List<ReadOnlyNodeWithEdges> nodes) IQuery Capture nodes mid-pipeline.
// Iterate in endpoint code
foreach (var node in Q().StartAt(N.SupportCase.Type).AsEnumerable())
{
    var summary = node.GetString(N.SupportCase.Summary);
}

// Materialize to list
var recent = Q().StartAt(N.SupportCase.Type)
               .SortByTimestamp(oldestFirst: false)
               .Take(20)
               .ToList();

// Check existence
bool hasOpen = Q().StartWhereString(N.SupportCase.Type, N.SupportCase.Status, "Open").Any();

// Single element
var newest = Q().StartAt(N.SupportCase.Type).SortByTimestamp(oldestFirst: false).FirstOrDefault();

// Index nodes by a key for fast lookup
var byId = Q().StartAt(N.Device.Type)
             .ToDictionary(n => n.GetString(N.Device.Id));

Shell vs. endpoint, side by side

The same query produces shell-friendly output with Emit, or a typed endpoint response when materialized and projected into a record.

Shell — Emit
Q().StartAt(N.Device.Type)
.SortByConnectivity(mostConnectedFirst: true)
.Take(10)
.Emit("devices");
Endpoint — materialize into a record
public record DeviceDto(string Id, string Name, int Cases);

return Ok(
Q().StartAt(N.Device.Type)
.SortByConnectivity(mostConnectedFirst: true)
.Take(10)
.AsEnumerable()
.Select(n => new DeviceDto(
n.GetString(N.Device.Id),
n.GetString(N.Device.Name),
Q().StartAt(n.UID).Out(N.SupportCase.Type).Count()))
.ToList());

Composing asynchronous pipelines

Some entry points are asynchronous — for example StartSearchAsync(...), OutManyAsync(...), and StartAtSimilarTextAsync(...) return a Task<IQuery>. Awaiting after every async step breaks the fluent chain:

var q    = await Q().StartSearchAsync(expr, includeHidden: false);
q        = q.OfType(N.Person.Type).Take(50);
q        = await q.OutManyAsync(2, [N.Company.Type]);
var hits = q.ToList();

The Then(...) extensions (in QueryAsyncExtensions) let you chain sync and async steps onto a pending query and await the whole pipeline once:

var hits = await Q()
    .StartSearchAsync(expr, includeHidden: false)   // Task<IQuery>
    .Then(q => q.OfType(N.Person.Type).Take(50))    // sync step
    .Then(q => q.OutManyAsync(2, [N.Company.Type]))  // async step
    .Then(q => q.ToList());                          // terminal projection

Then(...) is overloaded for synchronous steps (Func<IQuery, IQuery>), asynchronous steps (Func<IQuery, Task<IQuery>>), and terminal projections that return any type — including async ones such as ToArrayAsync() or ToJsonAsync().


Reading Node Properties

In endpoints and shell, nodes returned from AsEnumerable(), ToList(), or inside a Where() predicate expose typed accessors:

Accessor Type
GetString(key) / GetStringList(key) string / IReadOnlyList<string>
GetInt(key) / GetIntList(key) int / IReadOnlyList<int>
GetBool(key) / GetBoolList(key) bool / IReadOnlyList<bool>
GetFloat(key) / GetFloatList(key) float / IReadOnlyList<float>
GetDouble(key) / GetDoubleList(key) double / IReadOnlyList<double>
GetLong(key) / GetLongList(key) long / IReadOnlyList<long>
GetTime(key) / GetTimeList(key) Time / IReadOnlyList<Time>
GetUID128(key) / GetUID128List(key) UID128 / IReadOnlyList<UID128>
node[key] dynamic

In data connectors, EmittedNode (from QueryResults.GetEmitted()) provides GetField<T>(string field).

Use auto-generated constants to avoid string typos:

foreach (var node in Q().StartAt(N.SupportCase.Type).AsEnumerable())
{
    var id      = node.GetString(N.SupportCase.Id);
    var summary = node.GetString(N.SupportCase.Summary);
    var time    = node.GetTime(N.SupportCase.Time);
}

Next Steps

© 2026 Curiosity. All rights reserved.