Writing queries
Introduction
In the following examples, we will utilize the domain model presented by Kelvin Lawrence in their book PRACTICAL GREMLIN: An Apache TinkerPop Tutorial. The book provides insights into a fictional airline network graph, where airports are represented as vertices and flights between airports as edges. You can find the document here. With this domain model as our foundation, we will explore various Gremlinq queries to extract valuable insights from the airline network graph.
We'll assume the existence of two POCOs, Airport
and Route
, representing airports and routes
between them and some instances of these classes:
public class Airport
{
public string Code { get; set; }
public string Name { get; set; }
}
public class Route
{
public double Dist { get; set; }
}
// Define a few airports
var airports = new List<Airport>
{
new Airport { Code = "JFK", Name = "John F. Kennedy International Airport" },
new Airport { Code = "LAX", Name = "Los Angeles International Airport" },
new Airport { Code = "ORD", Name = "Chicago O'Hare International Airport" },
new Airport { Code = "DFW", Name = "Dallas/Fort Worth International Airport" },
new Airport { Code = "MIA", Name = "Miami International Airport" }
};
// Define connections between airports along with their distances in miles
var connections = new Dictionary<(string, string), double>
{
{ ("JFK", "LAX"), 2475 },
{ ("JFK", "ORD"), 740 },
{ ("JFK", "DFW"), 1370 },
{ ("LAX", "ORD"), 1745 },
{ ("ORD", "DFW"), 802 },
{ ("ORD", "MIA"), 1197 },
{ ("DFW", "MIA"), 1121 },
{ ("MIA", "JFK"), 1090 }
};
Populate the graph
In the upcoming code, we'll illustrate the process of populating an airport network graph, connecting various airports with routes between them.
Add connections between airports
Alternative way of adding elements
There is an overload of AddE
(and AddV
) that takes no argument. Instead, properties of the newly
added element can be added after the fact:
Simple queries
Retrieve names of airport vertices
Which airport have their name starting with the letter 'J' ?
Walk the graph
Find all destination airports from JFK
Find all airports connected to JFK within 1500 miles
Find all airports that can reach JFK
Find all airports that are reachable from JFK by taking two flights
Using sub-queries
Find all airports that are reachable from JFK by taking one or two flights
var results = await g
.V<Airport>()
.Where(a => a.Code == "JFK")
.Union(
__ => __
.Out<Route>(),
__ => __
.Out<Route>()
.Out<Route>())
.OfType<Airport>()
.ToArrayAsync();
Simpler:
Find all destinations from JFK that have a connection to Heathrow
Orderings
This section marks the first introduction of a special of a sub-domain-specific language (sub-DSL), enabling developers to ensure that only sensible methods are within scope. This sub-DSL streamlines the programming process by providing a focused and tailored set of methods relevant to the specific context by providing a builder instance.
Order all airports lexically by their code
Order all airports by their longest route
Order all airports by their longest route, then lexically by their code
Limiting results
Get the five airports with the longest routes
Using Range instead
Skip the first five results
Take only the latter five
Step labels
Avoid returning to the departure airport right away
Projections
Project to value tuples
Project to value tuples with sub-query
Project to dynamics
Project to dynamics with explicit names
Project to custom data structure instances
Aggregates
Fold
Sum
Max
Groupings
Building upon the previously introduced concept of sub-DSL, building groups is another case where such a sub-DSL is employed:
Group airports by number of routes
Project to specific group values
The previous example illustrated a grouping by numbers without explicitly specifying a value, resulting in the projection of values to the airports themselves. However, it's worth noting that we can also project values specifically to the airport codes:
Loops
Again, there is a specific DSL for loops that, through a builder, lets the user chain reasonable combinations of the
Emit
, Repeat
, Until
and Times
operators:
Where can we go with three flights?
Unroll all the paths from JFK that don't become cyclic
Trees
A tree in the TinkerPop sense is essentially just a wrapper for a Dictionary<TRoot, TTree>
where TRoot
can be
anything (scalars, entities) and TTree
is, again, a Tree. So a tree in this sense can be seen as having many
root nodes where each of the root nodes maps to a subtree.
Gremlinq defines the non-generic ITree
-marker interface and two derived classes:
Tree<TNode>
is a tree where each node in the tree is of typeTNode
Tree<TRoot, TTree>
is a tree where the roots are of typeTRoot
and the subtrees are of typeTTree
.
There are currently 3 overloads of the Tree
-operator in Gremlinq:
-
Tree()
will map to aTree<object>
. This is the quickest way to collect the traversers and their paths into a tree structure. Note that although the signature only defines the nodes to be of typeobject
, upon deserialization, Gremlinq will usually be able to recognize elements (vertices and edges) and create instances accordingly. -
Tree<TNode>()
will deserialize to a tree where all the nodes are known to be of typeTNode
. -
Precise type information of the tree nodes might be desired for later processing. Unfortunately, Gremlinq can't record all the types encountered on a path in its type system, so this information is not statically available. It can, however, be provided throgh call a
Tree
-overload that provides a special builder-DSL:C#var tree = await g .V<Airport>() .OutE<Route>() .InV<Airport>() .Tree(_ => _ .Of<Airport>() .Of<Route>() .Of<Airport>()) .FirstAsync();
It's also possible to map to members which will change the node types in the resulting tree