Skip to content

Commit

Permalink
Now possible to set breadth or depth first traversals [#198 state:res…
Browse files Browse the repository at this point in the history
  • Loading branch information
andreasronge committed Nov 14, 2011
1 parent 293b1ae commit f09aea9
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 65 deletions.
2 changes: 2 additions & 0 deletions lib/neo4j/neo4j.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def query(query, params = {})
engine = org.neo4j.cypher.javacompat.ExecutionEngine.new(db)
engine.execute(query, params)
end


# Returns the logger used by neo4j.
# If not specified (with Neo4j.logger=) it will use the standard Ruby logger.
# You can change standard logger threshold by configuration :logger_level.
Expand Down
38 changes: 38 additions & 0 deletions lib/neo4j/traversal/traverser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,44 @@ def initialize(from, type = nil, dir=nil)
end
end


# Sets traversing depth first.
#
# The <tt>pre_or_post</tt> parameter parameter can have two values: :pre or :post
# * :pre - Traversing depth first, visiting each node before visiting its child nodes (default)
# * :post - Traversing depth first, visiting each node after visiting its child nodes.
#
def depth_first(pre_or_post = :pre)
case pre_or_post
when :pre then @td = @td.order(org.neo4j.kernel.Traversal.preorderDepthFirst())
when :post then @td = @td.order(org.neo4j.kernel.Traversal.postorderDepthFirst())
else raise "Unknown type #{pre_or_post}, should be :pre or :post"
end
self
end


# Sets traversing breadth first (default).
#
# This is the default ordering if none is defined.
# The <tt>pre_or_post</tt> parameter parameter can have two values: :pre or :post
# * :pre - Traversing breadth first, visiting each node before visiting its child nodes (default)
# * :post - Traversing breadth first, visiting each node after visiting its child nodes.
#
# ==== Note
# Please note that breadth first traversals have a higher memory overhead than depth first traversals.
# BranchSelectors carries state and hence needs to be uniquely instantiated for each traversal. Therefore it is supplied to the TraversalDescription through a BranchOrderingPolicy interface, which is a factory of BranchSelector instances.
#
def breadth_first(pre_or_post = :pre)
case pre_or_post
when :pre then @td = @td.order(org.neo4j.kernel.Traversal.preorderBreadthFirst())
when :post then @td = @td.order(org.neo4j.kernel.Traversal.postorderBreadthFirst())
else raise "Unknown type #{pre_or_post}, should be :pre or :post"
end
self
end


def eval_paths(&eval_path_block)
@td = @td.evaluator(Evaluator.new(&eval_path_block))
self
Expand Down
155 changes: 90 additions & 65 deletions spec/traversal/traversal_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

describe Neo4j::Node, :type => :transactional do

def create_nodes
before(:all) do
new_tx
#
# f
# ^
Expand All @@ -14,34 +15,75 @@ def create_nodes
# +--- work -----+
# |
# +--- work ---> d --- work --> e
a = Neo4j::Node.new :name => 'a'
b = Neo4j::Node.new :name => 'b'
c = Neo4j::Node.new :name => 'c'
d = Neo4j::Node.new :name => 'd'
e = Neo4j::Node.new :name => 'e'
f = Neo4j::Node.new :name => 'f'
a.outgoing(:friends) << b
b.outgoing(:friends) << c
b.outgoing(:work) << c
b.outgoing(:work) << d
d.outgoing(:work) << e
b.outgoing(:friends) << f
[a, b, c, d, e, f]
@a = Neo4j::Node.new :name => 'a'
@b = Neo4j::Node.new :name => 'b'
@c = Neo4j::Node.new :name => 'c'
@d = Neo4j::Node.new :name => 'd'
@e = Neo4j::Node.new :name => 'e'
@f = Neo4j::Node.new :name => 'f'
@a.outgoing(:friends) << @b
@b.outgoing(:friends) << @c
@b.outgoing(:work) << @c
@b.outgoing(:work) << @d
@d.outgoing(:work) << @e
@b.outgoing(:friends) << @f
finish_tx
end

describe "traversal order" do

def connect(parent, child)
Neo4j::Relationship.new(:child, parent, child)
end

before(:all) do
# create a tree
new_tx
@t0 = Neo4j::Node.new :name => 't0'
@t1 = Neo4j::Node.new :name => 't1'
@t11 = Neo4j::Node.new :name => 't11'
@t111 = Neo4j::Node.new :name => 't111'
@t12 = Neo4j::Node.new :name => 't12'
@t2 = Neo4j::Node.new :name => 't2'
connect(@t0, @t2)
connect(@t0, @t1)
connect(@t1, @t11)
connect(@t1, @t12)
connect(@t11, @t111)
end

it "can traverse depth first, visiting each node before visiting " do
order = @t0.outgoing(:child).depth_first(:pre).depth(:all).to_a.map { |n| n[:name] }
order.should == ["t2", "t1", "t11", "t111", "t12"]
end

it "can traverse depth first, visiting each node after visiting its child nodes" do
order = @t0.outgoing(:child).depth_first(:post).depth(:all).to_a.map { |n| n[:name] }
order.should == ["t2", "t111", "t11", "t12", "t1"]
end

it "can traverse breadth first, visiting each node before visiting its child nodes" do
order = @t0.outgoing(:child).breadth_first(:pre).depth(:all).to_a.map { |n| n[:name] }
order.should == ["t2", "t1", "t11", "t12", "t111"]
end

it "can traverse breadth first, visiting each node after visiting its child nodes" do
order = @t0.outgoing(:child).breadth_first(:post).depth(:all).to_a.map { |n| n[:name] }
order.should == ["t111", "t11", "t12", "t2", "t1"]
end
end

describe "#outgoing(:friends).paths" do
it "returns paths objects" do
a,* = create_nodes
paths = a.outgoing(:friends).outgoing(:work).depth(:all).paths.to_a
paths = @a.outgoing(:friends).outgoing(:work).depth(:all).paths.to_a
paths.each {|x| x.should be_kind_of(org.neo4j.graphdb.Path)}
paths.size.should == 5
end
end

describe "#outgoing(:friends).rels" do
it "returns paths objects" do
a,* = create_nodes
paths = a.outgoing(:friends).outgoing(:work).depth(:all).rels.to_a
paths = @a.outgoing(:friends).outgoing(:work).depth(:all).rels.to_a
paths.each {|x| x.should be_kind_of(org.neo4j.graphdb.Relationship)}
paths.size.should == 5
end
Expand Down Expand Up @@ -108,9 +150,8 @@ def create_nodes
end

it "#both returns all outgoing nodes of any type" do
a, b, c, d, e, f = create_nodes
b.both.should include(a, c, d, f)
[*b.both].size.should == 4
@b.both.should include(@a, @c, @d, @f)
[*@b.both].size.should == 4
end

it "#incoming returns all incoming nodes of any type" do
Expand All @@ -128,94 +169,78 @@ def create_nodes
end

it "#outgoing(type) should only return outgoing nodes of the given type of depth one" do
a, b, c, d = create_nodes
b.outgoing(:work).should include(c, d)
[*b.outgoing(:work)].size.should == 2
@b.outgoing(:work).should include(@c, @d)
[*@b.outgoing(:work)].size.should == 2
end

it "#outgoing(type1).outgoing(type2) should return outgoing nodes of the given types" do
a, b, c, d, e, f = create_nodes
nodes = b.outgoing(:work).outgoing(:friends)
nodes.should include(c, d, f)
nodes = @b.outgoing(:work).outgoing(:friends)
nodes.should include(@c, @d, @f)
nodes.size.should == 3
end

it "#outgoing(type).depth(4) should only return outgoing nodes of the given type and depth" do
a, b, c, d, e = create_nodes
[*b.outgoing(:work).depth(4)].size.should == 3
b.outgoing(:work).depth(4).should include(c, d, e)
[*@b.outgoing(:work).depth(4)].size.should == 3
@b.outgoing(:work).depth(4).should include(@c, @d, @e)
end

it "#outgoing(type).depth(4).include_start_node should also include the start node" do
a, b, c, d, e = create_nodes
[*b.outgoing(:work).depth(4).include_start_node].size.should == 4
b.outgoing(:work).depth(4).include_start_node.should include(b, c, d, e)
[*@b.outgoing(:work).depth(4).include_start_node].size.should == 4
@b.outgoing(:work).depth(4).include_start_node.should include(@b, @c, @d, @e)
end

it "#outgoing(type).depth(:all) should traverse at any depth" do
a, b, c, d, e = create_nodes
[*b.outgoing(:work).depth(:all)].size.should == 3
b.outgoing(:work).depth(:all).should include(c, d, e)
[*@b.outgoing(:work).depth(:all)].size.should == 3
@b.outgoing(:work).depth(:all).should include(@c, @d, @e)
end

it "#incoming(type).depth(2) should only return outgoing nodes of the given type and depth" do
a, b, c, d, e = create_nodes
[*e.incoming(:work).depth(2)].size.should == 2
e.incoming(:work).depth(2).should include(b, d)
[*@e.incoming(:work).depth(2)].size.should == 2
@e.incoming(:work).depth(2).should include(@b, @d)
end


it "#incoming(type) should only return incoming nodes of the given type of depth one" do
a, b, c, d = create_nodes
c.incoming(:work).should include(b)
[*c.incoming(:work)].size.should == 1
@c.incoming(:work).should include(@b)
[*@c.incoming(:work)].size.should == 1
end

it "#both(type) should return both incoming and outgoing nodes of the given type of depth one" do
a, b, c, d, e, f = create_nodes
# [a,b,c,d].each_with_index {|n,i| puts "#{i} : id #{n.id}"}
b.both(:friends).should include(a, c, f)
[*b.both(:friends)].size.should == 3
@b.both(:friends).should include(@a, @c, @f)
[*@b.both(:friends)].size.should == 3
end

it "#outgoing and #incoming can be combined to traverse several relationship types" do
a, b, c, d, e = create_nodes
nodes = [*b.incoming(:friends).outgoing(:work)]
nodes.should include(a, c, d)
nodes.should_not include(b, e)
nodes = [*@b.incoming(:friends).outgoing(:work)]
nodes.should include(@a, @c, @d)
nodes.should_not include(@b, @e)
end


it "#prune takes a block with parameter of type Java::org.neo4j.graphdb.Path" do
a, b, c, d, e = create_nodes
b.outgoing(:friends).depth(4).prune { |path| path.should be_kind_of(Java::org.neo4j.graphdb.Path); false }.each {}
@b.outgoing(:friends).depth(4).prune { |path| path.should be_kind_of(Java::org.neo4j.graphdb.Path); false }.each {}
end

it "#prune, if it returns true the traversal will be 'cut off' that path" do
a, b, c, d, e = create_nodes

[*b.outgoing(:work).depth(4).prune { |path| true }].size.should == 2
b.outgoing(:work).depth(4).prune { |path| true }.should include(c, d)
[*@b.outgoing(:work).depth(4).prune { |path| true }].size.should == 2
@b.outgoing(:work).depth(4).prune { |path| true }.should include(@c, @d)
end

it "#filter takes a block with parameter of type Java::org.neo4j.graphdb.Path" do
a, b, c, d, e = create_nodes
b.outgoing(:friends).depth(4).filter { |path| path.should be_kind_of(Java::org.neo4j.graphdb.Path); false }.each {}
@b.outgoing(:friends).depth(4).filter { |path| path.should be_kind_of(Java::org.neo4j.graphdb.Path); false }.each {}
end

it "#filter takes a block with parameter of type Java::org.neo4j.graphdb.Path" do
a, b, c, d, e = create_nodes
nodes = [*b.outgoing(:work).depth(4).filter { |path| path.length == 2 }]
nodes = [*@b.outgoing(:work).depth(4).filter { |path| path.length == 2 }]
nodes.size.should == 1
nodes.should include(e)
nodes.should include(@e)
end

it "#filter accept several filters which all must return true in order to include the node in the traversal result" do
a, b, c, d, e = create_nodes
nodes = [*b.outgoing(:work).depth(4).filter { |path| %w[c d].include?(path.end_node[:name]) }.
nodes = [*@b.outgoing(:work).depth(4).filter { |path| %w[c d].include?(path.end_node[:name]) }.
filter { |path| %w[d e].include?(path.end_node[:name]) }]
nodes.should include(d)
nodes.should_not include(e)
nodes.should include(@d)
nodes.should_not include(@e)
nodes.size.should == 1
end

Expand Down

0 comments on commit f09aea9

Please sign in to comment.