This document describes the methods available in the Graph object, as published in the Curiosity.Library NuGet package. You can see an example of using this library to write a data connector in our space library demo repository.

Key Concepts

Node objects

The Node return type used by the library is a wrapper around either a known Node Type + Key pair, or the equivalent Unique Identifier (UID) used in the system. Both can be usually used interchangeable, and one can create a new Node object as follows:

var nodeA = Node.UID("MyNodeUID");
var nodeB = Node.Key("MyNodeType", "MyNodeKey");

Methods that create or change nodes will always return a Node object you can use in subsequent calls (such as adding edges or aliases, restricting access or deleting it).

Synchronous and Asynchronous Methods & Batch Session

The graph object operates around batches of operations that are accumulated and committed asynchronously to the server. Once you open a Graph connection, methods that change the graph (like adding or deleting nodes or edges) will immediately return until reaching the batch size, and only then submit the batch to the server. Once the Graph object is disposed, it will also commit any remaining operations - thus it is important to always use the using statement to ensure the graph object is disposed properly.

All methods that are asynchronous (i.e. have the Async suffix and return a Task or Task<T>) will usually execute immediately and need to be awaited using the await operator.

using (var graph = Graph.Connect("server URL", "API token", "My Connector"))
{
graph.AddNode(...);
// ... more graph operations here

await graph.LogAsync("At 50%"); //this happens immediately

// ... more graph operations here
graph.AddNode(...); //Might commit here due to hitting batch size
// ... more graph operations here

} //Final commit of pending operations

Idempotent Connectors

We recommend when designing a data connector to create it in a way that operations are idempotent - i.e. no matter how many times the data connector is run, or the order of the operations, the outcome is the same.


Method List

For the examples below, we will use the following example class that has been marked with the Node attribute:

[Node]
public class Person
{
[Key] public string FullName { get; set; }
[Property] public float Height { get; set; }
[Property] public DateTime Birthday { get; set; }
}

Schema

While you can always create your node and edge schemas using the front end, sometimes it is easier to do so programmatically. For that, the graph object exposes two methods:

//Creates the Person schema as above
await graph.CreateNodeSchemaAsync<Person>();

//Creates the Edge types used by the examples below
await graph.CreateEdgeSchemaAsync("BrotherOf", "SisterOf", "Visited");

When creating Node Schemas, the graph will throw an error if your class definition changed from what is in the graph. To overwrite any changes, you can pass the optional attribute overwrite: true - but be careful as depending on the type of changes, this might result in all your data of the given node type being deleted.

[Node]
public class Person
{
[Key] public string FullName { get; set; }
}


//Creates the Person schema with only the FullName field
await graph.CreateNodeSchemaAsync<Person>();

//sometime in the future, you changed the Person schema to:

[Node]
public class Person
{
[Key] public string FullName { get; set; }
[Property] public float Height { get; set; }
[Property] public DateTime Birthday { get; set; }
}

// Calling this will update the existing schema on the graph
// As this is a supported update path, there will be no data loss.
// But any existing data will have a default value (i.e. Height = 0 and Birthday = 1970/01/01 (Unix Epoch)
await graph.CreateNodeSchemaAsync<Person>(overwrite: true);

Adding and Updating Nodes

There are three types of methods that can add nodes to the graph. We recommend the usage of strongly typed classes that reflect your node schemas, but you can also pass an object that serializes to the same JSON as your schema.

var john = new Person() { FullName = "John Doe", Height = 1.72f, Birthday = new DateTime(1972, 8, 23) };

graph.AddOrUpdate(john); // Add or update the node on the graph
graph.Update(john); // Updates the node ONLY if it exists
graph.TryAdd(john); // Adds the node ONLY if it doesn't exist

Note: For most usages, the preferred method to use is graph.AddOrUpdate.

Deleting Nodes

To delete a node, pass either a Node object, or a class that has the Node attribute added to it:

var john = new Person() { FullName = "John Doe", Height = 1.72f, Birthday = new DateTime(1972, 8, 23) };

graph.Delete(john); // Delete using the strongly typed object
graph.Delete(Node.Key("Person", "John Doe")); // Delete using Type + Key
graph.Delete(Node.UID("3euj2tQnZ6DZWtBnreNHa9")); // Delete using the UID

Adding Edges

To link two nodes in the graph, you can use the following methods:

var john = new Person() { FullName = "John Doe", Height = 1.72f, Birthday = new DateTime(1972, 8, 23) };

var anna = new Person() { FullName = "Anna Doe", Height = 1.67f, Birthday = new DateTime(1980, 1, 12) };

//Create the nodes in the graph:
graph.AddOrUpdate(john);
graph.AddOrUpdate(anna);

//This creates the single edge John -> BrotherOf -> Anna
graph.Link(john, anna, "BrotherOf");

//This creates both John -> BrotherOf -> Anna and Anna -> SisterOf -> John
graph.Link(john, anna, "BrotherOf", "SisterOf");

Edges are usually unique (i.e. uniquely identified by the relationship From + To + Edge Type). If you would like to add non-unique edges, add unique: false to the calls:

//This creates an edge John -> Visited -> Anna
graph.Link(john, anna, "Visited", unique: false);

//This creates another edge John -> Visited -> Anna
graph.Link(john, anna, "Visited", unique: false);

Note: It is important to not mix the usage of unique for a given edge type, as it is only enforced when adding the new edge. In most scenarios, you would like to use the default unique edges option.

You can also pass a Node object to link existing nodes in the graph:

graph.Link(Node.Key("Person", "John Doe"), Node.Key("Person", "Anna Doe"), "BrotherOf");

Deleting Edges

To remove an existing edge from the graph, you can use the Unlink method:

//This removes the edge John -> Visited -> Anna
graph.Unlink(john, anna, "BrotherOf");

//This removes both John -> BrotherOf -> Anna and Anna -> SisterOf -> John
graph.Unlink(john, anna, "BrotherOf", "SisterOf");

You can also pass a Node object, similar to the Link method.

Aliases

Aliases are used for the linking of entities captured by the NLP pipelines to nodes in the graph. They provide a way of adding "nicknames" for your nodes, so that they can multiple forms of referring to them in text are linked correctly in the graph.

graph.AddAlias(john, Language.Any, "Mr. John Doe", ignoreCase: false);
graph.AddAlias(john, Language.Any, "Johnny Doe", ignoreCase: false);

To remove all existing aliases before adding new ones (for example if your aliases definition changed), use the ClearAliases method first:

graph.ClearAliases(john); //Deletes any previous existing aliases
graph.AddAlias(john, Language.Any, "Johnny Doe", ignoreCase: false);

Note: You shouldn't always clear and add new aliases, as this will result in the NLP models continuously being recreated due to changes in the graph. Use ClearAliases only in the case where you really need to delete all previous existing aliases, and then remove the call afterwards,

Logging

The graph object provides an easy way to log some information of your data connector progress, execution status, and any errors, so that it can be accessed in the Data Sources hub. As these methods are asynchronous, don't forget to await for them.

await graph.LogAsync("Starting data ingestion");
// some operations
await graph.LogAsync("50 %");
// some more operations
await graph.LogAsync("Finished data ingestion");

if(error)
{
await graph.LogErrorAsync("Some operations failed");
}

Uploading Files

In order to upload a file to the system, you just need to pass it as a Stream. Files uploaded using the connector are uniquely identified by the combination of source name and file name (file name can include folders in it using either the Unix '/' or Windows '\' folder separator - and will be normalized in the server to '/').

using(var file = File.OpenRead("c:\path\to\my file.pdf"))
{
var fileNode = await graph.UploadFileAsync(file, "path\to\my file.pdf", "Local Files");
}

Query Graph

You can use the following method to load specific fields from the graph:

//Gets all names for all persons in the graph
var names = await graph.Map("Person", "FullName");

// names is a dictionary of Dictionary<Node, string>

Note: We're currently reworking the query interface exposed by the C# library. There will be more functionality coming soon here.

Did this answer your question?