Graph Data Model
With its graph API, Siren Federate allows to view existing Elasticsearch indices as property graphs.
A property graph is a data model where pairs of entities (i.e., vertices) are connected by directed relations (i.e., edges). Entities and relationships are associated to a label and can have properties.
|
See Properties on Relations in order to model an edge associated with properties, |
{
"model": {
"label": "<NAME>", (1)
"entities": [], (2)
"relations": [] (3)
}
}
| 1 | The name of a graph, that is also used to reference the corresponding property graph inside GQL queries (see GQL Syntax). |
| 2 | The set of entities to be considered for this graph. |
| 3 | The set of relations between entities. |
Siren Federate requires that a model is provided as part of its graph API requests. This allows to view existing Elasticsearch documents as nodes of a property graph, while edges are joins between the indices of such documents.
The Elasticsearch indices and documents below are used to illustrate the concepts of entities and relations described further in this section.
PUT /people
{
"mappings": {
"properties": {
"id": { "type": "integer" },
"firstName": { "type": "keyword" },
"age": { "type": "integer" },
"knows": { "type": "integer" }
}
}
}
PUT /forums
{
"mappings": {
"properties": {
"id": { "type": "integer" },
"title": { "type": "keyword" },
"members": { "type": "integer" },
"messages": { "type": "integer" }
}
}
}
PUT /messages
{
"mappings": {
"properties": {
"id": { "type": "integer" },
"content": { "type": "text" },
"creator": { "type": "integer" }
}
}
}
POST _bulk
{ "index" : { "_index" : "people" } }
{ "id": 1, "firstName": "John", "age": 25, "knows": 2 }
{ "index" : { "_index" : "people" } }
{ "id": 2, "firstName": "Paul", "age": 42 }
{ "index" : { "_index" : "forums" } }
{ "id": 1, "title": "BeatlesFans", "members": [1, 2], "messages": 1 }
{ "index" : { "_index" : "messages" } }
{ "id": 1, "content": "Hello, world!", "creator": 2 }
Such a collection of documents could be seen as a property graph like the one below, featuring two nodes with Person label, one node with Forum label, and one node with Message label. One person ("John") knows the other ("Paul") and both are members of the "BeatlesFans" forum, as shown by the hasMember relations. The forum is the containerOf a "Hello, World!" message which was written by "Paul" as indicated by the hasCreator relation.
┌──────────────────┐
│ Person │
│ │
│ id: 1 ◄─────────hasMember───────────┐
│ firstName: John │ │
│ │ │
└─────────┬────────┘ │
│ │
│ │
│ ┌─────────┴─────────┐ ┌───────────────────┐
│ │ Forum │ │ Message │
│ │ │ │ │
knows │ id: 1 ├─────containerOf────► id: 1 │
│ │ title: BeatlesFans│ │ content: "Hello, │
│ │ │ │ World!"│
│ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
│ │ │
│ │ │
┌─────────▼────────┐ │ │
│ Person │ │ │
│ │ │ │
│ id: 2 ◄─────────hasMember───────────┘ │
│ firstName: Paul │ │
│ │ │
└─────────▲────────┘ │
│ │
└──────────────────────────────────hasCreator───────────────────────────────────┘
In the context of our example, the model would look like the following:
{
"model": {
"label": "my-graph",
"entities": [
{ "label": "Person", "indices": [ "people" ] },
{ "label": "Forum", "indices": [ "forums" ] },
{ "label": "Message", "indices": [ "messages" ] }
],
"relations": [
{
"label": "knows",
"from": "Person",
"to": "Person",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "knows", "to_key": "id" }
]
}
},
{
"label": "hasMember",
"from": "Forum",
"to": "Person",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "members", "to_key": "id" }
]
}
},
{
"label": "containerOf",
"from": "Forum",
"to": "Message",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "messages", "to_key": "id" }
]
}
},
{
"label": "hasCreator",
"from": "Message",
"to": "Person",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "creator", "to_key": "id" }
]
}
}
]
}
}
In our example, we can see that three classes of entities can occur in our property graph, i.e., those labelled as "Person", "Forum", or "Message". "Person" entities are represented by documents coming from the "people" index, while "Forum" entities come from the "forums" index, and "Message" entities from the "messages" index.
Similarly, we can see that three classes of relations exist in our example, i.e., those labelled "knows", "hasMember", and "containerOf". For instance, a "hasMember" relations will connect a "Forum"-labelled entity with a "Person"-labelled entity if their corresponding documents satisfy a set of join conditions. In our example, the only condition is that one values of the "members" property from the "Forum" entity is equal (i.e., EQ) to one of the values from the "id" property of the "Person" entity. Similar considerations applies to the other classes of relations from our example.
Entities
An entity represents a node in the graph with a given label. An entity is defined from a set of indices, and each document represents a particular node of the graph
Below is the entity definition for a Person in the example graph whose age is at least 18 years old.
{
"label": "Person", (1)
"indices": [ "people" ], (2)
"request": { (3)
"query": {
"range": {
"age": {
"gte": 18
}
}
}
}
}
| 1 | The label of this entity. |
| 2 | The indices where such entities are stored. An index can be a pattern, e.g., people*. |
| 3 | An optional request object which defines a query to filter the set of possible entities. |
|
It is possible to have several entities defined with the same label. |
Relations
A relation represents a directed edge in the graph with a given label. The relation is defined as the join from an index to the another based on some conditions.
Below is the relation definition for the edge hasMember in the example graph connecting a Forum to a Person.
{
"label": "hasMember", (1)
"uid": "forum_has_member", (2)
"from": "Forum", (3)
"to": "Person", (4)
"conditions": { (5)
"must": [ (6)
{
"op": "EQ", (7)
"from_key": "members", (8)
"to_key": "id" (9)
}
]
}
}
| 1 | The label of this relation. |
| 2 | The optional uid of this relation. If omitted, the label is used as the uid. The uid must be unique across all relations in the model. |
| 3 | The label of the entity that this relation connects from. |
| 4 | The label of the entity that this relation connects to. |
| 5 | The conditions that define how two entities relates to each other. |
| 6 | A boolean clause for the set of conditions. Only must is currently supported. |
| 7 | The type of condition this represents, here EQ is the equality between two fields. |
| 8 | The field name in the entity referenced in from. The field must appear in one of the indices of that entity. |
| 9 | The field name in the entity referenced in to. The field must appear in one of the indices of that entity. |
|
It is possible to have several relations defined with the same label. In that case, the |
Properties on Relations
A relation can be associated with properties if it is backed by an index. Indexed documents contain the properties of the relation, in addition to foreign keys pointing to the adjacent entities at the source and destination of the relation.
The approach taken to model such a relation is to reify the relation: intermediate relation and entity classes are declared, creating an entity for the relation.
The intermediate classes can then be hidden from the GQL query, so that they do not match graph patterns: query results are not "polluted" with those intermediate classes.
|
Consider this example, and that intermediate relations (
The direct connection
Given the model, that second pattern is actually the direct connection that got first evaluated.
In order to avoid such confusing matching patterns, the |
Syntax
The reification of such a relation is achieved by declaring four classes:
-
An intermediate entity class describing the backing index, declared using the Entities syntax.
-
An intermediate relation class connecting the source entity class to (1), declared using the Relations syntax.
-
An intermediate relation class connecting the destination entity class to (1), declared using the Relations syntax.
-
A relation class that models the direct connection between the source and destination entity classes.
The intermediate classes are hidden by adding the option "hidden": true to the classes definitions.
The 4th relation class refers to the intermediate classes, thus making a direct between two entities.
{
"label": "calls",
"from": "Person",
"to": "Person",
"references": [ (1)
"makes", (2)
"isReceivedBy" (3)
]
}
| 1 | A list of referenced relations, each identified by the uid of the referenced relation. |
| 2 | The uid of the relation that connects entities from Person to Call. |
| 3 | The uid of the relation that connects entities from Call to Person. |
Example
For example, imagine to extend our running example by connecting ''Paul'' and ''John'' with a new calls relation. This relation represents a phone call and has some properties like the date of the call and its duration.
┌──────────┐ ┌──────────┐
│ Person │ │ Person │
│ │ │ │
│name: Paul├───────calls────────►name: John│
│ │ date: 10-12-2025 │ │
└──────────┘ duration: 3mins └──────────┘
To represent this property graph with Siren Federate, the following classes are declared for the calls relation:
-
A Call entity containing the properties of the call.
-
The Call entity must then be connected to "Paul" and "John" with two new relations (e.g., makes and isReceivedBy) that preserve "Paul" and "John" as source and destination of the phone call.
-
The calls relation is declared as previously detailed.
┌────────────────┐
│ Call │
│ │
┌──────────►date: 10-12-2025├──────────┐
│ │duration: 3mins │ │
│ │ │ │
│ └────────────────┘ │
│ │
makes isReceivedBy
│ │
┌────┴─────┐ ┌────▼─────┐
│ Person │ │ Person │
│ │ │ │
│name: Paul│ │name: John│
│ │ │ │
└──────────┘ └──────────┘
To conclude, the model of the graph with a direct relation between the Person entities would be defined as follows:
{
"model": {
"label": "my-graph",
"entities": [
{ "label": "Person", "indices": [ "people" ] },
{
"hidden": true, (1)
"label": "Call", "indices": [ "phonecalls" ]
}
],
"relations": [
{
"hidden": true, (1)
"label": "makes",
"from": "Person",
"to": "Call",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "id", "to_key": "caller_id" }
]
}
},
{
"hidden": true, (1)
"label": "isReceivedBy",
"from": "Call",
"to": "Person",
"conditions": {
"must": [
{ "op": "EQ", "from_key": "callee_id", "to_key": "id" }
]
}
},
{ (2)
"label": "calls",
"from": "Person",
"to": "Person",
"references": [
"makes",
"isReceivedBy"
]
}
]
}
}
| 1 | Intermediate classes are hidden and won’t be matched against a query patterns. |
| 2 | The calls relation refers to the hidden relations makes and isReceivedBy by their uid. |