Investigation into bringing together graph and reactive computing. This is the third post in the series and deals with the platform’s data model.
Everything in ReactGraph is a node. A node has an identity and stores a value. An edge is also a node, which means it too has identity and can store a value. Furthermore, an edge describes a directed path from a source to a destination node. The edge also has a predicate that gives some meaning to the path. An edge can be marked as “bidirectional”, which means that it also represents the destination-to-source reverse path.
The above shouldn’t be of surprise to anyone who has dealt with the representation of information using graphs. The Neo4j property graph data model is very similar (with the exception of “an edge is also a node” characteristic). Facebook’s graph data model also exhibits similar properties to that of Neo4j.
public interface INode { string Id { get; } T Value<T>(); } public interface IEdge : INode { string SourceId { get; } string DestinationId { get; } string Predicate { get; } bool IsBidirectional { get; } }
So that the can act as the source of destination of other edges. This way there is no need to reify a relationship, if the application designers don’t wish to do so. The data model is flexible. For a discussion on this see my post from 2013 entitled “On Graph Data Model Design – Relationships”.
Here’s an example I just discussed with Jim. There are multiple ways to model the data in an application of domain. One might decide to reify relationships by creating extra nodes. The best approach is the one that makes sense for the specific application.
(id: jim) -> (id: foo, predicate: watched, on: 10/10/17) -> (id: SW-IV, title: "StarWars Episode IV") (id: savas) -> (id: bar1, predicate: liked) -> (id: foo) (id: savas) -> (id: bar1, predicate: shared, on: 10/11/17) -> (id: foo)
With the graph data representation in place, we move to the basic abstractions necessary to support both “pull” graph querying and “push” continuous querying (reactive computing). The interface for a node is expanded with four additional properties.
public interface INode { string Id { get; } T Value<T>(); IQueryable<IEdge> IncomingEdges { get; } IQueryable<IEdge> OutgoingEdges { get; } IQbservable<IValueEvent<INode>> ValueChanges { get; } IQbservable<IEdgeEvent> EdgeChanges { get; } }
The IncomingEdges
and OutgoingEdges
let us create query expressions that can be submitted for evaluation to the data provider supporting the respective IQueryable
interfaces. The properties represent the collection of incoming and outgoing edges respectively.
The ValueChanges property allows the construction of Rx expression trees that can be submitted for continuous evaluation at the reactive computing data provider supporting the implementation of the INode
interface. The ValueChanges
represents the stream of changes to the value that the INode
holds.
Similarly, the EdgeChanges
allows the construction of Rx expression trees that can be submitted for continuous evaluation at the reactive computing data provider supporting the implementation of the INode
interface. The EdgeChanges
represents the stream of changes to the incoming and outgoing edges (e.g. when a new edge is created or deleted with the specific INode
as a source of destination).
Please note that it’s an open design issue whether these properties should really be part of the INode
interface. They can easily be provided as extension methods on INode
by an implementor.
One could interact with the ReactGraph active computing store through an implementation of the following interface.
public interface IGraph { IQueryable<INode> Nodes { get; } IQueryable<IEdge> Edges { get; } IQbservable<IValueEvent<INode>> ValueChanges { get; } IQbservable<IEdgeEvent> EdgeChanges { get; } Task<INode> GetNode(string id); Task<IEdge> GetEdge(string id); Task<INode> NewNode<T>(string id, T value = default(T)); Task<IEdge> NewEdge<T>( string id, string sourceId, string predicate, string destinationId, T value = default(T), bool bidirectional = false); Task Update<T>(string id, T value); Task Delete(string id); }
The interface should be self-explanatory.
INode
interfaceDuring the implementation stage of the interfaces, it became aparent that INode
could use a couple of more things. Again, it’s an open design issue whether these should be part of the core interface.
public interface INode { string Id { get; } T Value<T>(); Type ValueType { get; } IGraph Graph { get; } IQueryable<IEdge> IncomingEdges { get; } IQueryable<IEdge> OutgoingEdges { get; } IQbservable<IValueEvent<INode>> ValueChanges { get; } IQbservable<IEdgeEvent> EdgeChanges { get; } }
Finally, for completeness, here are the event-related interfaces. It’s an open design issue whether these two interfaces could be consolidated somehow.
public interface IValueEvent<out T> { ChangeOperation Operation { get; } T Item { get; } } public interface IEdgeEvent { INode Node { get; } IEdge Edge { get; } ChangeOperation Operation { get; } } public enum ChangeOperation { New, Delete, Update, }
With the basic abstractions in place, we are now ready to start exploring what we can do with these. Then we will move to the implementation of the “(re)active graph store”.
I will try to make the code available on github as soon as possible.
I am embarking on a side project that involves memory and multimodal understanding for an…
I was in Toronto, Canada. I'm on the flight back home now. The trip was…
The BBC article "How we fell out of love with voice assistants" by Katherine Latham…
Like so many others out there, I played a bit with ChatGPT. I noticed examples…