EctoNeo4j - an Ecto adapter for Neo4j

Hi Elixir world,
last night, bolt_sips v2 was released by @Florin, which means that Neo4j Entreprise Edition is now covered by the driver.
Using bolt_sips can be rough sometimes, especially in a Phoenix project when you wan to use Ecto… and can’t.
That’s the purpose of EctoNeo4j, have (almost) all the Ecto features, from Shema to Repo work with Neo4j.
You can find the library here: https://hex.pm/packages/ecto_neo4j

You can now enjoy your Repo.*, have your migrations work, etc. But remember that Ecto is designed for relational database and Neo4j is a graph database then not all Ecto features work. So don’t expect join, etc. to work.
Additionnally, Ecto.Query is sql-oriented then only simple cypher query are supported (i.e on one node or one label).
See docs for more information.

Hope this library will help you.
And thanks to @Florin and @mschae for their work on bolt_sips and boltex!

12 Likes

@domvas - thank you so much for the shoutout, and for your feedback and support. We had lots of users asking us for an Ecto driver, for Neo4j, and I think your project will fill up that gap, perfectly. Good luck with your new project, and thanks again for the shoutout :heart:

1 Like

I second @Florin: Thanks for making this work and thanks for the shoutout. Great to see the neo4j/Elixir ecosystem prosper! :heart:

1 Like

Version 0.6.2 of ecto_neo4j is out with… a (light) relationship support.
It was a bit like putting squares in triangles but it works for simple operations, which ease the data management.
It is now possible to insert, update, preload your relationship in a classic ecto way even if specific functions (with same name) need to be used.

A comprehensive doc has been added too: https://hexdocs.pm/ecto_neo4j/up_and_running.html
which demonstrate all the features with examples.

Hope you’ll enjoy this new version.

2 Likes

@domvas Hi, thanks so much for creating as well as keeping this package up to date. Also, thanks for adding details about its implementation and how it relates to Ecto. Well done!!! :pray:t5::pray:t5:

As I said in another topic, note that soon ecto_neo4j won’t be maintained anymore.
Having a way to connect Neo4j and Ecto has been a dream for most of Neo4j’s users who love Elixir, but in the end, it’s an unsolvable problem: graph concept doesn’t fit in relational ones.
ecto_neo4j was an attempt, and yes it has some good parts but you can’t have a really nice modeled graph if you always have to think relational…

But don’t be alarmed, it’s not my goal to leave you with unmaintained / unsatisfying package. An other one is on its way and will be soon available (in the next few days if I’m a genius, so maybe next few weeks :D). It’ll be announced here on elixir forum so keep it touch!

3 Likes

I’m interested to see what you have in mind.

It seems like it would be fairly easy to implement a cypher dsl that would in all likelihood work a lot better than ecto’s sql dsl anyways (not a jab at ecto_sql, just that sql is not very composable).

I’m not sure to understand what you’re looking for.
Could you elaborate?

Imagine you have some ecto schemas.

defmodule MyUser do
  schema "user" do
    field(:name, :string)
  end
end

defmodule Post do
  schema "post" do
    field(:contents, :string)
  end
end

# a schema for a relation
defmodule Wrote do
  schema "wrote" do
    field(:edited_at, :utc_datetime)
  end
end

In cypher you would need to create nodes and relations, so you would do so like this in elixir

node() # an anonymous node
node(id) # node by id
node(%{name: "foobar"}) # a node that matches attributes
node(MyUser) # a node of your schema user
node(%MyUser{ name: "foobar"})
user = MyUser { name: "foobar"}
node(user) # a node that matches an existing user (either by its id or its attributes, depending on how it was created)
node(user, as: :some_user) # and the ability to tag a node with a name you can reference later
node(:user, %{name: "foobar"}) # an anonymous node of type :user with an attribute)

And then relations

rel() # anonymous bidirectional relation
rel(:wrote) # user :wrote post
rel(%Wrote{ edited_at: "2020-01-01"}) # a relation based on your schema with an attribute match
out(:wrote) # an outgoing relation
in(:wrote) # an incoming relation
rel(:wrote, as: wrote) # with an alias

Now you can start making statements, such as match

match_writer = match |> node(MyUser, as: :someone) |> out(Wrote, as :wrote) |> node(Post, as: :post)

today = Timex.now |> Timex.beginning_of_day()
query = match_writer |> where([someone: s, post: p], s.name == "foobar" and p.edited_at > ^today)

query |> sort([someone: s], s.name) |>  return([someone: s, wrote: w, post: p], {s.name, p.id, w.edited_at}) |> Repo.all()

You would just have to have a query struct that held each binding and enough information to turn it into a fragment of cypher statement.

for schema, you can already have a look here: https://hexdocs.pm/seraph/schema.html#content
With this, everything you can think of as a graph model can fit in, except for one thing: multiple relationship with same type between same node (ex: multiple :WROTE between :User and :Post) which is possible in Neo4j but does not make sense in real life.

For the DSL, I didn’t take the node/x approach because of the proxmity with Kernel.node/1,2 and go on a more “cypher” path.
Nodes are like this:

{identifier, queryable, properties}

# Example
{u, User, %{firstName: "John"} # is (u:User {firstName: "John"})

# Almost all variations are valid
{u} # (u)
{User , %{firstName: "John"} # (:User {firstName: "John"})
...
{} and {User} won't be valid in MATCH because they have no sense there

Relationships like this:

[start_node, [identifier, queryable, properties], end_node]


# Example
[{User}, [rel, READ, %{nb_times: 3}], {Post}]

# And you can also use the variations

for query, what you wrote will be like this:

today = Timex.now |> Timex.beginning_of_day()
    
match([
  {s, MyUser},
  {p, Post},
  [{s}, [w, Wrote], {post}]
])
|> where(s.name == "foobar" and p.edited_at > ^today)
|> return(s.name, p.id, w.edited_at)
|> order_by(s.name)

# the match can also be written like this:
match([[{s, MyUser}, [w, Wrote], {post, Post}]])

# and query can also be written like this:
match [
  {s, MyUser},
  {p, Post},
  [{s}, [w, Wrote], {post}]
],
where: s.name == "foobar" and p.edited_at > ^today,
return: s.name, p.id, w.edited_at
order_by: s.name

And, little trick, order DOES matter, writing this

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

will result in a syntax error from Neo4j because it translates to:

MATCH (u:User)
RETURN u
WHERE u.firstName = "John"

This is maybe this point that can make composition a bit tricky.
But I think it’s mandatory if we want to be able to write quite complex queries, or just to put the OPTIONAL MATCH exactly where we want.

Sounds like you are basically in the same frame of mind as I am. I look forward to checking out your new code.