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
It uses two powerful and mature java libraries:
-
Neo4J (www.neo4j.org/) - for persistance and traversal of the graph
-
Lucene (lucene.apache.org/java/docs/index.html) for quering and indexing.
-
There are over 250 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).
-
Issue Tracking - neo4j.lighthouseapp.com
-
Twitter - twitter.com/ronge
-
API Yard Documentation - neo4j.rubyforge.org/
-
Source repo - git://github.com/andreasronge/neo4j.git
-
Ruby Manor 2008 - Jonathan Conway: jaikoo.com/assets/presentations/neo4j.pdf
-
Neo4j wiki - wiki.neo4j.org/content/Main_Page (check the guidelines and domain modeling gallery pages)
-
Neo4j.rb - MIT, see the LICENSE file github.com/andreasronge/neo4j/tree/master/LICENSE.
-
Lucene - Apache, see lucene.apache.org/java/docs/features.html
-
Neo4j - Dual free software/commercial license, see neo4j.org/
This page contains the following information:
-
Installation guide
-
Ten Minute Tutorial
-
Lucene API Documentation
-
Neo4j API Documentation
-
Ruby on Rails with Neo4j.rb
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
JRuby version 1.1.4 does not work with Neo4j.rb because of a JRuby bug. This bug is fixed in JRuby 1.1.5.
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
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 using those class methods at an alread existing node (see Neo4j::DynamicAccessorMixin).
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 Person node instance
person = Person.new
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.
There are different ways to write 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
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])
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'
Adding a relationship between two nodes:
person2 = Person.new person.friends << person2
To delete the relationship between person and person2:
person.relations[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
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.
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::RelationMixin # 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[Relation])--> Movie[Node] # has_n(:acted_in).to(Movie).relation(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.relations.outgoing(:acted_in)[matrix].name = 'neo'
More information about neo4j can be found after the Lucene section below.
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
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 relation 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 relation 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.
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 + 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
Let say a person can have several phone numbers. How do we index that ?
index << {:id=>'1', :name=>'adam', :phone => ['987-654', '1234-5678']}
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.
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.
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'
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}
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)
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
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 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]])
The Lucene::Index is thread safe. It guarantees that an index is not updated from two thread at the same time.
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 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
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
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'
Neo4j.rb uses the Lucene module. That means that the Neo4j::NodeMixin has method for both traversal and lucene queries/indexing.
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'
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 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
If a block is provided then the creation of the instance will be performed in an transaction, see below for more information on transactions.
node = MyNode.new { }
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
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
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
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.
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 relations method)
Car.all.nodes # => [c,v] Volvo.all.nodes # => [c,v]
Neo relationships are none symmetrical. That means that if A has a relation to B then it may not be true that B has a relation to A.
Relationships can be declared by using the ‘has_n’ or ‘has_one’ Neo4j::NodeMixin class methods.
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
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
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 { ...}
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.
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.
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
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]
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.
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' }
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]
A relationship between two nodes can have properties just like a node.
Example:
p1 = Person.new p2 = Person.new relation = p1.friends.new(p2) # set a property 'since' on this relationship bewteen p1 and p2 relation.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.relation 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).relation(Role) end
The Neo4j::NodeMixin#relations method can be used to find incoming or outgoing relationship objects. Example of listing all types of outgoing (default) relation objects (of depth one) from the me node.
me.relations.each {|rel| ... }
If we instead want to list the nodes that those relationships points to then the nodes method can be used.
me.relations.nodes.each {|rel| ... }
Listing all incoming relationship obejcts of any relationship type:
me.relations.incoming.each { ... }
Listing both incoming and outgoing relationship object of a specific type:
me.relations.both(:friends) { }
Finding one outgoing relationship of a specific type and node (you)
me.relations.outgoing(:friends)[you] # => [#<Neo4j::RelationMixin:0x134ae32]
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.relations.to_a # => [#<Neo4j::RelationMixin:0x134ae32]
A Neo4j::RelationMixin object represents a relationship between two nodes.
n1.relations[0].start_node # => n1 n1.relations[0].end_node # => n2
A RelationMixin contains the relationship type which connects it connects two nodes with, example:
n1.relations[0].relationship_type # => :friends
Relationships can also have properties just like a node (NodeMixin).
If we are only interested in all incoming nodes, we can do
n2.relations.incoming # => [#<Neo4j::RelationMixin:0x134ae32]
Or outgoing:
n1.relations.outgoing # => [#<Neo4j::RelationMixin:0x134aea2]
To find a specific relationship use the [] operator:
n1.relations.outgoing[n2] = #<Neo4j::RelationMixin:0x134aea2
Or which is better performance wise (since only friends relationships are being traversed):
n1.relations.outgoing(:friends)[n2] = #<Neo4j::RelationMixin:0x134aea2
Use the Neo4j::RelationMixin#delete method. For example, to delete the relationship between n1 and n2 from the example above:
n1.relations.outgoing(:friends)[n2].delete
If you do not want those relationship object but instead want the nodes you can use the ‘nodes’ method in the Neo4j::RelationMixin object.
For example:
n2.relations.incoming.nodes # => [n1]
Let say we want to find who has my phone number and who consider me as a friend
# who has my phone numbers me.relations.incoming(:phone_numbers).nodes # => people with my phone numbers # who consider me as a friend me.relations.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.
All operations that work with the node space (even read operations) must be wrapped in a transaction. Luckly neo4j.rb will automatically create a transaction for those operation that needs it if one is not already provided.
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 TransactionalMixin that lets you declare which method should be wrapped inside a transaction. Example:
class Person include Neo4j::NodeMixin extend Neo4j::TransactionalMixin property :name, :age def do_stuff # ... no transaction stuff needed to be written here. end transactional :do_stuff end
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
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.
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
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]
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.
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]
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')
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
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
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. It is used internally to recreate an index or finding all nodes of a specific type.
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 ActiveMQ (see the cluster branch). Another solution might be to simply copy the master database (rsync ?) to the slaves.
Neo4j.rb does work nicely with R&R.
It has been verified on rail 2.2.2, JRuby 1.1.6 RC1, Glassfish 0.9.1.
gem install neo4j
gem install rails
rails movies
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 model in movies/app/models actor.rb:
class Role include Neo4j::RelationMixin property :title, :character end class Actor include Neo4j::NodeMixin property :name, :phone, :salary has_n(:acted_in).to(Movie).relation(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
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
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 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 %>