Skip to content

Latest commit

 

History

History
1312 lines (861 loc) · 35.2 KB

README.rdoc

File metadata and controls

1312 lines (861 loc) · 35.2 KB

Neo4j.rb

Neo4j.rb is a graph database for JRuby.

It provides:

  • Mapping of ruby objects to nodes in networks rather than in tables.

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

Status

  • There are over 200 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

License

Content

This page contains the following information:

  • Installation guide

  • Ten Minute Tutorial

  • Lucene API Documentation

  • Neo4j API Documentation

  • 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

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.

Ten Minute Tutorial

Creating a model

require "rubygems"
require "neo4j"

class Person
  include Neo4j::NodeMixin

  # define Neo4j properties
  property :name, :salary.

  # define an one way relationship to any other Person node
  has_n(:friends).to(Person)

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

The example above specifies how to map a Neo node to a ruby Person instance. Neo properties and relationships are declared using the ‘property’ and ‘has_n’/‘has_one’ NodeMixin class method. In the example above 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. Neo4j does handle primitive properties of type like String, Fixnum, Float.

Queries

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

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'

Relationships

Adding a relationship between two nodes:

person2 = Person.new
person.friends << person2

Traversing relationships

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

Deleting a relationship

person.relations[person2].delete

Deleting a node (and all its relationships)

person.delete

Example on Relationships

A more complecated example that shows how to navigate from incomming and outgoing relationships.

class Role
  include Neo4j::RelationMixin
  property :name
end

class Actor
  include Neo4j::NodeMixin
  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

Notice that relationships also can have properties. Creating a new Customer Order 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

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

Example of accessing the Role between an Actor and a Movie

keanu_reeves = Actor.new
matrix       = Movie.new
role = keanu_reeves.acted_in.new(matrix)
role.name = 'neo'

or

keanu_reeves = Actor.new
matrix       = Movie.new
keanu_reeves.acted_in << matrix
keanu_reeves.acted_in
keanu_reeves.relations.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.

Example of how to write a document and find it: (a document is like a record or row in a relation database).

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.

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

Start and Stop of the Neo Database

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'

Before using Neo4j the location where the database is stored on disk must be configured:

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

To stop the neo database - you do not need to do this since it will be done automatically when the virtual machine exits.

Neo4j.stop

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

Index in Memory instead of disk

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.

Create a Node

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 { }

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 relations method)

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

Relationship has_n

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 has to be declared by using the ‘relations’ class method. For example, let say that Person can have a relationship to other nodes with the type ‘friends’:

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

Example how to add a relation to another node:

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

Notice that you can also add relationship on the incoming relationship The following example is the same as the example above:

me = Person.new 
neo = Person.new
neo.known_by << me # neo is known by me, that means that I know neo but neo does have to 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

Properties on a relationship

A relationship 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

Traversing has_n 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 in a relationship

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.

Finding Relationships

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

Finding outgoing and incoming relationships

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

Deleting a relationship

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

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::RelationMixin object.

For example:

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

Traversing Nodes

The Neo4j::NodeMixin also has a method for traversing nodes instead of relationships. It works in a similar way to the Neo4j::NodeMixin#relations method but also allows traversing the node space of arbitrary depth.

Example:

me.relations.incoming(:friends).nodes # => people with a friend relationship to me
# is the same as
me.traverse.incoming(:friends) # => people with a friend relationship to me

Unlike the Neo4j::NodeMixin#relations the type(s) of relationships must 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 Neo4j::NodeMixin#traverse method allows you to traverse the node space of any depth with a filter, see below.

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

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

The filter method can take a block with the parameter of type TraversalPosition which contains information about current node, how many nodes has been returned, depth etc. It that case the provided filter function will not be evaluated in the context of the current node as described above.

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

Transactions

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

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.

Deleting or Reindex

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]

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. It is used internally to recreate an index or finding all nodes of a specific type.

Ruby on Rails with Neo4j.rb

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.

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

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