Getting Started with Neo4j and Ruby

Goals
This course provides an overview on everything that you need to build a Neo4j application with the Ruby programming language. Ruby on Rails and Sinatra examples are given but any web framework (or lack thereof) can be used.
Prerequisites
You should have Ruby installed on your system. Some experience with Ruby and/or Rails is suggested.

Let’s say you would like to write a web application to track entities for yourself or your organization. Being a good DRY programmer you might decide that what you want is an asset portal: an application which gives you an GUI for browsing and editing entities while also making it easy to define new assets. This guide will show you how you might start creating such an application using Ruby on Rails and Neo4j.

Why Neo4j?

Neo4j is the world’s most popular graph database. This offers a number of advantages:

  • Neo4j provides a schemaless representation of both entities and relationships between entities.

  • Relationships between entities are traversed rather than joined. Traversals explore the local subgraph meaning that query times stay the same as your database grows.

  • Because of the traversal paradigm we think in terms of the complex relationships in our data without worrying as much how to model it

Rubyists generally prefer tools which are developer friendly and which don’t bother you with details until it’s neccessary. Neo4j makes it easy to create nodes and relationships in whatever way seems most natural, but you can also change the structure of your database with a query.

Introduction to Neo4j

Connected information is everywhere in our world. Neo4j was built to efficiently store, handle, and query highly-connected elements in your data model. With a powerful and flexible data model, you can represent your real-world, variably-structured information without a loss of fidelity. The property graph model is easy to understand and handle, especially for object-oriented and relational developers.

simple graph

The property graph model consists of:

Nodes, which have:

  • properties: schemaless key/value pairs

  • labels: describe and group nodes much like tables group rows, but nodes can have multiple labels

Relationships, which connect two nodes directionally and have:

  • properties: schemaless key/value pairs

  • A type: gives a description of how it connects the two nodes

While relationships are directional, querying relationships in either direction has no associated performance cost.

Cypher

Cypher is Neo4j’s built-in query language. Cypher queries look like the following code block:

MATCH (p:Person)-[:LIKES]->(f:Fruit)
RETURN p, f.name

The MATCH clause is the most common starting point for Cypher queries. It defines a pattern for which to search and returns one result per match. For example, we might get the following two matches:

cypher match 1 cypher match 2

With the RETURN clause, we would end up returning a table such as:

Table 1. Result of Cypher query
p f.name

{name: "Denise"}

"Mango"

{name: "Denise"}

"Banana"

Here, you see we can return entire entities in our database rather than just properties. This might be returned as a Hash in Ruby, though by default in the Neo4j.rb gems these are wrapped in an object.

This is very handy, but it would also be nice to avoid the duplication of our Person node. You can perform the same match but instead use the collect function to aggregate the values:

MATCH (p:Person)-[:LIKES]->(f:Fruit)
RETURN p, collect(f.name)
Table 2. Result of Cypher aggregation query
p f.name

{name: "Denise"}

["Mango", "Banana"]

While it’s possible to get started using the Neo4j.rb without learning Cypher, it is a very powerful way to query a Neo4j database and is worth learning. Also, since the Neo4j.rb project works by making Cypher queries to Neo4j it is good to understand Cypher as your queries get more complex. There is a Cypher tutorial if you would like to learn more.

Neo4j in Ruby

For this guide, we will be using the Neo4j.rb project. The project consists of the following gems:

Neo4j.rb

The Neo4j.rb project is made up of the following Ruby gems:

neo4j-ruby-driver

A neo4j driver for ruby with an api consistent with the official drivers. It is based on seabolt and ffi. Available on all rubies (including jruby) and all platforms supported by seabolt.

neo4j-java-driver

A neo4j driver for ruby based on the official java implementation. It provides a thin wrapper over the java driver (only in jruby).

activegraph

A Object-Graph-Mapper (OGM) for the Neo4j graph database. It tries to follow API conventions established by ActiveRecord but with a Neo4j flavor. It requires one of the above drivers.

neo4j-rake_tasks

A set of rake tasks for installing and managing a Neo4j database within your project.

Specifically in this guide, we will be using the ActiveNode and ActiveRel modules from the neo4j gem to model nodes and relationships from our database.

Setup

The following example is in Ruby on Rails, but there is a Sinatra example below.

Here, we describe how to create a fresh Rails application with Neo4j as the database. If you have an existing Rails application, you can refer to the Neo4j.rb documentation.

Here is how you would setup your asset portal Rails app:

rails new asset_portal -m http://neo4jrb.io/neo4j/neo4j.rb -O
cd asset_portal
rake neo4j:install[community-latest]
rake neo4j:start

What do these commands do?

The first creates a new Rails app skipping ActiveRecord (the -O flag) and setting up Neo4j.rb in your project (the -m flag). Then, we change into our directory and install the latest version of the community edition of Neo4j into our app directory (into db/neo4j/development/). Last, we start up our copy of Neo4j.

Next, you should open up your config/application.rb file and find the config.neo4j.* lines. Here, you have a choice between *embedded* and server modes:

  • Server mode allows you to connect to Neo4j via it’s HTTP JSON APIs.

  • Embedded mode requires JRuby and allows you to run Neo4j as part of your JRuby process. This gives you access to the Neo4j Java APIs directly.

By default, you will be configured to Neo4j in server mode on the default port (7474). If you would like something other than the default console, take a look at the documentation.

By default the rake neo4j:install command disables Neo4j’s authentication. It is suggested that you enable the authentication for any exposed Neo4j instances.

To see an example of setting up Neo4j in Rails, check out this short screencast.

Create Basic Models

In this guide, we will be setting up different ActiveNode models, which will serve as assets. This is a textbook example of where we can use class inheritance in Neo4j.rb. First, we create some basic models:

rails generate scaffold User name:string email:string
rails generate scaffold Category name:string
rails generate scaffold Asset title:string

This will generate scaffolds just like any Rails application, with the exception that the models will be ActiveNode models rather than ActiveRecord models and will look like this:

app/models/user.rb
class User
  include Neo4j::ActiveNode
  property :name, type: String
  property :email, type: String
end

Since Neo4j is schemaless, we need to define our properties in our model.

By default there will be a uuid property created on our model. If you would like to define your own unique identifier you can use the id_property method. Either can be accessed or changed via the \#id and \#id= methods.

To learn more about properties, check out this short screencast.

Once we’ve set up those models, we can define our asset models like so:

rails generate scaffold Book isbn:string title:string year_published:integer author:references category:references

That should generate a model that looks like this:

app/models/book.rb
class Book
  include Neo4j::ActiveNode
  property :isbn, type: String
  property :title, type: String
  property :year_published, type: Integer

  has_one :in_or_out_or_both, :author, type: :FILL_IN_RELATIONSHIP_TYPE_HERE
  has_one :in_or_out_or_both, :category, type: :FILL_IN_RELATIONSHIP_TYPE_HERE

end

You should change that to look like the following (note the Asset superclass definition):

app/models/book.rb
class Book < Asset
  id_property :isbn
  property :year_published, type: Integer

  has_one :in, :author, type: :CREATED, model_class: :User
  has_one :out, :category, type: :HAS_CATEGORY
end
You can remove the title property because it is inherited from the Asset model.
We also need to specify our Neo4j relationship directions and types here. Since author isn’t enough for ActiveNode to understand that we want to reference users, we specify a model_class option.

By inheriting from Asset, our Book model will create nodes with two labels (Book and Asset). Likewise, when you query for nodes via the Book model, it will only find nodes which have both labels.

Lastly, we just need to make a couple of small fixes. Change these lines to get the names of book authors and categories:

app/views/books/index.html.erb
<td><%= book.author.try(:name) %></td>
<td><%= book.category.try(:name) %></td>

And change these lines to be able to choose the author when creating or editing books:

app/views/books/_form.html.erb
<div class="field">
  <%= f.label :author %><br>
  <%= f.select :author, options_from_collection_for_select(User.all, :id, :name, @book.author.try(:id)), include_blank: true %>
</div>
<div class="field">
  <%= f.label :category %><br>
  <%= f.select :category, options_from_collection_for_select(Category.order(:name), :id, :name, @book.category.try(:id)), include_blank: true %>
</div>

So that you can set your associations, change the book_params method in the BooksController to remove the \_id:

app/controllers/books_controller.rb
def book_params
  params.require(:book).permit(:isbn, :title, :year_published, :author, :category)
end

Running the migrations

Now that we have created our scaffolding, let’s run the migrations to create constraints for our models and start up our Rails server:

rake neo4j:migrate
rails s
open http://localhost:3000/books

From there, you can create, update, browse, and delete books via the scaffolding. You can visit /books, /users, and /categories to get entry points into the various sections.

The Fun Stuff

If you just wanted to do simple CRUD operations, there are plenty of other databases to choose from. How can we do something a bit more fun using the power of Neo4j?

Eager Loading

First, let’s look at a performance improvement, which is not available from ActiveRecord. When you go to your list of books, you should see something like this in your log:

log/development.log
Started GET "/books/" for ::1 at 2016-12-23 14:50:39 -0500
Processing by BooksController#index as HTML
  Rendering books/index.html.erb within layouts/application
 HTTP REQUEST: 6ms GET http://localhost:7474/db/data/schema/constraint (0 bytes)
 HTTP REQUEST: 3ms GET http://localhost:7474/db/data/schema/index (0 bytes)
 Book MATCH (n:`Book`:`Asset`) RETURN n HTTP REQUEST: 5ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)

 Book#author MATCH (previous:`Book`:`Asset`) WHERE (ID(previous) = $ID_previous) OPTIONAL MATCH (previous)<-[rel1:`CREATED`]-(next:`User`) RETURN ID(previous), collect(next) | {:ID_previous=>4}
 HTTP REQUEST: 5ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)

 Book#category MATCH (previous:`Book`:`Asset`) WHERE (ID(previous) = $ID_previous) OPTIONAL MATCH (previous)-[rel1:`HAS_CATEGORY`]->(next:`Category`) RETURN ID(previous), collect(next) | {:ID_previous=>4}
 HTTP REQUEST: 4ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
  Rendered books/index.html.erb within layouts/application (32.7ms)
Completed 200 OK in 84ms (Views: 81.5ms)

If you find that a bit hard to read, then you can add the following line to your application’s configuration:

config/application.rb
config.neo4j.pretty_logged_cypher_queries = true
Don’t forget to restart your Rails server!

Once you complete that, your log will look more like this:

log/development.log
Started GET "/books/" for ::1 at 2016-12-23 14:51:34 -0500
Processing by BooksController#index as HTML
  Rendering books/index.html.erb within layouts/application
 Book
  MATCH (n:`Book`:`Asset`)
  RETURN n
 HTTP REQUEST: 5ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
 Book#author
  MATCH (previous:`Book`:`Asset`)
  WHERE (ID(previous) = $ID_previous)
  OPTIONAL MATCH (previous)<-[rel1:`CREATED`]-(next:`User`)
  RETURN
    ID(previous),
    collect(next) | {:ID_previous=>4}
 HTTP REQUEST: 4ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
 Book#category
  MATCH (previous:`Book`:`Asset`)
  WHERE (ID(previous) = $ID_previous)
  OPTIONAL MATCH (previous)-[rel1:`HAS_CATEGORY`]->(next:`Category`)
  RETURN
    ID(previous),
    collect(next) | {:ID_previous=>4}
 HTTP REQUEST: 3ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
  Rendered books/index.html.erb within layouts/application (20.2ms)
Completed 200 OK in 51ms (Views: 48.3ms)

First, the books are loaded, and then separate queries are made to get all of the authors and categories for those books.

With ActiveRecord, you would need to specify an includes in order to make this happen rather than having each entity loaded individually. ActiveNode, on the other hand, makes the assumption that if you refer to an association from a list of items, you are almost certainly going to want that association for all of the objects.

One Step Further

But we can do better! Now, modify the index action of the BooksController like so:

app/controllers/books_controller.rb
def index
  @books = Book.all.with_associations(:author, :category)
end

The with_associations method is similar to includes, except that our associations are loaded in the same query using the collect() function demonstrated earlier.

log/development.log
Started GET "/books/" for ::1 at 2016-12-23 14:52:16 -0500
Processing by BooksController#index as HTML
  Rendering books/index.html.erb within layouts/application
 Book
  MATCH (n:`Book`:`Asset`)
  WITH n
  OPTIONAL MATCH (n)<-[:`CREATED`]-(author)
  WHERE (author:`User`)
  WITH
    n,
    collect(author) AS author_collection
  OPTIONAL MATCH (n)-[:`HAS_CATEGORY`]->(category)
  WHERE (category:`Category`)
  WITH
    n,
    collect(category) AS category_collection,
    author_collection
  RETURN
    n,
    [author_collection,category_collection]
 HTTP REQUEST: 6ms POST http://localhost:7474/db/data/transaction/commit (1 bytes)
  Rendered books/index.html.erb within layouts/application (11.8ms)
Completed 200 OK in 46ms (Views: 43.6ms)

What we get is a list of Book objects that are pre-populated with authors and categories.

Recommendations

You may have heard that Neo4j makes building recommendations from your data easy. Let’s take a look at how we might make some recommendations. For this, we are going to introduce has_many associations. Since entities are connected via relationships in Neo4j, the database doesn’t draw any distinction for when we want to have a single relationship or many to/from a node. In our Ruby apps, however, it is often convenient to be able to draw this distinction.

To learn more about associations, check out this short screencast.

First, change the category association for the Book model:

app/models/book.rb
has_many :out, :categories, type: :HAS_CATEGORY

Don’t forget to change the :category argument in the with_associations call in the controller to :categories like below.

app/controllers/books_controller.rb
  def index
    @books = Book.all.with_associations(:author, :categories)

Then add a books association to the Category model:

app/models/category.rb
has_many :in, :books, origin: :categories

Since we have changed the has_one to has_many for the book categories, we should update our scaffold UI to match:

app/controllers/books_controller.rb
def index
  @books = Book.all.with_associations(:author, :categories)
end

... Further down ...

def book_params
  params.require(:book).permit(:isbn, :title, :year_published, :author, :category_ids => [])
end
app/views/books/index.html.erb
<th>Categories</th>

... Further down ...

<td>
  <ul>
  <% book.categories.each do |category| %>
    <li><%= link_to category.name, category %></li>
  <% end %>
  </ul>
</td>
app/views/books/_form.html.erb
<div class="field">
  <%= f.label :categories %><br>
  <%= f.select :category_ids, options_from_collection_for_select(Category.order(:name), :id, :name, @book.categories.map(&:id)), {include_blank: true}, {multiple: true, size: 5} %>
</div>
app/views/books/show.html.erb
<p>
  <strong>Categories:</strong>
  <%= @book.categories.map(&:name).to_sentence %>
</p>

Whew!

Now, with the ability for a book to have many categories and for a category to have many books, you can have a much better picture about recommending books.

Querying for Recommendations

It is simple to get a start on querying potential recommendations. Try running this in your Rails console:

Book.all.categories.books.to_a

It should show you the query which was made and it should look something like:

MATCH (n:`Book`:`Asset`)
MATCH (n)-[rel1:`HAS_CATEGORY`]->(node3:`Category`)
MATCH (node3)<-[rel2:`HAS_CATEGORY`]-(result_books:`Book`:`Asset`)
RETURN result_books

This query is finding all of the books that share categories with all other books. This is not particularly useful until we start introducing some variables. In the neo4j gem, this is called association chaining. For more information about association chaining, check out this short screencast:

Finding Shared Categories

What if you wanted to list every book and find out, for every other book with which it shares a category, how many categories it shares?

Book.as(:book).
  categories(:category).
  books(:other_book).
  pluck('book', 'other_book', 'count(category)')

Notice how we are starting to assign variables. These eventually become the variables in the cypher query made to Neo4j.

Taking it a step further, let’s create a query which finds, all the books that share at least two categories. We can also display these recommendations in our app like so:

app/controllers/books_controller.rb
@recommendations = Book.as(:book).
                    categories(:category).
                    books(:other_book).
                    where('book <> other_book').
                    query.
                    with('book, other_book, count(category) AS count').
                    where('count > 1').
                    pluck('book.isbn', 'collect(other_book)')
@recommendations = Hash[*@recommendations.flatten(1)]
app/views/books/index.html.erb
<th>Recommendations</th>

... Further down ...

<td><%= (@recommendations[book.isbn] || []).map(&:title).to_sentence %></td>

Of course, we don’t want to put too much logic in the controller, so we can extract this to a model class method:

app/controllers/books_controller.rb
Book.recommendations
app/controllers/books_controller.rb
def self.recommendations
  recommendations = all(:book).
                      categories(:category).
                      books(:other_book).
                      where('book <> other_book').
                      query.
                      with('book, other_book, count(category) AS count').
                      where('count > 1').
                      pluck('book.isbn', 'collect(other_book)')

  Hash[*recommendations.flatten(1)]
end

Because the all method starts it off, we can actually add this to an existing chain rather than just calling it on the Book model. For example, if we had a recent scope which only gave us books from the past ten years:

Book.recent.recommendations

Doing More

Are you getting into the idea of using Neo4j? Great! If you still have a lot of questions, there are a number of resources to help you along with your journey.

For the fastest help or answers to questions, take a look at or reach out to us on our Neo4j Online Community!

An example Sinatra application

app.rb
require 'sinatra'
require 'neo4j'

neo4j_url = ENV['NEO4J_URL'] || 'http://localhost:7474'
neo4j_username = ENV['NEO4J_USERNAME']
neo4j_password = ENV['NEO4J_PASSWORD']

Neo4j::Session.open(:server_db, neo4j_url, basic_auth: {username: neo4j_username, password: neo4j_password})

class Asset
  include Neo4j::ActiveNode
  property :title, type: String
end

class Book < Asset
  id_property :isbn
  property :year_published, type: Integer

  has_one :in, :author, type: :CREATED, model_class: :User
  has_many :out, :categories, type: :HAS_CATEGORY

  def self.recommendations
    recommendations = all(:book).
                        categories(:category).
                        books(:other_book).
                        where('book <> other_book').
                        query.
                        with('book, other_book, count(category) AS count').
                        where('count > 1').
                        pluck('book.isbn', 'collect(other_book)')

    Hash[*recommendations.flatten(1)]
  end
end

get '/books' do
  @books = Book.all.with_associations(:author, :categories)

  @recommendations = Book.recommendations

  erb :books_index
end

For the example view, see the one from the result of the Rails example above.

You can also view Sintra example applications for the neo4j-core and neo4j gems.

The asset_portal application

You can find the result of this guide in its GitHub repository. If you would like to play with a more developed application, check out our asset_portal app. The project introduces a single AssetController to avoid the duplication from this guide and also uses other tools like Semantic UI for a cleaner interface.

Getting Help

The maintainers of the Neo4j.rb project love to help! There is a wonderful Neo4j Online Community if you have questions about installation, configuration, Cypher, or any topic. You can also check out the Neo4j Ruby website and the documentation.

More Screencasts

In addition to the screencasts embedded in this guide, there are two others to help you learn more about the Neo4j.rb project:

Integrations with other gems