Skip to content

Neo4j::Wrapper Rules and Functions

andreasronge edited this page Oct 2, 2012 · 6 revisions

Rules

Rules can be used to group a set of nodes. Rules and functions are very fast for read operations.
The grouping of nodes take place when you update, insert, delete a node, relationship or property.
So, if you have not too many writes but mostly read operations using rules and function might give you better performance compared to using lucene queries or traversals.

Example of rule groups: all nodes of a class, all nodes with property x == y, all nodes with relationship type z == q. The Neo4j::Rails::Model has the same functionality (since it includes the same mixin).

Find all nodes of a class

Notice this rules is always included when using Neo4j::Model

class Person
  include Neo4j::NodeMixin
  rule :all # Do not use the :all rule for Neo4j::Rails::Model since it is already included !
end

You can then find all node of the class Person.

Person.all.each {|p| puts p}

This also works for subclasses.

class Employee < Person
end

Employee.all # only the employee subclass nodes

TIP: When a node is created or deleted it will add or remove a relationship (all in the example above) to the rule node. Every node class has it’s own rule node which in turn has a relationship to the Neo4j.ref_node. When finding all the nodes of a rule group Neo4j.rb simply traverse the relationships from the rule node with the same name as the rule group.

Notice The Neo4j::Rails::Model already defines the rule :all with a count function (see below).

Rule Groups

You can add a proc to the rule which will decide if the node should be included or not in the given rule group.

class Reader
  include Neo4j::NodeMixin
  property :age
  rule(:old) { age > 10}
end

The rule group old will contain all nodes that evaluates the given proc to true. The proc will be called
when a node or relationship is created, deleted, updated.

To find all nodes of class Reader with property age > 10

Reader.old  # returns an Enumerable object

Each node will also have a method old?
Example:

r = Neo4j::Transaction.run {Reader.new :age => 1}
r.old? #=> false
Neo4j::Transaction.run {r.age = 15}
r.old? #=> true

TIP: Notice that you must commit the transaction in order to trigger the Rules, as shown in the example above.

Chaining Rules

You can combine rules, example

class NewsStory
  include Neo4j::NodeMixin
  has_n :readers

  rule :all
  rule(:featured) { |node| node[:featured] == true }
  rule(:embargoed) { |node| node[:publish_date] > 2010 }
end

NewsStory.featured.embargoed
NewsStory.all.featured.embargoed

Rules Triggering other Rules

Let say we have two classes: Reader and NewsStory.
We want to find out if a story has young or old readers.
You can trigger other rules with the :triggers parameter.

class Reader
  include Neo4j::NodeMixin
  property :age
  rule(:young, :triggers => :readers) { |node| age < 10 } 
end

class NewsStory
  include Neo4j::NodeMixin
  has_n :readers

  # young readers for only young readers - find first person which is not young, if not found then the story has only young readers
  rule(:young_readers) { !readers.find { |user| !user.young? } }
end

When a node in the young rule group changes it will trigger the incoming relationship readers (defined by has_n :readers).

user  = Reader.new :age => 200
story = NewsStory.new 
story.readers << user

# create a new transaction so that it can trigger rules
NewsStory.young_readers #  should NOT include story

user[:age] = 2
# create a new transaction so that it can trigger rules

NewsStory.young_readers # should include story

Cypher and Rules

The traversals can be converted to cypher queries by using the query method.
In this example we are going to use the Neo4j::Rails::Model instead of including the Neo4j::NodeMixin
(they both include the same mixin). See Cypher DSL query how to use the Neo4j.rb Cypher DSL.

class Person < Neo4j::Rails::Model
end

Person.all.query.to_s # => "START n0=node(2) MATCH (n0)-[:`_all`]->(default_ret) RETURN default_ret" 
Person.all.query.to_a # => returns a once only forward read Enumerable of all person instances.

Notice that the all rule is by default included when using the Neo4j::Rails::Model

Cypher Where Clause

To filter using a cypher where clause you can use a hash.

# "START n0=node(2) MATCH (n0)-[:`_all`]->(default_ret) WHERE default_ret.age = 42 RETURN default_ret"
Person.all.query(:age => 42).to_a #=> only return person objects with age property == 42

Cypher DSL blocks

The query a set of nodes scoped by a rule with a Cypher DSL block.

Person.all.query{|p| ret(m).asc(m[:strength])}

Declared Relationships

You can also use Rules and Cypher on node instances.
Let say you have defined the following classes:

class Monster < Neo4j::Rails::Model
  rule(:dangerous) { |m| m[:strength] > 15 }
end

class Dungeon < Neo4j::Rails::Model
  property :name, :index => :exact # lucene index
  has_n(:monsters).to(Monster)
end

class Room < Neo4j::Rails::Model
  has_n(:monsters).to(Monster)
end

When you declare a has_n relationship using a to specifier the relationship accessor method will get access to the rule methods (above Monster#dangerous). That means that you can from a Dungeon or Room instance use the monsters.dangerous method. Since that method is a rule method you can also combine that with Cypher !

Example: return all monsters of strengh > 16

dungeon = Dungeon.find_by_name("Dragonwald")
dungeon.monsters { |m| m[:strength] > 16 }

Example: Using rules with cypher: find all dangerous monsters with swords

dungeon = Dungeon.find_by_name("Dragonwald")
dungeon.monsters.dangerous { |m| m[:weapon?] == 'sword' }

Example: returns the rooms we should avoid

dungeon = Dungeon.find_by_name("Dragonwald")
dungeon.monsters.dangerous { |m| m.incoming(Room.monsters) }

Functions

Each rule group can have a set of functions.

Counting

To count all nodes

class Person
  include Neo4j::NodeMixin
  include :all, :functions => [Count.new]
end

Person.new
# create new transaction, required since rules are triggered when transaction finish
Person.all.count # => 1
# same as
Person.count(:all)

To count only a subset.

class Person
  include Neo4j::NodeMixin
  # notice the :functions parameter can take an array of functions, or just one function
  rule(:old, :functions => [Count.new]) {  age > 10 }
end
Person.count(:old)

TIP: Neo4j/Rails Neo4j::Model already includes one rule:

rule(:all, functions => Count.new)
. The count method on the Person.all.count will not traverse and count all nodes. Instead, it will read the count function value on the rule node, which is much faster.

Sum

The following function will sum the age of every people in the rule group old

class Person
  include Neo4j::NodeMixin
  rule(:old, :functions => Sum.new(:age)) {  age > 10 }
end

Person.sum(:old, :age)
# same as
Person.old.sum(:age)

Creating your own Rule Function

Inherit from the Neo4j::Functions::Function class, and implement the update and function_name method.
Here is the implementation of the sum function

class Sum < Function
  # Updates the function's value.
  # Called after the transactions commits and a property has been changed on a node.
  #
  # ==== Arguments
  # * rule_name :: the name of the rule group
  # * rule_node :: the node which contains the value of this function
  # * old_value new value :: the changed value of the property (when the transaction commits)
  def update(rule_name, rule_node, old_value, new_value)
    key            = rule_node_property(rule_name)
    rule_node[key] ||= 0
    old_value      ||= 0
    new_value      ||= 0
    rule_node[key] += new_value - old_value
  end

  def self.function_name
    :sum
  end

Performance

You may get some better performance using a parameter in your proc.

Example:

class Person
  rule(:old) {|node| node[:age] > 10}
end

Then a ruby object (of type Person in the example above) will not be created that wraps the Java Node
Instead it will use the java node object as an parameter as shown above.

Clone this wiki locally