Using the Seraph.Query API

Notation

There is two entities in Neo4j graph database: Node and Relationship.
To be as close as possible to Cypher, we used the following notations in queries.

Nodes are defined between {}

{u, GraphApp.Blog.User, %{firstName: "John"}}
 ^       ^                 ^
 |       |                 |
 |       |                 -- properties 
 |       -- schema
 -- variable

All variations are valid depending on the query keyword.

Relationship are defined betwen []

[{u}, [rel, GraphApp.Blog.Relationship.Wrote, %{at: ...}], {p}]
  ^     ^              ^                           ^        ^
  |     |              |                           |        |
  |     |              |                           |        -- end_node
  |     |              |                           -- properties 
  |     |              -- schema
  |     -- variable
  -- start_node

All variations are valid depending on the query keyword.

About keywords

Available keywords

For now, the available keywords are:

Keyword order

Keywords can be used in any order in order to give flexibility when writing queries.
But only match, create and merge can be used to start a query.

Query syntax

There are to ways to write query: kewyord syntax and macro syntax.

Keyword syntax

import Seraph.Query

query = match [{u, User}],
  where: [u.firstName == "John"],
  return: [u]

GraphApp.Repo.all(query)

Macro syntax

import Seraph.Query

match([{u, User}])
|> where([u.firstName == "John"])
|> return([u])
|> GraphApp.Repo.query()

Both syntaxes required variables to be pinned using ^:

import Seraph.Query

first_name = "John"
match([{u, User}])
|> where([u.firstName == ^first_name])
|> return([u])
|> GraphApp.Repo.query()

Query options

:with_stats

Sometimes, a query result is not enough, we need to get its summary, especially when dealing with create, merge, set or delete operations.
To get the summary, just use the option with_stats: true.

Examples:

# default - with_stats: false
import Seraph.Query
create([{u, GraphApp.Blog.User}])
|> set([u.uuid = "0223a553-a474-46e1-8798-805411827b20", u.firstName = "Jim", u.lastName = "Cook"])
|> return([u])
|> GraphApp.Repo.one()

# Result
%{
  "u" => %GraphApp.Blog.User{
    __id__: 1,
    __meta__: %Seraph.Schema.Node.Metadata{
      primary_label: "User",
      schema: GraphApp.Blog.User
    },
    additionalLabels: [],
    uuid: "0223a553-a474-46e1-8798-805411827b20",
    firstName: "Jim",
    lastName: "Cook",
    ...
  }
}


# with_stats: true
create([{u, GraphApp.Blog.User}])
|> set([u.uuid = "1f178997-5f32-4c1b-acc7-0079f7eea9c6", u.firstName = "Jane", u.lastName = "Doe"])
|> return([u])
|> GraphApp.Repo.one(with_stats: true) 

# Result
%{
  results: %{
    "u" => %GraphApp.Blog.User{
      __id__: 42,
      __meta__: %Seraph.Schema.Node.Metadata{
        primary_label: "User",
        schema: GraphApp.Blog.User
      },
      additionalLabels: [],
      uuid: "1f178997-5f32-4c1b-acc7-0079f7eea9c6",
      firstName: "Jane",
      lastName: "Doe",
      ...
    }
  },
  stats: %{"labels-added" => 1, "nodes-created" => 1, "properties-set" => 3}
}

:relationship_result

Relationship struct holds both start node and end node data and this can be quite a lot of data when retrieving numerous relationships. Also sometimes, we just want the complete relationship without having to return the start and end nodes from the query.
:relationship_result address both this issue with 3 values:

  • :contextual (default) - The relationship will be built only using the query result, meaning that if the nodes aren't part of the return, start and end node will be empty
  • :no_nodes - start and end node data won't be filled up, even if they are present in query return
  • :full - start and end node data will be filled up, even if they are not present in query return

Examples: Let's create a relationship first

match([
    {u, GraphApp.Blog.User, %{firstName: "Jim"}},
    {u2, GraphApp.Blog.User, %{firstName: "Jane"}}
])
|> merge([{u}, [GraphApp.Blog.Relationship.NoProperties.Follows], {u2}])
|> GraphApp.Repo.execute(with_stats: true)

# Result
{:ok, %{results: [], stats: %{"relationships-created" => 1}}}

Now we build query

query = match [
    {u, GraphApp.Blog.User, %{firstName: "Jim"}},
    {u2, GraphApp.Blog.User, %{firstName: "Jane"}},
    [{u}, [rel, GraphApp.Blog.Relationship.NoProperties.UserToUser.Follows], {u2}]
],
return: [u, rel]

relationship_result: :contextual (default value)

GraphApp.Repo.all(query)

# Result
# end_node is nil because it is not part of query result
[
  %{
    "rel" => %GraphApp.Blog.Relationship.NoProperties.UserToUser.Follows{
      __id__: 42,
      __meta__: %Seraph.Schema.Relationship.Metadata{...},
      end_node: nil,
      start_node: %GraphApp.Blog.User{
        __id__: 1,
        __meta__: %Seraph.Schema.Node.Metadata{
          primary_label: "User",
          schema: GraphApp.Blog.User
        },
        additionalLabels: [],
        uuid: "0223a553-a474-46e1-8798-805411827b20",
        firstName: "Jim",
        lastName: "Cook",
        ...
      },
      type: "FOLLOWS"
    },
    "u" => %GraphApp.Blog.User{
      __id__: 1,
      __meta__: %Seraph.Schema.Node.Metadata{...},
      additionalLabels: [],
      uuid: "0223a553-a474-46e1-8798-805411827b20",
      firstName: "Jim",
      lastName: "Cook",
      ...
    }
  }
]

relationship_result: :no_nodes

GraphApp.Repo.all(query, relationship_result: :no_nodes)

# Result
# Both start_node and end_node are nil, even if the start_node (u) is part of the query result
[
  %{
    "rel" => %GraphApp.Blog.Relationship.NoProperties.UserToUser.Follows{
      __id__: 42,
      __meta__: %Seraph.Schema.Relationship.Metadata{
        schema: GraphApp.Blog.Relationship.NoProperties.UserToUser.Follows,
        type: "FOLLOWS"
      },
      end_node: nil,
      start_node: nil,
      type: "FOLLOWS"
    },
    "u" => %GraphApp.Blog.User{
      __id__: 1,
      __meta__: %Seraph.Schema.Node.Metadata{...},
      additionalLabels: [],
      uuid: "0223a553-a474-46e1-8798-805411827b20",
      firstName: "Jim",
      lastName: "Cook",
      ...
    }
  }
]

relationship_result: :full

GraphApp.Repo.all(query, relationship_result: :full)

# Result
# start_node and end_node are filled up, even if end_node (u2) is not part of query result
[
  %{
    "rel" => %GraphApp.Blog.Relationship.NoProperties.UserToUser.Follows{
      __id__: 42,
      __meta__: %Seraph.Schema.Relationship.Metadata{...},
      end_node: %GraphApp.Blog.User{
        __id__: 42,
        __meta__: %Seraph.Schema.Node.Metadata{...},
        additionalLabels: [],
        uuid: "1f178997-5f32-4c1b-acc7-0079f7eea9c6",
        firstName: "Jane",
        lastName: "Doe",
      },
      start_node: %GraphApp.Blog.User{
        __id__: 1,
        __meta__: %Seraph.Schema.Node.Metadata{
          primary_label: "User",
          schema: GraphApp.Blog.User
        },
        additionalLabels: [],
        uuid: "0223a553-a474-46e1-8798-805411827b20",
        firstName: "Jim",
        lastName: "Cook",
        ...
      },
      type: "FOLLOWS"
    },
    "u" => %GraphApp.Blog.User{
      __id__: 1,
      __meta__: %Seraph.Schema.Node.Metadata{...},
      additionalLabels: [],
      uuid: "0223a553-a474-46e1-8798-805411827b20",
      firstName: "Jim",
      lastName: "Cook",
      ...
    }
  }
]