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:

Click on each keyword for more information (inputs, restrictions, etc.)

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",
      ...
    }
  }
]