Overview Why Neo4j? Introduction to Neo4j Cypher Neo4j in Ruby Setup The Fun Stuff Eager Loading Recommendations Doing More An example Sinatra application The asset_portal application Getting Help More Screencasts Integrations with other gems Goals This course provides an overview… Learn More →

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 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:

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 A Object-Graph-Mapper (OGM) for the Neo4j graph database. It tries to follow API conventions established by ActiveRecord but with a Neo4j flavor.
  • neo4j-core A low level driver for connecting to Neo4j. Used by the neo4j gem.
  • neo4j-rake_tasks A set of rake tasks for installing and managing a Neo4j database within your project. Used by the neo4j-core gem.

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 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.

In this guide we’ll 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 which 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 need to specify our Neo4j relationship directions and types here. Additionally 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>

And 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

Now that we’ve created our scaffolding let’s run the migrations to create constraints for our models and then 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 isn’t 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 done this 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’re almost certainly going to want that association for all of the objects.

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 which 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’re 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:

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’ve 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.

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 isn’t 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:

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’re 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, for all books, the books which 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 let’s 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! There are a number of resources to help you along with your journey.

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 example Sintra example applications for the neo4j-core and neo4j gems.

The asset_portal application

You can find the result of this guide in it’s 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! You can ask a question on StackOverflow (make sure to use the neo4j.rb tag), or join us in our Gitter chat room. You can also check out the website and the documentation.

There is also a wonderful public Slack group for the wider Neo4j community if you have questions about installation, configuration, Cypher, etc…​

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

There are many common gems that you’ll want to use with your Neo4j database. Many are supported for the Neo4j.rb project:

Authentication

File Attachment

Pagination

ElasticSearch Integration

Admin User Interface

Integration With the Neo4j Spatial Plugin

Ruby Object Manager

Misc.