Thanks to the workshop I started using Neo4j


update-java-alternatives --list
sudo apt install default-jre default-jre-headless
sudo apt-get install openjdk-8-jdk

wget -O - | sudo apt-key add -
echo 'deb stable/' | sudo tee -a /etc/apt/sources.list.d/neo4j.list
sudo apt update
sudo apt install neo4j
lynx localhost:7474/browser

sudo neo4j start
# sudo mkdir /var/run/neo4j # this is needed if permission denied
# cat /etc/neo4j/neo4j.conf
# cat /var/log/neo4j/neo4j.log
# server will accepts connection on bolt://localhost:7687
# user:password neo4j:neo4j but it has to be changed
# reset password with: sudo rm /var/lib/neo4j/data/dbms/auth  or
# sudo neo4j-admin set-initial-password ...
sudo service neo4j status
sudo service neo4j start

To enable remote access just uncomment the line

# /etc/neo4j/neo4j.conf

For debugging you can also uncomment http logs so you can see if firewall is blocking

# /etc/neo4j/neo4j.conf

Open browser on http://localhost:7474/ Since communications is over http/bold, you need to create new neo4j instance for each your project and environment. Use and install with the script

ineo instances
ineo status
ineo create -v 3.1.0 -p 7004 my_app_development
ineo create -p7006 my_app_test
ineo set-port my_app_development 7000 # this will change only http port
ineo destroy my_app_test
ineo delete-db my_app_test # this will clear only database
ineo versions

But I think bolt is not supported

Alternativelly you can use Add gem 'neo4j-rake_tasks'. Note that there is no authentication enabled so do not use on production.

# install neo4j to db/neo4j/development`
rake neo4j:install[community-latest,development]
rake neo4j:install[community-latest,test]

# To change the server port (default is 7474) type:
rails neo4j:config[development,$(expr $NEO4J_BOLT_PORT + 2)]
rails neo4j:config[test,$(expr $NEO4J_TEST_BOLT_PORT + 2)]

# vi db/neo4j/development/conf/neo4j.conf
# this will add something like this
# Bolt connector

# HTTP Connector. There must be exactly one HTTP connector.
rake neo4j:start[development]
echo http://localhost:$(expr $NEO4J_BOLT_PORT + 2)
rake neo4j:start[test]
echo http://localhost:$(expr $NEO4J_TEST_BOLT_PORT + 2)

rails neo4j:stop[development]
kill -9 `cat db/neo4j/development/run/`

Note that when you are changing the ports, then run spring stop to reload new env.

Usine neo4j with docker

docker pull neo4j
docker run     --publish=7474:7474 --publish=7687:7687     --volume=$HOME/neo4j/data:/data     neo4j

Learn from examples in browser, type :play movies.

Cypher commands


  • create node () and relationship []. In CREATE only directed relationships. Nodes can have 0 or more labels :Person and can have different properties (strings, numbers or booleans). Relationships always have a direction and have a single type (:KNOWS) and also properties.

    CREATE (d:Covek { name: 'Dusan' }), (m:Covek { name: 'Milan' }),
    (s:Covek { name: 'Srdjan' }),
  • RETURN matched nodes and relationships, WHERE filter and what to RETURN, with eventual: LIMIT 10.
    MATCH (n:Covek) WHERE = 'Dusan' RETURN n;
    • match relationships by type [edge:BRO]. You can write long edges
      MATCH (n)-[:BRO]->()<-[:BRO]-(m) RETURN n, m
    • instead writting two edges MATCH (n)-[]->(t), (t)-[]->(m) you can write number of edges [*2] or even interval of number [*1..4]
    • if direction is ommited it will return twice (one in both directions)
      MATCH (n)-[:BRO]-(m) RETURN n, m;
    • WHERE can be moved inside node or edge declaration MATCH (n:Covek { name: 'Dusan' }) RETURN n; so WHERE is usualy used with WHERE NOT. For example cocoautors which are not coauthors (not equal is <>)
    MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),
    WHERE NOT (tom)-[:ACTED_IN]->()<-[:ACTED_IN]-(cocoActors) AND tom <> cocoActors
    RETURN AS Recommended, count(*) AS Strength ORDER BY Strength DESC
    • RETURN can be node or edge, Type(), DISTINCT(),
  • delete all nodes for >2.3.0

  • EXPLAIN and PROFILE will give more info about query


Follow to create example app asset_portal, there is screen cast episode 6 is advance and links

rails new asset_portal -m -O
# this will add gem neo4j and neo4j-rake_tasks
# config/neo4j,yml
# db/neo4j folder
# config/application.rb uncomment server url with
config.neo4j.session.type = :bolt
config.neo4j.session.url = 'bolt://neo4j:dusan10@localhost:7687'

rails generate scaffold User name:string email:string
rails s
gnome-open http://localhost:3000/users

Model are defined with

class User
  include Neo4j::ActiveNode
  has_many :out, :assets, type: :OWNS

class Asset
  include Neo4j::ActiveNode
  property :created_at, type: DateTime
  property :updated_at, type: DateTime
  has_one :in, :user, origin: :assets
  • properties are declared with property :name. It can also define
    • default: 'default_value' is set when property is nil so for new object it will be 'default_value'. For existing object you need to run migration
    • type: Integer define property type in ruby: integer, float, string, time, date, datetime, boolean, BigDecimal. Istead of DateTime, if you need some expensive query, better is to use type: Integer and store as unix timestamp
    • I have some problems with serialize: :name so I used plain old = { a: 3}.to_json so I save hash or array to database. When I load, I need to deserialize to that object JSON.parse
  • enum status: [:draft, :published] maps integer in db to strings in rails
    • you can use user.draft!, user.draft? or User.draft
    • _default: :draft to define default value, note that default value is when value is NULL for existing objects, so better is to run migration (user.draft? will return true even in neo4j it is NULL!!!) and you need to save two times next unless user.draft? ; user.status = :published;! user.status = :draft;!
    • usually you need to add index on enum column but you can disable _index: false
    • validation like validates :name, presence: true, or validates_uniqueness_of :name
    • you can pass undeclared properties if you insert the line include Neo4j::UndeclaredProperties
  • relationship: has_many :in, :posts
    • first parameter can be :in, :out, :both. Once it is created, you should not change it (unless you write data migration)
    • second set the association name (can be overriden with :model_class and association name :type)
    • has_one should be used with singular and .first is automatically called so result is non chainable (unless you call true)
    • :type override association name in Neo4j (Rails uses asociation name). type: false match all relationships
    • :model_class is NODE model on other end of association, usualy resolved from second parametar - association name, model_class: false will match any node on other end. Value can be a symbol with class name: model_class: :Asset, or can be array in case of polymorphic association has_many :in, :written_post_or_comments, type: :WROTE, model_class: [:Post, :Comment]. When using polymorphic than chains will not work unless proxy_as and query_as are used

    • :rel_class is REL model
    • :origin name of association in oposite model so you don’t need to write :type in both classes, often used in :in relationships. Value is the association name in reciprocal model, origin: :user
    • dependent: delete to delete all associated nodes in cypher (:destroy will call .each and .destroy with callbacks). :delete_orphans delete associated records that have no other relationships of the same type (:destroy_orphans do that in Ruby and callbacks will be called).
    • unique: true will use CREATE UNIQUE relationship (only one). unique: :all will create unless all nodes, type, direction and rel properties are matched. unique: { on: [keys] } is looking only on keys to determine if already exists.
    • eager loading is implicit, but if you need explicit you can use .with_associations(:tags, :comments) to generate COLLECT(). For nested you can use hash and array notation:
      @users = User.all.with_associations moves: { from_group: [:location], to_groups: [:location] }

    CRUD associations

    Comment.create post: post1, text: 'text'   # create comment and relationship
    post.comments << comment3             # Creates new relationship
    post.comments.create comment3, posted_by: 'me' # text property on relationship
    # can not create like text: '' since will not
    # be defined
    # remove association
    post.comments = [comment1, comment2]  # Removes all existing relationships = post1                  # Removes all existing relationships
    post.comments.delete(comment) # destroys the relationship
    post.comments.find(comment).delete # destroys the relationship and node
    # update nodes
    post.comments.update_all flagged: true
    # update relationships
    post.comments.update_all_rels flagged: true
    # iterate over relationships &:neo_id
    post.comments.each_with_rel { |node, rel| }
    # you can combine several associations # will return all companies for comments
    # find does not work if argument is AssociationProxy so you should use map
    # &:uuid
    # find_by or where has some problem if argument is array, so I use another
    # association and find [ids]
    to_location = move.to_groups.where(location: some_locations)
    to_location = move.to_groups.location.find(
    # custom query_as match pluck
    post.query_as(:p).match('(p)-[rel1:CONTAINS]->(n2)').where('rel1.on = true').pluck(:rel1)
    # with_associations and order
    @users =
             .with_associations(moves: { from_group: [:location], to_groups: [:location] })
             .order('u.last_sign_in_at DESC')
    # to find nodes that does not have relationship
    Move.query_as(:m).match('(m)').where('NOT (m)-[:WANTS]-()').pluck :m
    # to find nodes that does not have relation with EmailMessage with specific
    # property tag=some_tag
    User.query_as(:user).where("NOT (user)-[:RECEIVED]-(:EmailMessage { tag: '#{tag}'})").pluck(:user)
  • when comment is destroyed than you can not get it’s post, it is better to get it’s post before

    def destroy
      post =
      redirect_to post_path post
  • callbacks like before_save

Relationships is defined Type name will be the same as class name uppercased (:KNOWS)

class Knows
  include Neo4j::ActiveRel
  from_class :user
  to_class :any
  property :created_at, type: DateTime
  property :updated_at, type: DateTime

# create
Knows.create from_node: u, to_node: u
# fetch retreive

  • it is not able to access relationships directly, always using a node
  • properties are the same as for nodes
  • usefull for polymophic, for example User knowns companies, places, … so it contains different logic for each relation.


You can generate migration to add new constrains. For new fields you can change models and start using it. To remove old fields you can also create migration.

rails g neo4j:migration AddUniqEmailToUsers
rake neo4j:generate_schema_migration[constraint,User,email]

Constrain is uniq and index. So to add uniq constrain you do not need to add index after that (only think is to remove if the index exists)

class ForceCreateUserEmailConstraint < Neo4j::Migrations::Base
  def up
    drop_index :User, :email, force: true
    add_constraint :User, :email, force: true


Index and constraints are defined in migrations. To generate use ‘rake neo4j:generate_schema_migration[index|constraint,Label,property]’ for example: rake neo4j:generate_schema_migration[constraint,Person,uuid] will generate

class ForceCreatePersonUuidConstraint < Neo4j::Migrations::Base
  def up
    add_constraint :Person, :uuid, force: true

  def down
    drop_constraint :Person, :uuid

code This will add uniqueness constrain ASSERT person.uuid IS UNIQUE, I think in neo4j-core implement that. I see in :schema that we alse got a index.

Not sure how to generate existence ASSERT exists(person.uuid) constraint. Also you can add_index :Person, :email, force: true. Run migrations with

rake neo4j:migrate

Or use my shortcut rake db:migrate, RAILS_ENV=test rake db:drop and rake db:seed

# lib/tasks/db.rake
namespace :db do
  desc 'seed'
  task seed: :environment do
    b = {}
    # User
      { var: :user1, email: '[email protected]' },
      { var: :user2, email: '[email protected]' },
      { var: :user3, email: '[email protected]' },
    ].each do |doc|
      params = doc.except(:var).merge(name: doc[:var])
      user = User.find_by params
      unless user.present?
        user = User.create! params.merge(password: 'asdfasdf', confirmed_at:
        puts "User #{}"
      b[doc[:var]] = user if doc[:var]

    # Location
      { var: :location1 },
      { var: :location2 },
      { var: :location3 },
      { var: :location4 },
      { var: :location5 },
    ].each do |doc|
      params = doc.except(:var).merge(name: doc[:var])
      location = Location.find_by params
      if location.blank?
        location = Location.create! params
        puts "Location #{}"
      b[doc[:var]] = location if doc[:var]

    # Group
      { var: :g2_l1, location: b[:location1], age_min: 2,
        age_max: 2 },
      { var: :g2_l2, location: b[:location2], age_min: 2,
        age_max: 2 },
      { var: :g2_l3, location: b[:location3], age_min: 2,
        age_max: 2 },
      { var: :g2_l4, location: b[:location4], age_min: 2,
        age_max: 2 },
      { var: :g2_l5, location: b[:location5], age_min: 2,
        age_max: 2 },
      { var: :g4_l1, location: b[:location1], age_min: 4,
        age_max: 4 },
    ].each do |doc|
      params = doc.except(:var).merge(name: doc[:var])
      group = Group.find_by params
      unless group.present?
        group = Group.create! params
        puts "Group #{}"
      b[doc[:var]] = group if doc[:var]

    # Move
      { var: :m1_l1_l2, from_group: b[:g2_l1], prefered_groups: b[:g2_l2],
        user: b[:user1] },
      { var: :m1_l1_l3, from_group: b[:g2_l1], prefered_groups: b[:g2_l3],
        user: b[:user1] },
      { var: :m2_l3_l4, from_group: b[:g2_l3], prefered_groups: b[:g2_l4],
        user: b[:user2], available_from_date: },
      { var: :m3_l4_l1, from_group: b[:g2_l4], prefered_groups: b[:g2_l1],
        user: b[:user3], }
    ].each do |doc|
      params = doc.except(:var).merge(name: doc[:var])
      move = Move.find_by params
      unless move.present?
        move = Move.create! params
        puts "Move #{}"

  desc 'drop'
  task drop: :environment do
    Rake::Task["neo4j:stop"].invoke Rails.env
    puts sh "rm -rf db/neo4j/#{Rails.env}/data/databases/graph.db"
    Rake::Task["neo4j:start"].invoke Rails.env
    puts "Find database on http://" +
         Rails.application.secrets.neo4j_host.to_s + ":" +
         (Rails.application.secrets.neo4j_bolt_port.to_i + 2).to_s

  desc 'migrate'
  task migrate: :environment do
    puts "running neo4j:migrate"
    Rake::Task["neo4j:migrate"].invoke Rails.env

  desc 'setup = drop, migrate and seed'
  task setup: :environment do
    puts 'this is not implemented'
    puts 'since drop need some time to bootup, and migrate raises exception'

In migration def change should be replaced with def up and def down since there is an error:

rake aborted!
NotImplementedError: NotImplementedError

If using the bold, in migrations there are exception

rake aborted!
should be implemented!

Test data should be wiped manually There are attempts to use database_cleaner gem

# test/test_helper.rb
class ActiveSupport::TestCase
  teardown do
    Neo4j::ActiveBase.current_session.query %(
      MATCH (n)
      WHERE NOT (n:`Neo4j::Migrations::SchemaMigration`)
      DELETE n

In tests you can not use fixtures. Maybe raw cypher can help

    query = %(
      CREATE (c:City),
    Neo4j::ActiveBase.current_session.query query

but best advice to use factory girl.


Debug with user.assets.categories.assets.print_cypher or some query move.query_as(:m1).match('(m1)-[]-(g:Group)').to_cypher (similar to_sql)

Query on property User.where(name: ?) parametar ? can be: string, nil, array, regular expression User.where(name: /.*dule.*/i), range User.where(age: 1..2).

If you use params inside where string you should use params to excape and prevent injection attack

Location.query_as(:location).where("location.name_en =~ {name_en}").params(name_en: "(?iu).*#{@term}.*").pluck(:location)

There is neo_id on each node and relationships but are somewhat volatile so do not use it. Instead, each new node requires primary key uuid property which is also accessed using .id method. Rels do not have ids since they are traversed by nodes. SecureRandom uuid is generated in migration rake neo4j:generate_schema_migration[constraint,Model,uuid] and rake neo4j:migrate

Raw query can be done using Neo4j::ActiveBase.current_session.query('MATCH (n) RETURN n LIMIT {limit}', limit: 10) or when searching for maximum

But it is better to use ActiveNode finders:

# find by id_property
User.find my_uuid
# find by any property property
User.find_by name: 'Dule'
# find by associations
User.find_by from_group: Group.last
# find_by supports only single value even it is has_many
User.find_by to_groups: Group.last
# you can use where with associations
User.all.where to_groups: Group.last

Queries can be chained in 3 ways: Model.all, Model.association, model_object.association. It returns AssociationProxy.

If where is used, than QueryProxy is returned.

Query object is used to simplify on which fields is used: User.query_as(:user).where(user: { age: 1..2 }).pluck(:user)

You can use where or rel_where in the middle of a chain (it is applied to last) for example user.created_assets.where(public: true).categories. Also .order, .limit and .skip. You can pass single attribute to assign variable in cypher, so we can pluck on it user.created_assets.categories(:category).pluck('', 'count(*)')

Using where can be with string or with hash. If you want to target specific node you can use string like where("node.uuid = '#{}'") or hash where(node: { neo_id: }), or using parms where('node.uuid = {node_id}').params(node_id: or match_nodes(node: n). Note that you have to use uuid since id in cypher does not exists. For a list you do not need to wrap in square brackets where('node.uuid IN {node_ids}').params(node_ids:

Regular expression

You can search with case insensitive regex


gem 'devise-neo4j'
rails g devise:install --orm=neo4j
rails g neo4j:devise User


export MYAPP_NAME=my-app # only dash, not underscore
heroku apps:create $MYAPP_NAME
heroku addons:create graphenedb

heroku buildpacks:set
heroku buildpacks:add --index 1
heroku buildpacks # should return  1. nodejs  2. ruby (latest wins :)

heroku config # copy all GRAPHENEDB_BOLT... variables
# use http, not bolt port
heroku run rake db:migrate
heroku run rake db:seed

You can export from Graphenedb To restore localy you need to copy files to neo4j/data/graph.db/


Model your entity attribute as property on a node if

  • it is simple value and there is no need to qualify relationship.


  • use relationship to a new node instead of property of a node in cases where you need to add another relationships to it. For example instead of property user_id on relationship you should use User node with :OWNS relationship to it, since you will probably add another relation to that User
  • use relationship if there is a need to specify something relationship (for example profiency in a skill)
  • use relationship if its a complex property like multiple fields (address) So rule is to use node if you need start a query from this entity like skill ruby -> users.

Instead to name relationship with :BELONGS_TO use something more meaningfull to businness so you know what it is about:

  • something belongs to Category use something-[:IN_CATEGORY]->category
  • for review [:REVIEW_FOR].
  • For has_many use verb ()-[:POSTS]->(:Post), or [:CONTAINS], [:USING], [:MENTIONS]
  • For has_and_belongs_to_many use whatever (:User)-[:FRIENDS]->(:User), (Tweet)-[:RETWEETS]->(:Tweet)

Common Graph stuctures:

  • Intermediate Nodes: when when you have three or more entity in single context: ‘Ian bought a book in Mercator’, ‘Patrick work at Trk in role Designer’ …
  • Linked List: entities are linked as sequence, you need to traverse: ‘Job history’, ‘Broadcast or Production of Movies’
  • Versioning Graphs: time based
    • structure: we have idendtity nodes as placeholders and timestamped relationships.
    • state: every note is snapshot,
  • ignore case is with (?i) but for cyrilic we need (?iu), for example MATCH (d:Covek) WHERE d.nameL =~ '(?iu).*DUŠAN.*' RETURN d

  • use gem 'rack-mini-profiler' to see actual numbers of queries
  • use kaminari for pagination… use proxy_as instead of changing the scope to associations


Error defined for a second time. Associations can only be defined once is when you name the associataion with already used name. Please use different name and same model_class.

In migration rails aborted! Neo4j::MigrationError: Duplicate constraint for Location means that there exists in schema so you need to manually remove. You can use :schema or CALL db.constraints to see all constrains and you can drop with


To wipe all data, destroy clear database, remove graph.db and restart.

rake neo4j:stop[development]
rm -rf db/neo4j/development/data/databases/graph.db
rake neo4j:start[development]

ArgumentError: Invalid value for 'city' condition is error in seed task when you assign symbol :city1 instead of object b[:city1]

If you open rails c console1 and than rails c console2, and exit in console1, it will not exit. You need to exit console2 to release something and both consoles exists… When rails console hangs than spring stop helps.

I got some error:

Neo4j::Core::CypherSession::CypherError (  Cypher error:
  Neo.DatabaseError.Transaction.TransactionStartFailed: Database has encountered some problem, please perform necessary action (tx recovery/restart)

For which I restart my machine to recover from this error.


Neo4j::DeprecatedSchemaDefinitionError:           Some schema elements were defined by the model (which is no longer supported), but they do not exist in the database.  Run the following to create them if you haven't already:

rake neo4j:generate_schema_migration[constraint,Chat,uuid]

And then run `rake neo4j:migrate`

(zshell users may need to escape the brackets)

can be solved with

RAILS_ENV=test rake neo4j:migrate
heroku run rake neo4j:migrate:status
# try adding force: true so there is no error when performing migrations
class CreateChat < Neo4j::Migrations::Base
  def up
    add_constraint :Chat, :uuid, force: true

  def down
    drop_constraint :Chat, :uuid


Neo4j::Core::CypherSession::CypherError:   Cypher error:
  Neo.DatabaseError.Transaction.TransactionStartFailed: The database has encountered a critical error, and needs to be restarted. Please see database logs for more details.

There is a problem when there exists a node when searching by relation but does not exists when searching by n.uuid


rails g model will generate model files with rubocop offencies

rails neo4j:migrate event it is successfull, generate error message:

should be implemented!

device-neo4j generate two migrations with same id… Best solution is to rename migration and clear database.

Errors format should be used:


On Heroku GrapheneDB in tab Admin -> Export database, you can download and export to your graph.db folder

export GRAPH_DB_PATH=db/neo4j/development/data/databases/graph.db
unzip tmp/ -d $GRAPH_DB_PATH
rails neo4j:restart[development]

To dump to production you need also to restart heroku

wget 'graphdb_url'
export GRAPH_DB_PATH=/var/lib/neo4j/data/databases/graph.db
sudo rm -rf $GRAPH_DB_PATH
sudo -u neo4j mkdir $GRAPH_DB_PATH
sudo -u neo4j unzip -d $GRAPH_DB_PATH
sudo service neo4j restart
heroku restart
tail -f /var/log/neo4j/debug.log

To make a backup you can

# we have to cd to directory because otherwise zip will store path in output
cd db/neo4j/development/data/databases/graph.db/
zip -r ../ .


  • and differences with sql
  • youtube series

Todo 42min time versioned graphs