Skip to content

Neo4j v3 (Archived)

Brian Underwood edited this page Aug 11, 2014 · 3 revisions

THIS PAGE WAS ARCHIVED AUGUST 6, 2014

The Neo4j V3 is a complete rewrite and will work on MRI since you can run it using either the Neo4j server or embedded API. The Neo4j embedded API is only available if running on JRuby.

Neo4j-core

Please read the neo4j-core README. The neo4j gem uses the neo4j-core gem. Neo4j gem provides an active model compliant api which means it will work great together with rails. Neo4j-core gem contains many useful methods and classes.

Usage from Ruby

Installation of Neo4j Server and start server:

rake neo4j:install[community-2.0.2]
rake neo4j:start

Example, Open a session to the neo4j server database (in IRB for example)

  Neo4j::Session.open(:server_db, "http://localhost:7474")

After you have created a session you can now use the database, see below.

Usage from JRuby

On JRuby you can access the database in two different ways: using the embedded db or the server db.

Example, Open a session to the neo4j embedded database (running in the same JVM)

  session = Neo4j::Session.open(:embedded_db, '/folder/db')
  session.start

Usage with a remote Server

Example of a rails config/application.rb file:

config.neo4j.session_options = { basic_auth: { username: 'foo', password: 'bar'} } 
config.neo4j.session_type = :server_db 
config.neo4j.session_path = 'http://localhost:7474'
config.neo4j[:cache_class_names] = true #improves Cypher performance

All options from HTTParty are available. For more Information see http://rdoc.info/github/jnunemaker/httparty/HTTParty/ClassMethods

See https://gist.github.com/andreasronge/11189170 how to configure the Neo4j::Session with basic authentication from a none rails application.

The cache_class_names adds a _classname property to all nodes during node creation that stores the object's class name. This prevents an extra query to the database when wrapping the node in a Ruby class. To change the property name, add this to application.rb:

config.neo4j[:class_name_property] = :new_name

Usage from heroku

Add a Neo4j db to your application:

heroku addons:add graphenedb

Example of a rails config/application.rb file:

config.neo4j.session_type = :server_db 
config.neo4j.session_path = ENV["GRAPHENEDB_URL"] || 'http://localhost:7474'

Usage from Rails

 rails new myapp -m http://andreasronge.github.com/neo4j/neo4j.rb -O
 cd myapp
 rake neo4j:install[community-2.0.2]
 rake neo4j:start

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

Or manually modify the rails config file config/application.rb:

require 'neo4j/railtie'

module Blog
  class Application < Rails::Application
     # This is for embedded db, only available from JRuby
     #config.neo4j.session_type = :embedded_db # or server_db
     #config.neo4j.session_path = File.expand_path('neo4j-db', Rails.root) # or http://localhost:port
  end
end

You can skip Active Record by using the -O flag when generating the rails project.

Property

All properties for Neo4j::ActiveNode objects must be declared (unlike neo4j-core nodes). Properties are declared using the property method which is the same as attribute from the active_attr gem.

Example:

class Post
  include Neo4j::ActiveNode
  property :title, index: :exact
  property :text, default: 'bla bla bla'
  property :score, type: Integer, default: 0

  validates :title, :presence => true
  validates :score, numericality: { only_integer: true }

  before_save do
    self.score = score * 100
  end

  has_n :friends
end

Properties can be indexed using the index argument on the property method, see example above.

Property Index

To declare a index on a property

class Person
  include Neo4j::ActiveNode
  property :name, index: :exact
end

Only exact index is currently possible.

Indexes can also be declared like this:

class Person
  include Neo4j::ActiveNode
  property :name
  index :name
end

Property Constraint

You can declare that a property should have a unique value.

class Person
  property :id_number, constraint: :unique # will raise an exception if id_number is not unique
end

Notice an unique validation is not enough to be 100% sure that a property is unique (because of concurrency issues, just like ActiveRecord). Constraints can also be declared just like indexes separately, see above.

Property Serialization

Pass a property name as a symbol to the serialize method if you want to save a hash or an array with mixed object types* to the database.

class Student
  include Neo4j::ActiveNode

  property :links

  serialize :links
end

s = Student.create(links: { neo4j: 'http://www.neo4j.org', neotech: 'http://www.neotechnology.com' })
s.links
# => {"neo4j"=>"http://www.neo4j.org", "neotech"=>"http://www.neotechnology.com"}
s.links.class
# => Hash

Neo4j.rb serializes as JSON by default but pass it the constant Hash as a second parameter to serialize as YAML. Those coming from ActiveRecord will recognize this behavior, though Rails serializes as YAML by default.

*Neo4j allows you to save Ruby arrays to undefined or String types but their contents need to all be of the same type. You can do user.stuff = [1, 2, 3] or user.stuff = ["beer, "pizza", "doritos"] but not user.stuff = [1, "beer", "pizza"]. If you wanted to do that, you could call serialize on your property in the model.

Callbacks

Implements like Active Records the following callback hooks:

  • initialize
  • validation
  • find
  • save
  • create
  • update
  • destroy

created_at, updated_at

See http://neo4j.rubyforge.org/classes/Neo4j/Rails/Timestamps.html

class Blog
  include Neo4j::ActiveNode
  has_n(:comments, on_updated: set_timestamp, :on_created: set_timestamp)
  property :updated_at  # will automatically be set when model changes
end

Relationship callbacks

You can specify before and after callback methods on all declared relationships. There are some simple guidelines:

  • Your callback methods must accept two parameters: the first for the "from" node, the second for the "to" node.
  • If your before callback explicitly returns false, the relationship will not be created.
  • has_n relationship setters will return false if a callback explicitly returns false, has_one will always return the object passed into the setter, so check for failure if you need to take action in your app
class Topic
  include Neo4j::ActiveNode
  property :last_post, type: DateTime

  has_n(:posts, before: :check_poster, after: :set_topic_stats).from(Post, :topics)
  has_one(:poster).to(User)

  private

  def check_poster(from, to)
    return false if from.poster.nil?
  end

  def set_topic_stats(from, to)
   self.poster = from.poster
   self.last_post = DateTime.now
   self.save
  end
end

class Post
  include Neo4j::ActiveNode

  has_one(:poster, before: :check_post_privileges, after: :notify_friends).from(User)

  def check_post_privileges(from, to)
    return false if to.allowed_to_post == false
  end

  def notify_friends(from, to)
    # call a method to notify the poster's friends of a new post
    from.notify_friends(self)
  end
end

# elsewhere in the app...

if !(@topic.posts << post)
  #raise an error, notify someone, do something...
end

# but what if...
@post.poster = @user
# has_one callbacks do not return false, so check after setting
if @topic.poster.nil?
  notify_admins_banned_user_is_being_shady
end

Validation

Support the Active Model validation, such as:

  • validates :age, presence: true
  • validates_uniqueness_of :name, :scope => :adult

Query

The Neo4j::ActiveNode module has a few ways to query:

id_property (Primary Key)

Nodes and relationships have unique ids - neo_id. Since Neo4j's id may be recycled you can define which key should act as primary key on Neo4j::ActiveNode classes instead of using the internal Neo4j ids. The Neo4j internal id can always be accessed using Neo4j::ActiveNode#neo_id You can define which key should act as primary key (Neo4j::ActiveNode#id) either on your models or globally in Neo4j::Config or Rails configuration, for example see config/application.rb or model

UUID

An id can be generated for you and stored as an indexed property.

Example

class Person
  include Neo4j::ActiveNode
  id_property :my_uuid, auto: :uuid
end

User Defined Id

The on parameter tells which method is used to generate the unique id.

class Person
  include Neo4j::ActiveNode
  id_property :personal_id, on: :phone_and_name

  property :name
  property :phone

  def phone_and_name
    self.name + self.phone # strange example ...      
  end
end

There will always be a unique contraint on all id_properties.

Finding a node

Find returns one ruby object or nil if none was found.

# Example, find by id (neo_id or defined by id_property)
Blog.find(4242)

Basic querying

All of the following result in enumerable results:

# Find all blog models
Blog.all

# Limit results
Blog.where(title: 'neo4j')

# Order
Person.where(age: 30).order(age: :desc).limit(5)

Detailed querying

The ActiveNode.query_as and ActiveNode#query_as methods return Query objects which allow you to chain together query clauses and then determine how you'd like the results to be returned.

# Find all comments for any blog which have the world "pottery"
result = Blog.query_as(:blog).match("blog<-[:COMMENTS_ON]-(comment:Comment)").where(comment: {body: /pottery/}).pluck(:blog, :comment)
result.first.blog     # First blog
result.first.comment  # Comment

# Find all comment authors who's age is greater than 30
blog.query_as(:blog).match("blog<-[:COMMENTS_ON]-(:Comment)<-[:AUTHORED]-(author:Person)").where("author.age > 30").pluck(:author)

# You can even get start with basic query methods and then transfer to detailed querying
blog.where(title: 'neo4j').query_as(:blog).match("blog<--(comment:Comment)").where("comment.created_at >= '2014-07-09'").pluck(:comment)

For more information on Query objects, see the Neo4j::Core::Query documentation (LINK TO YARD DOCS NEEDED)

Orm_Adapter

You can also use the orm_adapter API, by calling #to_adapter on your class. See the API, https://github.com/ianwhite/orm_adapter

Cypher

Same as in neo4j-core, see Neo4j::Session.query

Relationship

Relationships does not have to be declared in advanced before using them (just like properties). Example:

blog.rels(dir: :outgoing, type: :comments)

See Neo4j-core API

Declared Relationships

Will be changed !

class Blog
  include Neo4j::ActiveNode
  has_n :comments
end
class Comment
  include Neo4j::ActiveNode
  has_one(:belongs_to_blog).from(:comments)
end

comment = Comment.create
blog = Blog.create
comment.belongs_to_blog = blog
blog.comments.to_a #=> includes comment

# or create a relationship object with properties
rel = Blog.comments.create(comment, since: 1994)

Similar to https://github.com/andreasronge/neo4j/wiki/Neo4j%3A%3ARails-Relationships

Notice, has_one relationships can be created and queries. Example

class Person
  include Neo4j::ActiveNode
  has_one(:address)
end

my_address = Address.create # a Neo4j::ActiveNode
Person.create(address: my_address)
Person.all(conditions: {address: my_address})

Also see the section on relationship callbacks from more info and examples.

QuickQuery (work in progress)

QuickQuery is intended as Cypher shorthand for quick query retrieval and property setting over defined models and declared relationships. It is friendly for ActiveRecord users who may not be Cypher experts and anyone who wants to return whole objects for inclusion in Views. Its methods generate queries using Neo4j::Core::Query, which is the preferred method of performing Cypher queries for maximum flexibility and power.

Limitations as of July 11, 2014

  • Does not return relationship objects

Getting Started

Start a QuickQuery by calling the qq method on any ActiveModel class or instance.

Student.qq
#or
s = Student.all.first
s.qq

Identifiers

All queries generated using QuickQuery will automatically assign Cypher identifiers to all nodes and relationships. Nodes start with :n1 and increment, relationships start with :r1. You can set your own identifiers at any time by passing a symbol along with your method.

Student.qq(:student)
#or
s = Student.first
s.qq(:student)

Matching properties

To match properties, use hashes of properties and values. If you want to do a regex match or use greater than, less than, etc,... you can use a string. Make sure you set your integer fields as Integer (or Date or something other than String) in your models or your Cypher comparisons won't work.

These can be passed during any traversal or by using the where method. If you omit an identifier in where, it will assume you mean the newest identifier in the chain (the "node on deck") and enter it for you.

Student.qq.where(age: 30)
#or
Student.qq.where(:n1, age: 30)
#or
Student.qq(:student).where(age: 30)
#or
Student.qq(:student).where(:student, age: 30)
#or
Student.qq.where('age > 29')
#or you can specify an existing identifier in a string
Student.qq.where('n1.age > 29')

where can be used at any place in your traversal if you want to use an identifier.

Relationship Traversal

QuickQuery uses relationships defined in models to generate chainable traversal methods. They expect your relationships to have explicitly defined source and destination classes:

class Student
  include Neo4j::ActiveNode
  property :name
  has_n(:lessons).to(Lesson)
end

class Lesson
  include Neo4j::ActiveNode
  property :name
  has_n(:students).from(Student, :lessons)
  has_n(:teachers).from(Teacher, :teaching)
end

class Teacher
  include Neo4j::ActiveNode
  property :name
  has_n(:teaching).to(Lesson, :teachers)
end

Creating relationships in this way allows QuickQuery to anticipate where your relationship is going and insert the appropriate Label into the query. If you do not want to define relationships, see the section on the match method.

Traversing a relationship is easy.

#all students, age greater than 29, taking history
Student.qq(:students).where('students.age > 29').lessons(name: 'history')
#starting from a single student, find all of their classmates who are older than 21
#this time, we don't need to specify an identifier in the string because it will see it's missing and add it for us
s.qq(:me).lessons.students('age > 29')

You can also specify properties of relationships by using rel_as to set an identifier and rel_where to set parameters.

#all students who have lessons with grades equal to a+
Student.qq(:students).lessons(rel_as: :lesson_stats, rel_where: { grade: 'a+' }).return(:students)

Returning Objects

Return options are specified by an optional return method and either to_a or to_a!.

return is optional and accepts a symbol of the identifier to return as well as a boolean if you want distinct results. Very often, traversals overlap on nodes, so enabling distinct is helpful.

to_a returns an array, as you'd probably guess. If you specified return parameters, it will respect those; otherwise, it will return the last object touched in your traversal.

to_a! is identical to to_a except distinct is turned on.

#return all teachers who have students aged 21. Implicit return of teachers because it is the last link in the chain.
Student.qq(:student).where(:student, age: 21).lessons.teachers.to_a
#or for distinct results...
Student.qq(:student).where(:student, age: 21).lessons.teachers.to_a!

#return all lessons with students age 21 that are taught by Mr Smith, explicit return with distinct
Student.qq(:student).where(:student, age: 21).lessons(:l).teachers(name: 'Mr Smith').return(:l, true).to_a

It is NOT possible to return relationships at the moment.

Setting properties

Use set_props to set specific properties that match criteria. Use set to completely overwrite ALL properties that match the criteria. Call to_a to commit.

#mark all lessons with fewer than 20 students enrolled as having seats available
Lesson.qq.where('students_enrolled < 20').set_props(seats_available: true).to_a

#mark all students enrolled in history with grades greater than 'b+' as honor students
Student.qq(:students).lessons(name: 'history 101', rel_where: { 'grade > b+' }.set_props(:students, honor_student: true).to_a

You must call to_a to commit changes.

Skip, Limit, and Order

It is possible to skip or limit results to a certain number of records as well as set the order in which they are returned to you.

Pass skip or limit an integer to use them. Pass order a symbol of the property and a boolean true if you want it to descend. It ascends by default, per Neo4j default settings.

#return 10 distinct teachers, skipping 5, sort by teacher age, who teach lessons that have students. 
Student.qq.lessons.teachers.limit(10).skip(5).order(:age, true).to_a!

Match

If you want to perform a Cypher match that is not part of your models or do something not explicitly supported, you can use the match method. Pass it a string of Cypher. Beware, though: once you do this, behavior may be unpredictable since QuickQuery will not be able to increment your nodes for you.

Student.qq.lessons.teachers(:t).match('t-[:likes]-o').return(:o).to_a

If you find yourself doing this often, you should consider using Neo4j::Core::Query directly using the query_as method on any class or node.

to_cypher

At any time except after a return or to_a, call to_cypher to see the Cypher query generated by your method chain.

Inheritance

Index, properties and declared relationships (#has_n and #has_one) are inherited.

Example:

  class Vehicle
    include Neo4j::ActiveNode
    property :name, type: String
    index :name
  end

  class Car < Vehicle
    property :model
    index :model
  end

   bike = Vehicle.create(name: 'bike')
   volvo = Car.create(name: 'volvo', model: 'v60')
   saab = Car.create(name: 'saab', model: '900')

   Car.find(name: 'volvo') # => volvo
   Vehicle.find(name: 'volvo') # => volvo

   Car.find(model: '900') # => saab
   Vehicle.find(model: '900') # => saab

Neo4j with devise

See https://github.com/benjackson/devise-neo4j. Use the 2.0.0.alpha.X versions. See example application: https://github.com/andreasronge/rails-devise

Rails Example

See Rails 4 example: https://github.com/andreasronge/neo4j/tree/3.0/example/blog

Neo4j Core

The neo4j gem uses the neo4j-core gem, see https://github.com/andreasronge/neo4j-core See https://github.com/andreasronge/neo4j-core/tree/3.0

Clone this wiki locally