public
Description: A Graph Database for JRuby. It uses the java lib neo4j as storage and lucene for quering/indexing
Homepage: http://neo4j.lighthouseapp.com/
Clone URL: git://github.com/andreasronge/neo4j.git
neo4j /
name age message
file .gitignore Tue Jun 16 07:26:37 -0700 2009 Ignore files generated by 'rake gem:install' [Martin Kleppmann]
directory .idea/ Sun Jun 07 13:41:18 -0700 2009 Refactoring: renamed container_node to index_node [andreasronge]
file CHANGELOG Thu Jun 25 11:09:31 -0700 2009 Release of 0.3.0 [andreas]
file LICENSE Thu Oct 23 01:38:33 -0700 2008 prepare for rubyforge release of gem. Added lic... [andreasronge]
file README.rdoc Thu Jun 25 11:09:31 -0700 2009 Release of 0.3.0 [andreas]
file Rakefile Thu Jun 25 06:33:32 -0700 2009 removed gem dependencies - will add a complete ... [andreas]
directory examples/ Thu Jun 25 10:23:18 -0700 2009 Got replication of a simple node space working ... [andreas]
directory lib/ Thu Jun 25 12:26:23 -0700 2009 Merged with ept/neo4j/master branch Should not ... [andreas]
directory test/ Thu Jun 25 11:29:55 -0700 2009 Merge branch 'master' of git://github.com/andre... [Martin Kleppmann]

Neo4j.rb

Neo4j.rb is a graph database for JRuby.

It provides:

  • Mapping of ruby objects to nodes in networks rather than in tables.
  • Dynamic, and schema-free - no need to declare nodes/properties/relationships in advance.
  • Storage of ruby object to a file system.
  • Fast traversal of relationships between nodes in a huge node space.
  • Transaction with rollbacks support.
  • Indexing and querying of ruby objects.
  • Can be used instead of ActiveRecord in Ruby on Rails or Merb
  • Can be accessible as REST resources.

It uses two powerful and mature java libraries:

Status

  • There are over 300 RSpecs.
  • Has been tested with a simple rails application, used Neo4j.rb instead of ActiveRecord
  • Has been load tested (loaded 18000 nodes and done queries/traversal in several threads.)
  • Has not been used in production yet (as far as I know).

Project information

Presentation Materials and other URLs

Contributing

Have you found a bug, need help or have a patch ? Just clone neo4j.rb and send me a pull request or email me. Do you need help - send me an email (andreas.ronge at gmail dot com). Please also check/add issues at lighthouse, neo4j.lighthouseapp.com

License

Content

This page contains the following information:

  • Installation guide
  • Three Minute Tutorial
  • Ten Minute Tutorial
  • Lucene API Documentation
  • Neo4j API Documentation
  • Extensions: REST (see Neo4j::RestMixin)
  • Ruby on Rails with Neo4j.rb

Installation

To install it:

   gem install neo4j

To install from the latest source:

  git clone git://github.com/andreasronge/neo4j.git
  cd neo4j
  rake gem:install

This has been verified to work on JRuby 1.3.0

Running all RSpecs

To check that neo4j.rb is working:

  cd neo4j # the folder containing the Rakefile
  rake     # you may have to type jruby -S rake depending how you installed JRuby

Three Minute Tutorial

Neo node space consists of three basic elements: nodes, relationships that connect nodes and properties attached to both nodes and relationships. All relationships have a type, for example if the node space represents a social network, a relationship type could be KNOWS. If a relationship of the type KNOWS connects two nodes, that probably represents two people that know each other. A lot of the semantics, the meaning, of a node space is encoded in the relationship types of the application.

Creating Nodes

Example of creating a neo4j node:

  require "rubygems"
  require 'neo4j'

  Neo4j::Transaction.run do
    node = Neo4j::Node.new
  end

Transactions

Almost all Neo4j operation must be wrapped in a transaction as shown above. In the following examples we assume that the operations are inside an Neo4j transaction. Neo4j.rb supports auto commits without wrapping Neo4j operations in Neo4j::Transactions. To enable that require the file ‘neo4j/auto_tx’ instead of ‘neo4j’

Example

  require 'neo4j/auto_tx'

  # no need to wrap operations in Neo4j::Transaction.run do .. end
  Node.new

This feature should be used for testing and development purpose.

Setting Properties

Example of setting properties

  node = Neo4j::Node.new
  node[:name] = 'foo'
  node[:age]  = 123
  node[:hungry] = false
  node[4] = 3.14

Getting Properties

Example of getting properties

  node[:name] # => 'foo'

Creating Relationships

Example of creating a relationship

  node1 = Neo4j::Node.new
  node2 = Neo4j::Node.new
  node1.relationships.outgoing(:friends) << node2  # connects node1 to node2 with relationship type 'friends'

Accessing Relationships

Example of getting relationships

  node1.relationships.empty? # => false

  # The relationships method returns an enumeration of relationship objects.
  # The nodes method on the relationships returns the nodes instead.
  node1.relationships.nodes.include?(node2) # => true

  node1.relationships.first # => the first relationship this node1 has which is between node1 and node2 of any type
  node1.relationships.nodes.first # => node2  first node of any relationship type
  node1.relationships.incoming(:friends).nodes.first # => node1  first node of relationship type 'friends'
  node2.relationships.incoming(:friends).first # => a relationship object between node1 and node2

Properties on Relationships

Example of setting properties on relationships

  rel = node1.relationships(:friends).first
  rel[:since] = 1982
  node1.relationships.first[:since] # => 1982 (there is only one relationship defined on node1 in this example)

The Node and NodeMixin

Here is the complete implemenation of the Neo4j::Node class

  module Neo4j
    class Node
      include Neo4j::NodeMixin
    end
  end

In the next tutorial you will learn how to use this Neo4j::NodeMixin in your own domain model classes.

Ten Minute Tutorial

Creating a Model

The following example specifies how to map a Neo4j node to a Ruby Person instance.

  require "rubygems"
  require "neo4j"

  class Person
    include Neo4j::NodeMixin

    # define Neo4j properties
    property :name, :salary.

    # define an one way relationship to any other node
    has_n :friends

    # adds a lucene index on the following properties
    index :name, :salary
    index 'friends.age' # index each friend age as well
  end

Neo properties and relationships are declared using the ‘property’ and ‘has_n’/’has_one’ NodeMixin class method. Adding new types of properties and relationships can also be done without declaring those propertie/relationships by using the operator ’[]’ on Neo4j::NodeMixin and the

 '<<' on the Neo4j::Relationships::RelationshipTraverser.

By using the NodeMixin all instances of the Person class can now be stored in the Neo4j node space and be retrieved/queried by traversing the node space or performing Lucene queries.

A lucene index will be updated when the name or salary property changes. The salary of all friends are also indexed which means we can query for people who has friends with a certain salary.

Creating a node

Creating a Person node instance

  person = Person.new

Setting properties

Setting a property:

  person.name = 'kalle'
  person.salary  = 10000

If a transaction is not specified then the operation will automatically be wrapped in a transaction.

Dynamic Properties

Notice that it is not required to specify which attributes should be available on a node. Any attributes can be set using the [] operator.

Example:

  person['a_not_defined_property'] = 'hello'

Dynamic Relationships

Like dynamic properties, relationships does not have to be defined using has_n or has_one for a class. A relationship can be added at any time on any node.

Example:

  person.relationships.outgoing(:best_friends) << other_node
  person.relationship(:best_friend).end_node # => other_node (if there is only one relationship of type 'best_friend' on person)

Finding Nodes and Queries

There are three ways of finding/quering nodes in neo4j - by doing a traversal or by using lucene queries or using the unique neo4j id (Neo4j::NodeMixin#neo_node_id). When doing a traversal one start from a node and travers one or more relationships (one or more levels deep). This start node can be either the reference node which is always found (Neo4j.ref_node) or by finding a start node from a lucene query.

Lucene Queries

There are different ways to write lucene queries. Using a hash:

  Person.find (:name => 'kalle', :age => 20..30)  # find people with name kalle and age between 20 and 30

or using the lucene query language:

  Person.find("name:kalle AND salary:[10000 TO 30000]")

The Lucene query language supports wildcard, grouping, boolean, fuzzy queries, etc… For more information see: lucene.apache.org/java/2_4_0/queryparsersyntax.html

Sorting, example

  Person.find(:name => 'kalle').sort_by(:salary)
  Person.find(:name => 'kalle').sort_by(Desc[:salary], Asc[:country])
  Person.find(:name => 'kalle').sort_by(Desc[:salary, :country])

Search Results

The query is not performed until the search result is requested. Example of using the search result.

  res = Person.find(:name => 'kalle')
  res.size  # => 10
  res.each {|x| puts x.name}
  res[0].name = 'sune'

Creating a Relationships

Adding a relationship between two nodes:

  person2 = Person.new
  person.friends << person2

The person.friends returns an object that has a number of useful methods (it also includes the Enumerable mixin). Example

  person.friends.empty? # => false
  person.friends.first  # => person2
  person.friends.include?(person2) # => true

Deleting a Relationship

To delete the relationship between person and person2:

  person.relationships[person2].delete

If a node is deleted then all its relationship will also be deleted Deleting a node is performed by using the delete method:

  person.delete

Node Traversals

The has_one and has_many methods create a convenient method for traversals and managing relationships to other nodes. Example:

  Person.has_n :friends # generates the friends instance method
  # all instances of Person now has a friends method so that we can do the following
  person.friends.each {|n| ...  }

Traversing using a filter

  person.friends{ salary == 10000 }.each {|n| ...}

Traversing with a specific depth (depth 1 is default)

  person.friends{ salary == 10000}.depth(3).each { ... }

There is also a more powerful method for traversing several relationships at the same time - Neo4j::NodeMixin#traverse, see below.

Example on Relationships

In the first example the friends relationship can have relationships to any other node of any class. In the next example we specify that the ‘acted_in’ relationship should use the Ruby classes Actor, Role and Movie. This is done by using the has_n class method:

      class Role
        include Neo4j::RelationshipMixin
        # notice that neo4j relationships can also have properties
        property :name
      end

      class Actor
        include Neo4j::NodeMixin

        # The following line defines the acted_in relationship
        # using the following classes:
        # Actor[Node] --(Role[Relationship])--> Movie[Node]
        #
        has_n(:acted_in).to(Movie).relationship(Role)
      end

      class Movie
        include Neo4j::NodeMixin
        property :title
        property :year

        # defines a method for traversing incoming acted_in relationships from Actor
        has_n(:actors).from(Actor, :acted_in)
      end

Creating a new Actor-Role-Movie relationship can be done like this:

      keanu_reeves = Actor.new
      matrix       = Movie.new
      keanu_reeves.acted_in << matrix

or you can also specify this relationship on the incoming node (since we provided that information in the has_n methods).

      keanu_reeves = Actor.new
      matrix       = Movie.new
      matrix.actors << keanu_reeves

Example of accessing the Role relationship object between an Actor and a Movie

      keanu_reeves.relationships.outgoing(:acted_in)[matrix].name = 'neo'

More information about neo4j can be found after the Lucene section below.

The Lucene Module

You can use this module without using the Neo4j module.

Lucene provides:

  • Flexible Queries - Phrases, Wildcards, Compound boolean expressions etc…
  • Field-specific Queries eg. title, artist, album
  • Sorting
  • Ranked Searching

Lucene Document

In lucene everything is a Document. A document can represent anything textual: Word Document, DVD (the textual metadata only), or a Neo4j.rb node. A document is like a record or row in a relationship database.

The following example shows how a document can be created by using the ’’<<’’ operator on the Lucene::Index class and found using the Lucene::Index#find method.

Example of how to write a document and find it:

    require 'lucene'

    include Lucene

    # the var/myindex parameter is either a path where to store the index or
    # just a key if index is kept in memory (see below)
    index = Index.new('var/myindex')

    # add one document (a document is like a record or row in a relationship database)
    index << {:id=>'1', :name=>'foo'}

    # write to the index file
    index.commit

    # find a document with name foo
    # hits is a ruby Enumeration of documents
    hits = index.find{name == 'foo'}

    # show the id of the first document (document 0) found
    # (the document contains all stored fields - see below)
    hits[0][:id]   # => '1'

Notice that you have to call the commit method in order to update the index on the disk/RAM. By performing several update and delete operations before a commit will be much faster then performing commit after each operation.

Keep indexing on disk

By default Neo4j::Lucene keeps indexes in memory. That means that when the application restarts the index will be gone and you have to reindex everything again.

To keep indexes in memory:

   Lucene::Config[:store_on_file] = true
   Lucene::Config[:storage_path] => '/home/neo/lucene-db'

When creating a new index the location of the index will be the Lucene::Config[:storage_path] + index path Example:

   Lucene::Config[:store_on_file] = true
   Lucene::Config[:storage_path] => '/home/neo/lucene-db'
   index = Index.new('/foo/lucene')

The example above will store the index at /home/neo/lucene-db/foo/lucene

Indexing several values with the same key

Let say a person can have several phone numbers. How do we index that ?

  index << {:id=>'1', :name=>'adam', :phone => ['987-654', '1234-5678']}

Id field

All Documents must have one id field. If one is not specified it is :id of type String. A different id can be specified using the field_infos id_field property on the index:

  index = Index.new('some/path/to/the/index')
  index.field_infos.id_field = :my_id

To change the type of the my_id from String to a different type see below.

Conversion of types

Lucene.rb can handle type conversion for you. (The java lucene library stores all the fields as Strings) For example if you want the id field to be a fixnum

    require 'lucene'
    include Lucene

    index = Index.new('var/myindex')  # store the index at dir: var/myindex
    index.field_infos[:id][:type] = Fixnum

    index << {:id=>1, :name=>'foo'} # notice 1 is not a string now

    index.commit

    # find that document, hits is a ruby Enumeration of documents
    hits = index.find(:name => 'foo')

    # show the id of the first document (document 0) found
    # (the document contains all stored fields - see below)
    doc[0][:id]   # => 1

If the field_info type parameter is not set then it has a default value of String.

Storage of fields

By default only the id field will be stored. That means that in the example above the :name field will not be included in the document.

Example

    doc = index.find('name' => 'foo')
    doc[:id]   # => 1
    doc[:name] # => nil

Use the field info :store=true if you want a field to be stored in the index (otherwise it will only be searchable).

Example

    require 'lucene'
    include Lucene

    index = Index.new('var/myindex')  # store the index at dir: var/myindex
    index.field_infos[:id][:type] = Fixnum
    index.field_infos[:name][:store] = true # store this field

    index << {:id=>1, :name=>'foo'} # notice 1 is not a string now

    index.commit

    # find that document, hits is a ruby Enumeration of documents
    hits = index.find('name' => 'foo')

    # let say hits only contains one document so we can use doc[0] for that one
    # that document contains all stored fields (see below)
    doc[0][:id]   # => 1
    doc[0][:name] # => 'foo'

Setting field infos

As shown above you can set field infos like this

  index.field_infos[:id][:type] = Fixnum

Or you can set several properties like this:

  index.field_infos[:id] = {:type => Fixnum, :store => true}

Simple Queries

Lucene.rb support search in several fields: Example

    # finds all document having both name 'foo' and age 42
    hits = index.find('name' => 'foo', :age=>42)

Range queries

    # finds all document having both name 'foo' and age between 3 and 30
    hits = index.find('name' => 'foo', :age=>3..30)

Lucene Queries

If the query is string then the string is a lucene query.

  hits = index.find('name:foo')

For more information see: lucene.apache.org/java/2_4_0/queryparsersyntax.html

Advanced Queries (DSL)

The queries above can also be written in a lucene.rb DSL:

    hits = index.find { (name == 'andreas') & (foo == 'bar')}

Expression with OR (|) is supported, example

   # find all documents with name 'andreas' or age between 30 and 40
    hits = index.find { (name == 'andreas') | (age == 30..40)}

Sorting

Sorting is specified by the ‘sort_by’ parameter Example

  hits = index.find(:name => 'foo', :sort_by=>:category)

To sort by several fields:

  hits = index.find(:name => 'foo', :sort_by=>[:category, :country])

Example sort order

  hits = index.find(:name => 'foo', :sort_by=>[Desc[:category, :country], Asc[:city]])

Thread-safety

The Lucene::Index is thread safe. It guarantees that an index is not updated from two thread at the same time.

Lucene Transactions

Use the Lucene::Transaction in order to do atomic commits. By using a transaction you do not need to call the Index.commit method.

Example:

    Transaction.run do |t|
      index = Index.new('var/index/foo')
      index << { id=>42, :name=>'andreas'}
      t.failure  # rollback
    end

    result = index.find('name' => 'andreas')
    result.size.should == 0

You can find which documents are uncommited by using the uncommited index property.

Example

      index = Index.new('var/index/foo')
      index.uncommited #=> [document1, document2]

Notice that even if it looks like a new Index instance object was created the index.uncommited may return an not empty array. This is because Index.new is a singleton - a new instance object is not created.

The Neo4j Module

The Neo4j module is used to map Ruby objects to nodes and relationships in a network. It supports two different ways of retrieval/quering:

  • Neo4j traversal, Neo4j::NodeMixin#traverse (or Neo4j::NodeMixin#has_n)
  • Lucene indexes, Neo4j::NodeMixin#find

Start and Stop of the Neo4j

Unlike the Java Neo4j implementation it is not neccessarly to start Neo4j. It will automatically be started when needed. It also uses a hook to automatically shutdown Neo4j. Shutdown of neo4j can also be done using the stop method, example:

  Neo4j.stop

Neo4j Configuration

Before using Neo4j the location where the database is stored on disk should be configured. The neo4j configuration is kept in the Neo4j::Config class:

   Neo4j::Config[:storage_path] = '/home/neo/neodb'

Lucene Integration

Neo4j.rb uses the Lucene module. That means that the Neo4j::NodeMixin has method for both traversal and lucene queries/indexing.

Lucene Configuration

By default lucene indexes are kept in memory. Keeping index in memory will increase the performance of lucene operations (such as updating the index).

Example to configure Lucene to store indexes on disk instead

   Lucene::Config[:store_on_file] = true
   Lucene::Config[:storage_path] => '/home/neo/lucene-db'

Lucene Index in Memory

If index is stored in memory then one needs to reindex all nodes when the application starts up again.

   MyNode.update_index # will traverse all MyNode instances and (re)create the lucene index in memory.

Neo4j::NodeMixin

Neo4j::NodeMixin is a mixin that lets instances to be stored as a node in the neo node space on disk. A node can have properties and relationships to other nodes.

Example of how declare a class that has this behaviour:

  class MyNode
     include Neo4j::NodeMixin
  end

Create a Node

  node = MyNode.new

Delete a Node

The Neo4j::NodeMixin mixin defines a delete method that will delete the node and all its relationships.

Example:

  node = MyNode.new
  node.delete

The node in the example above will be removed from the neo database on the filesystem and the lucene index

Node Properties

In order to use properties they have to be declared first

  class MyNode
     include Neo4j::NodeMixin
     property :foo, :bar
  end

These properties (foo and bar) will be stored in the Neo database. You can set those properties:

  # create a node with two properties in one transaction
  node = MyNode.new { |n|
     n.foo = 123
     n.bar = 3.14
  }

  # access those properties
  puts node.foo

You can also set a property like this:

    f = SomeNode.new
    f.foo = 123

Neo4j.rb supports properties to by of type String, Fixnum, Float and true/false

Property Types and Marshalling

If you want to set a property of a different type then String, Fixnum, Float or true/false you have to specify its type.

Example, to set a property to any type

  class MyNode
    include Neo4j::NodeMixin
    property :foo, :type => Object
  end

  node = MyNode.new
  node.foo = [1,"3", 3.14]

  Neo4j.load(node.neo_node_id).foo.class # => Array

Property of type Date and DateTime

Example of using Date queries:

  class MyNode
    include Neo4j::NodeMixin
    property :since, :type => Date
    index :since, :type => Date
  end

  node.since = Date.new 2008,05,06
  MyNode.find("born:[20080427 TO 20100203]")[0].since # => Date 2008,05,06

Example of using DateTime queries:

  class MyNode
    include Neo4j::NodeMixin
    property :since, :type => DateTime
    index :since, :type => DateTime
  end

  node.since = DateTime.civil 2008,04,27,15,25,59
  MyNode.find("since:[200804271504 TO 201002031534]")[0].since # => DateTime ...

Only UTC timezone is allowed.

Finding all nodes

To find all nodes of a specific type use the all method. Example

  class Car
    include Neo4j::Node
    property :wheels
  end

  class Volvo < Car
  end

  v = Volvo.new
  c = Car.new

  Car.all   # will return all relationships from the reference node to car obejcts
  Volvo.all # will return the same as Car.all

To return nodes (just like the relationships method)

  Car.all.nodes    # => [c,v]
  Volvo.all.nodes  # => [c,v]

Relationship has_n and has_one

Neo relationships are none symmetrical. That means that if A has a relationship to B then it may not be true that B has a relationship to A.

Relationships can be declared by using the ‘has_n’ or ‘has_one’ Neo4j::NodeMixin class methods.

has_n

The has_n Neo4j::NodeMixin class method creates a new instance method that can be used for both traversing and adding new objects to a specific relationship type.

For example, let say that Person can have a relationship to any other node class with the type ‘friends’:

  class Person
     include Neo::Node
     has_n :knows  # will generate a knows method for outgoing relationships
  end

The generated knows method will allow you to add new relationships, example:

  me = Person.new
  neo = Person.new
  me.knows << neo  # me knows neo but neo does not know me

You can add any object to the ‘knows’ relationship as long as it includes the Neo4j::NodeMixin, example:

  person = Person.new
  car = Volvo.new # Volvo is a class that includes the Neo4j::NodeMixin
  person.knows << car

If you want to express that the relationship should point to a specific class use the ‘to’ method on the has_n method.

  class Person
     include Neo::Node
     has_n(:knows).to(Person)
  end

It is also possible to generate methods for incoming relationships by using the ‘from’ method on the has_n method.

Example:

  class Person
     include Neo::Node
     has_n :knows  # will generate a knows method for outgoing relationships
     has_n(:known_by).from(:knows)  #  will generate a known_by method for incomming knows relationship
  end

By doing this you can add a relationships on either the incoming or outgoing node. The from method can also take an additional class parameter if it has incoming nodes from a different node class (see the Actor-Role-Movie example at the top of this document).

Example of adding a ‘knows’ relationship from the other node:

  me = Person.new
  neo = Person.new
  neo.known_by << me # me knows neo but neo does not know me

The known_by method creates a ‘knows’ relationship between the me and neo nodes. This is the same as doing:

  me.knows << neo # me knows neo but neo does not know me

Relationship has_one

Example of has_one: A person can have at most one Address

      class Person
        include Neo4j::NodeMixin
        has_one(:address).to(Address)
      end

      class Address
        include Neo4j::NodeMixin
        property :city, :road
        has_n(:people).from(Person, :address)
      end

In the example above we have Neo4j.rb will generate the following methods

  • in Person, the method ’’address=’’ and ’’address’‘
  • in Address, the traversal method ’’people’’ for traversing incomming relationships from the Person node.

Example of usage:

      p = Person.new
      p.address = Address.new
      p.address.city = 'malmoe'

Or from the incoming ’’address’’ relationship

      a = Address.new {|n| n.city = 'malmoe'}
      a.people << Person.new

Relationship has_list

The has_n relationship will not maintain the order of when items are inserted to the relationship. If order should be preserved then use the has_list class method instead.

Example

  class Company
    has_list :employees
  end

  company = Company.new
  company.employees << employee1 << employee2

  # prints first employee2 and then employee1
  company.employees.each {|employee| puts employee.name}

Traversing Relationships

Each type of relationship has a method that returns an Enumerable object that enables you to traverse that type of relationship.

For example the Person example above declares one relationship of type friends. You can traverse all Person’s friend (depth 1 is default)

  f.friends.each { |n| puts n }

It is also possible to traverse a relationship of an arbitrary depth. Example finding all friends and friends friends.

  f.friends.depth(2).each { ...}

Traversing to the end of the graph

  f.friends.depth(:all).each { ...}

Filtering Nodes

If you want to find one node in a relationship you can use a filter. Example, let say we want to find a friend with name ‘andreas’

  n1 = Person.new
  n2 = Person.new {|n| n.name = 'andreas'}
  n3 = Person.new
  n1.friends << n2 << n3
  n1.friends{ name == 'andreas' }.to_a # => [n2]

The block { name == ‘andreas’ } will be evaluated on each node in the relationship. If the evaluation returns true the node will be included in the filter search result.

Traversing Nodes

The Neo4j::NodeMixin#traverse method is a more powerful method compared to the generated has_n and has_one methods. Unlike those generated method it can traverse several relationship types at the same time. The types of relationships being traversed must therefore always be specified in the incoming, outgoing or both method. Those three methods can take one or more relationship types parameters if more then one type of relationship should be traversed.

Traversing Nodes of Arbitrary Depth

The depth method allows you to specify how deep the traverse should be. If not specified only one level traverse is done.

Example:

  me.traverse.incoming(:friends).depth(4).each {} # => people with a friend relationship to me

Traversing Nodes With Several Relationship Types

It is possible to traverse sevaral relationship types at the same type. The incoming, both and outgoing methods takes list of arguments.

Example, given the following holiday trip domain:

  # A location contains a hierarchy of other locations
  # Example region (asia) contains countries which contains  cities etc...
  class Location
    include Neo4j::NodeMixin
    has_n :contains
    has_n :trips
    property :name
    index :name

  # A Trip can be specific for one global area, such as "see all of sweden" or
  # local such as a 'city tour of malmoe'
  class Trip
    include Neo4j::NodeMixin
    property :name
  end

  # create all nodes
  # ...

  # setup the relationship between all nodes
  @europe.contains << @sweden << @denmark
  @sweden.contains << @malmoe << @stockholm

  @sweden.trips << @sweden_trip
  @malmoe.trips << @malmoe_trip
  @malmoe.trips << @city_tour
  @stockholm.trips << @city_tour # the same city tour is available both in malmoe and stockholm

Then we can traverse both the contains and the trips relationship types Example:

  @sweden.traverse.outgoing(:contains, :trips).to_a # => [@malmoe, @stockholm, @sweden_trip]

It is also possible to traverse both incoming and outgoing relationships, example:

  @sweden.traverse.outgoing(:contains, :trips).incoming(:contains).to_a # => [@malmoe, @stockholm, @sweden_trip, @europe]

Traversing Nodes With a Filter

It’s possible to filter which nodes should be returned from the traverser by using the filter function. This filter function will be evaluated differently depending on if it takes one argument or no arguments, see below.

Filtering: Using Evaluation in the Context of the Current Node

If the provided filter function does not take any parameter it will be evaluted in the context of the current node being traversed. That means that one can writer filter functions like this:

  @sweden.traverse.outgoing(:contains, :trips).filter { name == 'sweden' }

Filtering: Using the TraversalPostion

If the filter method takes one parameter then it will be given an object of type TraversalPosition which contains information about current node, how many nodes has been returned, depth etc.

The information contained in the TraversalPostion can be used in order to decide if the node should included in the traversal search result. If the provided block returns true then the node will be included in the search result.

The filter function will not be evaluated in the context of the current node when this parameter is provided.

The TraversalPosition is a wrapper of java interface TraversalPosition, see api.neo4j.org/current/org/neo4j/api/core/TraversalPosition.html

For example if we only want to return the Trip objects in the example above:

  # notice how the tp (TraversalPosition) parameter is used in order to only
  # return nodes included in a 'trips' relationship.
  traverser = @sweden.traverse.outgoing(:contains, :trips).filter do |tp|
    tp.last_relationship_traversed.relationship_type == :trips
  end

  traverser.to_a # => [@sweden_trip]

Relationships

A relationship between two nodes can have properties just like a node.

Example:

  p1 = Person.new
  p2 = Person.new

  relationship = p1.friends.new(p2)

  # set a property 'since' on this relationship bewteen p1 and p2
  relationship.since = 1992

If a Relationship class has not been specified for a relationship then any properties can be set on the relationship. It has a default relationship class: Neo4j::DynamicRelation

If you instead want to use your own class for a relationship use the Neo4j::NodeMixin#has_n.relationship method, example:

      class Role
        # This class can be used as the relationship between two nodes
        # since it includes the following mixin
        include Neo4j::RelationMixin
        property :name
      end

      class Actor
        include Neo4j::NodeMixin
        # use the Role class above in the relationship between Actor and Movie
        has_n(:acted_in).to(Movie).relationship(Role)
      end

Finding Relationships

The Neo4j::NodeMixin#relationships method can be used to find incoming or outgoing relationship objects. Example of listing all types of outgoing (default) relationship objects (of depth one) from the me node.

  me.relationships.each {|rel| ... }

If we instead want to list the nodes that those relationships points to then the nodes method can be used.

  me.relationships.nodes.each {|rel| ... }

Listing all incoming relationship obejcts of any relationship type:

  me.relationships.incoming.each { ... }

Listing both incoming and outgoing relationship object of a specific type:

  me.relationships.both(:friends) { }

Finding one outgoing relationship of a specific type and node (you)

  me.relationships.outgoing(:friends)[you] # => [#<Neo4j::RelationshipMixin:0x134ae32]

Finding Relationships Example

Example, given we have the two nodes with a relationship between them:

  n1 = Person.new
  n2 = Person.new

  n1.friends << n2

Then we can find all incoming and outgoing relationships like this:

  n1.relationships.to_a # => [#<Neo4j::RelationshipMixin:0x134ae32]

A Neo4j::RelationshipMixin object represents a relationship between two nodes.

  n1.relationships[0].start_node # => n1
  n1.relationships[0].end_node # => n2

A RelationshipMixin contains the relationship type which connects it connects two nodes with, example:

  n1.relationships[0].relationship_type # => :friends

Relationships can also have properties just like a node (NodeMixin).

Finding outgoing and incoming relationships

If we are only interested in all incoming nodes, we can do

  n2.relationships.incoming # => [#<Neo4j::RelationshipMixin:0x134ae32]

Or outgoing:

  n1.relationships.outgoing # => [#<Neo4j::RelationshipMixin:0x134aea2]

To find a specific relationship use the [] operator:

  n1.relationships.outgoing[n2] = #<Neo4j::RelationshipMixin:0x134aea2

Or which is better performance wise (since only friends relationships are being traversed):

  n1.relationships.outgoing(:friends)[n2] = #<Neo4j::RelationshipMixin:0x134aea2

Deleting a relationship

Use the Neo4j::RelationshipMixin#delete method. For example, to delete the relationship between n1 and n2 from the example above:

    n1.relationships.outgoing(:friends)[n2].delete

Finding nodes in a relationship

If you do not want those relationship object but instead want the nodes you can use the ‘nodes’ method in the Neo4j::RelationshipMixin object.

For example:

  n2.relationships.incoming.nodes # => [n1]

Finding outgoing/incoming nodes of a specific relationship type

Let say we want to find who has my phone number and who consider me as a friend

  # who has my phone numbers
  me.relationships.incoming(:phone_numbers).nodes # => people with my phone numbers

  # who consider me as a friend
  me.relationships.incoming(:friends).nodes # => people with a friend relationship to me

Remember that relationships are not symmetrical. Notice there is also a otherway of finding nodes, see the Neo4j::NodeMixin#traverse method below.

Transactions

All operations that work with the node space (even read operations) must be wrapped in a transaction. For example all get, set and find operations will start a new transaction if none is already not runnig (for that thread).

If you want to perform a set of operation in a single transaction, use the Neo4j::Transaction.run method:

Example

  Neo4j::Transaction.run {
    node1.foo = "value"
    node2.bar = "hi"
  }

There is also a auto commit feature available which is enabled by requiring ‘neo4j/auto_tx’ instead of ‘neo4j’, see the three minutes tutorial above.

Rollback

Neo4j support rollbacks on transaction. Example: Example:

  include 'neo4j'

  node = MyNode.new

  Neo4j::Transaction.run { |t|
     node.foo = "hej"
     # something failed so we signal for a failure
     t.failure # will cause a rollback, node.foo will not be updated
  }

You can also run it without a block, like this:

   transaction = Neo4j::Transaction.new
   transaction.start
   # do something
   transaction.finish

Indexing

Properties and relationships which should be indexed by lucene can be specified by the index class method. For example to index the proeprties foo and bar

  class SomeNode
     include Neo4j::NodeMixin
     property :foo, :bar
     index :foo, :bar
  end

Everytime a node of type SomeNode (or a subclass) is create, deleted or updated the lucene index of will be updated.

Reindexing

Sometimes it’s neccessarly to change the index of a class after alot of node instances already have been created. To delete an index use the class method ‘remove_index’ To update an index use the class method ‘update_index’ which will update all already created nodes in the neo database.

Example

  require 'neo4j'
  require 'neo4j/extensions/tx_tracker' # needed for the update_index method
  class Person
    include Neo4j
    property :name, :age, :phone
    index :name, :age
  end

  p1 = Person.new {|n| n.name = 'andreas'; n.phone = 123}
  Person.find (:name => 'andreas') # => [p1]
  Person.find (:phone => 123) # => []

  # change index and reindex all person nodes already created in the neo database.
  Person.remove_index :name
  Person.index :phone  # add an index on phone
  Person.update_index

  Person.find (:name => 'andreas') # => []
  Person.find (:phone => 123) # => [p1]

In order to use the update_index method you must include the reindexer neo4j.rb extension. This extensions will keep a relationship to each created node so that it later can recreate the index by traversing those relationships.

Updating Lucene Index

The lucene index will be updated after the transaction commits. It is not possible to query for something that has been created inside the same transaction as where the query is performed.

Quering (using lucene)

You can declare properties to be indexed by lucene by the index method:

Example

      class Person
        include Neo4j::NodeMixin
        property :name, :age
        index :name, :age
      end

      node = Person.new
      node.name = 'foo'
      node.age  = 42

      Person.find(:name => 'foo', :age => 42) # => [node]

The query parameter (like property on a Neo4j::NodeMixin) can be of type String, Fixnum, Float, boolean or Range. The query above can also be written in a lucene query DSL:

      Person.find{(name =='foo') & (age => 42)} # => [node]

Or lucene query language:

     Person.find("name:foo AND age:42")

For more information see: lucene.apache.org/java/2_4_0/queryparsersyntax.html or the lucene module above.

Indexing and Property Types

In order to use range querie on numbers the property types must be converted. This is done by using the :type optional parameter:

  class Person
    include Neo4j::NodeMixin
    property :name, :age
    index :age, :type => Fixnum
  end

By using :type => Fixnum the age will be padded with ‘0’s (lucene only support string comparsion).

Example, if the :type => Fixnum was not specified then

  p = Person.new {|n| n.age = 100 }
  Person.find(:age => 0..8) # => [p]

Indexing and Quering Relationships

The Neo4j::NodeMixin#index method can be used to index relationships to other classes.

Example, let say we have to classes, Customer and Orders:

  class Customer
    include Neo4j::NodeMixin

    property :name

    # specifies outgoing relationships to Order
    has_n(:orders).to(Order)

    # create an index on customer-->order#total_cost
    index "orders.total_cost"
  end

  class Order
    include Neo4j::NodeMixin

    property :total_cost

    # specifies one incoming relationship from Customer
    has_one(:customer).from(Customer, :orders)

    # create an index on the order<--customer#name relationship
    index "customer.name"
  end

Notice that we can index both incoming and outgoing relationships.

Let’s create a customer and one order for that customer

    Neo4j::Transaction.run do
      cust = Customer.new
      order = Order.new
      cust.name = "kalle"
      order.total_cost = "1000"

      cust.orders << order
    end

Now we can find both Orders with a total cost between 500 and 2000 and Customers with name ‘kalle’ using lucene

Example:

   customers = Customer.find('orders.total_cost' => 500..2000, 'name' => 'kalle')

Or also possible from the other way:

   orders = Order.find('total_cost' => 500..2000, 'customer.name' => 'kalle')

Full text search

Neo4j supports full text search by setting the tokenized property to true on an index. (see JavaDoc for org.apache.lucene.document.Field.Index.ANALYZED).

  class Comment
    include Neo4j::NodeMixin

    property :comment
    index comment, :tokenized => true
  end

Unmarshalling

The neo module will automatically unmarshalling nodes to the correct ruby class. It does this by reading the classname property and loading that ruby class with that node.

  class Person
    include Neo::Node

    def hello
    end
  end

  f1 = Person.new {}

  # load the class again
  f2 = Neo4j.load(foo.neo_node_id)

  # f2 will now be new instance of Person, but will be == f1
  f1 == f2 # => true

Reference node

There is one node that can always be find - the reference node, Neo4j::ReferenceNode. Example:

    Neo4j.ref_node

This node has a relationship to all created nodes. You can add relationships from this node to your nodes.

Performance Issues

It is recommended to wrap several Neo4j operations including read operations in a singel transaction if possible for better performance. Updating a lucene index can be slow. A solution to this is to keep the index in memory instead of on disk.

I’m currently looking at how to scale neo4j.rb by a simple master-slave cluster by using REST, see the REST extension below.

Extensions: Replication

There is an experimental extension that makes it possible to replicate an neo4j database to another machine. For example how to use it see the test/replication/test_master.rb and test_slave.rb It has only been tested to work with a very simple node space.

Extension: REST

There is an REST extension to Neo4j.rb. It requires the following gems

 * Sinatra > 0.9.2 (I can only get the example.rb running by building from the latest from github).
 * Rack >= 1.0
 * json-jruby >= 1.1.6

For RSpec testing it also needs:

 * rack-test

For more information see the test/rest/example.rb or the examples/admin or Neo4j::RestMixin.

Ruby on Rails with Neo4j.rb

Neo4j.rb does work nicely with R&R. There are two ways to use neo4j.rb with rails - embedded or accessing it via REST.

The following example demonstrate how to embed neo4j.rb with rails. It has been verified to work on neo4j.rb 0.2.1 rail 2.2.2, JRuby 1.1.6 RC1, Glassfish 0.9.1.

Configuration

Install Neo4j.rb

  gem install neo4j

Install rails

  gem install rails

Create a rails project, movies

  rails movies

Config rails

Config rails to use Neo4j.rb instead of ActiveRecord, edit movies/config/environment.rb environment.rb:

  config.frameworks -= [ :active_record ] #, :active_resource, :action_mailer ]
  config.gem "neo4j", :version => "0.0.7"

Create Models

Create model in movies/app/models actor.rb:

  class Role
    include Neo4j::RelationshipMixin
    property :title, :character
  end

  class Actor
    include Neo4j::NodeMixin
    property :name, :phone, :salary
    has_n(:acted_in).to(Movie).relationship(Role)
    index :name
  end

movie.rb:

  class Movie
    include Neo4j::NodeMixin
    property :title
    property :year

    # defines a method for traversing incoming acted_in relationships from Actor
    has_n(:actors).from(Actor, :acted_in)
  end

Create RESTful routes

Edit the config/routes.rb file

  ActionController::Routing::Routes.draw do |map|
     map.resources :actors do |actor|
      actor.resources :acted_in
      actor.resource :movies, :controller => 'acted_in'
     end

Create Controllers

Add the following controllers in app/controllers

actors_controller.rb:

    class ActorsController < ApplicationController
      before_filter :find_actor, :only => [:show, :edit, :update, :destroy]

      def index
        @actors = Actor.all.nodes
      end

      def create
        @actor = Actor.new
        @actor.update(params[:actor])
        flash[:notice] = 'Actor was successfully created.'
        redirect_to(@actor)
      end

      def update
        @actor.update(params[:actor])
        flash[:notice] = 'Actor was successfully updated.'
        redirect_to(@actor)
      end

      def destroy
        @actor.delete
        redirect_to(actors_url)
      end

      def edit
      end

      def show
      end

      def new
        @actor = Actor.value_object.new
      end

      private
      def find_actor
        @actor = Neo4j.load(params[:id])
      end
    end

acted_in_controller.rb:

    class ActedInController < ApplicationController
      def index
        @actor = Neo4j.load(params[:actor_id])
        @movies = @actor.acted_in.nodes
      end

      def create
        @actor = Neo4j.load(params[:actor_id])
        @movie = Movie.new
        @movie.update(params[:movie])
        @actor.acted_in << @movie
        flash[:notice] = 'Movie was successfully created.'
        redirect_to(@actor)
      end

      def update
        @actor = Neo4j.load(params[:actor_id])
        @movie = Movie.new
        @movie.update(params[:movie])
        @actor.acted_in.new @movie
        @movie.update(params[:movie])
        flash[:notice] = 'Movie was successfully updated.'
        redirect_to(@movie)
      end

      def show
        @movie = Neo4j.load(params[:id])
      end

      def new
        @actor = Neo4j.load(params[:actor_id])
        @movie = Movie.value_object.new
      end

      def edit
        @movie = Neo4j.load(params[:id])
      end
    end

Add views

Add the following views in app/views/actors index.html.erb:

    <h1>Listing actors</h1>

    <table>
      <tr>
        <th>Name</th>
      </tr>

      <% for actor in @actors %>
        <tr>
          <td><%=h actor.name %></td>
          <td><%= link_to 'Edit', edit_actor_path(actor) %></td>
          <td><%= link_to 'Show', actor %></td>
          <td><%= link_to 'Destroy', actor, :confirm => 'Are you sure?', :method => :delete %></td>
        </tr>
      <% end %>
    </table>

    <br />

    <%= link_to 'New actor', new_actor_path %>

new.html.erb:

    <h1>New Actor</h1>

    <% form_for(@actor) do |f| %>
      <p>
        <%= f.label :name %><br />
        <%= f.text_field :name %>
      </p>
      <p>
        <%= f.label :phone %><br />
        <%= f.text_field :phone %>
      </p>
      <p>
        <%= f.label :salary%><br />
        <%= f.text_field :salary %>
      </p>
      <p>
        <%= f.submit "Update" %>
      </p>

    <% end %>

    <%= link_to 'Back', actors_path %>