There are two ways to retrieve records from a knowledge graph. Search allows you to run a free text search against the knowledge graph. Query provides a more nuanced way to retrieve records in the form of an openCypher query executed against the knowledge graph. Both search and query have two modes: non-streaming and streaming. Streaming search and streaming query return results in small chunks allowing the client to begin processing the data returned immediately rather than waiting for the entire result set to be returned before processing. Streaming is faster, more efficient, and will retrieve all matching records, even if the total exceeds the search and query limits set in the service definition. Another benefit of streaming is that the request is encoded which means that it is far smaller than a traditional HTTP GET or JSON POST body. This is especially important when trying to do a query on a very large argument, such as a large set of IDs or intersecting a complex geometry.
Search
Use GraphSearchStreaming to search the properties of both entities and relationships in the graph using the executeSearchStreaming() method. Additional optional search parameters such as specifying a list of entity or relationship types to search within can further constrain the search. The most efficient way to search a knowledge graph is with a streaming search.
// define the search terms
const search = new StreamingSearch({
searchQuery: "solar",
typeCategoryFilter: "both",
// optional parameters
returnSearchContext: false,
start: 1, //start at the first record
num: 200, //return 200 records.
namedTypesFilter: ["Company", "Supplier", "Part"],
globalIdsFilter: ["aiw-924", "esj-856", "snh-571", "hnk-9751", "pyo-7884", "hjl-2541"]
});
// search the knowledge graph
KnowledgeGraphModule.executeSearchStreaming(
// knowledge graph resources
knowledgeGraph,
// search parameters
search
).then((streamingSearchResult) => {
// the result of a streaming search is a readableStream which must be read to access the data.
readStream(streamingSearchResult);
});
Query
Retrieve a more specific subset of entities and relationships or their properties using the executeQueryStreaming() method.
Querying a graph uses the Esri implementation of openCypher query which supports read-only operations.
Query provides much more flexibility in what results are returned from the graph and the structure of the returned data.
For example, consider a graph of a supply chain containing the entities manufacturing Plant
which makes a Part
that is bought by a Supplier
; and there is a relationship produces
between the plant and the
part it produces and a relationship buys
between the supplier and the plant where it purchases the part.
To find the first ten entities of the Supplier
type, you could use the query MATC
. To discover which
parts(p
) are produced with by which plants (pl
), you can match the entities through the produces
relationship with a query such as MATC
.
GraphQueryStreaming also provides the ability to bind parameters to the query such as a bounding box.
For example, to find all the suppliers located in area of Washington DC, use an intersection query and pass a polygon that covers the Washington DC area as a bind parameter.
See GraphQueryStreaming for additional query parameters.
// select all Supplier's that are in the Washington DC area and the parts that they buy
const query = `MATCH (s:Supplier)-[:buys_part]-(p:Part)
esri.graph.ST_Intersects($geom, s.geometry)
RETURN s,p`;
KnowledgeGraphModule.executeQueryStreaming(knowledgeGraph, {
openCypherQuery: query,
bindParameters: {
//bounding box around Washington DC
geom: new Polygon({
rings: [
[
[38, -78],
[39, -78],
[39, -76],
[-38, -76],
[-38, -78]
]
]
})
}
}).then((streamingQueryResult) => {
// the result of a streaming query is a readableStream which must be read to access the data.
readStream(streamingQueryResult);
});
Working with stream results
The result of a streaming search or a streaming query is a GraphQueryStreamingResult which contains a readable stream that must be read using a getReader() function to access the data. The readable stream will return chunks of records until all records have been returned. Each chunk may be processed immediately, such as adding data to a table or adding entities with geometry to a map.
// a function to read the stream returned from the search
const readStream = async (streamingQueryResult) => {
let time = Date.now();
let reader = streamingQueryResult.resultRowsStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log(`Stream completed and closed: ${(Date.now() - time) / 1000} seconds`, value);
break;
}
// begin working with the data from the returned chunk immediately.
// list the parts bought by each supplier
let supplierParts = [];
// each element of the result array will contain one supplier and one part it buys
for (let v in value){
let supplier = value[v][0].properties.Name
let part = value [v][1].properties.Name
if(!(supplier in supplierParts)){
supplierParts[supplier] = [];
}
// collect parts by supplier that buys them
supplierParts[supplier].push(part);
}
// make a table that lists the suppliers and the parts they buy
let table = document.getElementById('supplierTableBody');
for (let supplier in supplierParts){
table.appendChild(`<tr><td>${supplier}</td><td>${supplierParts.supplier.join(', ')}</td>`);
}
// Entities that have geometry can be drawn on the map
addToMap(value);
// Since
}
// if there is an error in returning the stream or the stream is aborted
} catch (err) {
if (err.name === "AbortError") {
console.log("Request aborted as expected");
} else {
throw err;
}
}
};
// function to add entities with geometry to a map
function addToMap(records) {
let features = [];
//extract the geometry from the returned results and add it to a feature layer
for (let i = 0; i < records.length; i++) {
let item = records[i][0];
let props = item.properties;
// if the returned item contains geometry,
//extract the geometry information to create features
if ("shape" in props) {
features.push({
geometry: {
type: props.shape.type,
x: props.shape.longitude,
y: props.shape.latitude
},
attributes: props
});
}
}
// create feature layer
let featureLayer = new FeatureLayer({
source: features,
renderer: {
type: "unique-value", // autocasts as new UniqueValueRenderer()
defaultSymbol: {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
size: 2,
color: "#009816",
outline: null
}
}
});
map.add(featureLayer);
}