neo4j 1.0.0.beta.17 → 1.0.0.beta.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,156 @@
1
+ module Neo4j
2
+ module Index
3
+ # == LuceneQuery
4
+ #
5
+ # This object is returned when you call the #find method on the Node, Relationship.
6
+ # The actual query is not executed until the first item is requested.
7
+ #
8
+ # You can perform a query in many different ways:
9
+ #
10
+ # ==== By Hash
11
+ #
12
+ # Example:
13
+ # Person.find(:name => 'foo', :age => 3)
14
+ #
15
+ # ==== By Range
16
+ #
17
+ # Example:
18
+ # Person.find(:age).between(15,35)
19
+ #
20
+ # ==== By Lucene Query Syntax
21
+ #
22
+ # Example
23
+ # Car.find('wheels:"4" AND colour: "blue")
24
+ #
25
+ # For more information about the syntax see http://lucene.apache.org/java/3_0_2/queryparsersyntax.html
26
+ #
27
+ # ==== By Compound Queries
28
+ #
29
+ # You can combine several queries by <tt>AND</tt>ing those together.
30
+ #
31
+ # Example:
32
+ # Vehicle.find(:weight).between(5.0, 100000.0).and(:name).between('a', 'd')
33
+ #
34
+ # === See Also
35
+ # * Neo4j::Index::Indexer#index
36
+ # * Neo4j::Index::Indexer#find - which returns an LuceneQuery
37
+ #
38
+ class LuceneQuery
39
+ include Enumerable
40
+ attr_accessor :left_and_query, :left_or_query
41
+
42
+ def initialize(index, decl_props, query)
43
+ @index = index
44
+ @query = query
45
+ @decl_props = decl_props
46
+ end
47
+
48
+ def each
49
+ hits.each { |n| yield n.wrapper }
50
+ end
51
+
52
+ def close
53
+ @hits.close if @hits
54
+ end
55
+
56
+ def empty?
57
+ hits.size == 0
58
+ end
59
+
60
+ def [](index)
61
+ each_with_index {|node,i| break node if index == i}
62
+ end
63
+
64
+ def size
65
+ hits.size
66
+ end
67
+
68
+ def hits
69
+ @hits ||= perform_query
70
+ end
71
+
72
+ def between(lower, upper)
73
+ raise "Expected a symbol. Syntax for range queries example: index(:weight).between(a,b)" unless Symbol === @query
74
+ raise "Can't only do range queries on Neo4j::NodeMixin, Neo4j::Model, Neo4j::RelationshipMixin" unless @decl_props
75
+ type = @decl_props[@query] && @decl_props[@query][:type]
76
+ raise "Missing type declaration of property #{@query}. E.g. property :#{@query}, :type => Float; index :#{@query}" unless type
77
+ if type != String
78
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{lower} is not a Float or Fixnum" if lower === Float or lower === Fixnum
79
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{upper} is not a Float or Fixnum" if upper === Float or upper === Fixnum
80
+ @query = org.apache.lucene.search.NumericRangeQuery.new_double_range(@query.to_s, lower, upper, false, false)
81
+ else
82
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{lower} is not a String" if lower === String
83
+ raise "find(#{@query}).between(#{lower}, #{upper}) to allowed since #{upper} is not a String" if upper === String
84
+ @query = org.apache.lucene.search.TermRangeQuery.new(@query.to_s, lower, upper, false, false)
85
+ end
86
+ self
87
+ end
88
+
89
+ def and(query2)
90
+ new_query = LuceneQuery.new(@index, @decl_props, query2)
91
+ new_query.left_and_query = self
92
+ new_query
93
+ end
94
+
95
+ def desc(*fields)
96
+ @order = fields.inject(@order || {}) { |memo, field| memo[field] = true; memo }
97
+ self
98
+ end
99
+
100
+ def asc(*fields)
101
+ @order = fields.inject(@order || {}) { |memo, field| memo[field] = false; memo }
102
+ self
103
+ end
104
+
105
+ def build_and_query(query)
106
+ left_query = @left_and_query.build_query
107
+ and_query = org.apache.lucene.search.BooleanQuery.new
108
+ and_query.add(left_query, org.apache.lucene.search.BooleanClause::Occur::MUST)
109
+ and_query.add(query, org.apache.lucene.search.BooleanClause::Occur::MUST)
110
+ and_query
111
+ end
112
+
113
+ def build_sort_query(query)
114
+ java_sort_fields = @order.keys.inject([]) do |memo, field|
115
+ decl_type = @decl_props && @decl_props[field] && @decl_props[field][:type]
116
+ type = case
117
+ when Float == decl_type
118
+ org.apache.lucene.search.SortField::DOUBLE
119
+ when Fixnum == decl_type
120
+ org.apache.lucene.search.SortField::LONG
121
+ else
122
+ org.apache.lucene.search.SortField::STRING
123
+ end
124
+ memo << org.apache.lucene.search.SortField.new(field.to_s, type, @order[field])
125
+ end
126
+ sort = org.apache.lucene.search.Sort.new(*java_sort_fields)
127
+ org.neo4j.index.impl.lucene.QueryContext.new(query).sort(sort)
128
+ end
129
+
130
+ def build_hash_query(query)
131
+ and_query = org.apache.lucene.search.BooleanQuery.new
132
+
133
+ query.each_pair do |key, value|
134
+ raise "Only String values valid in find(hash) got :#{key} => #{value} which is not a String" if !value.is_a?(String) && @decl_props[key] && @decl_props[key][:type] != String
135
+ term = org.apache.lucene.index.Term.new(key.to_s, value.to_s)
136
+ term_query = org.apache.lucene.search.TermQuery.new(term)
137
+ and_query.add(term_query, org.apache.lucene.search.BooleanClause::Occur::MUST)
138
+ end
139
+ and_query
140
+ end
141
+
142
+ def build_query
143
+ query = @query
144
+ query = build_hash_query(query) if Hash === query
145
+ query = build_and_query(query) if @left_and_query
146
+ query = build_sort_query(query) if @order
147
+ query
148
+ end
149
+
150
+ def perform_query
151
+ @index.query(build_query)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
data/lib/neo4j/load.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  module Neo4j
2
2
 
3
- # Module used to load both Nodes and Relationship from the database
3
+ # === Mixin responsible for loading Ruby wrappers for Neo4j Nodes and Relationship.
4
+ #
4
5
  module Load
5
6
  def wrapper(node) # :nodoc:
6
7
  return node unless node.property?(:_classname)
@@ -11,7 +12,7 @@ module Neo4j
11
12
  class_name.split("::").inject(Kernel) {|container, name| container.const_get(name.to_s) }
12
13
  end
13
14
 
14
- # Checks if the given node or node id exists in the database.
15
+ # Checks if the given entity (node/relationship) or entity id (#neo_id) exists in the database.
15
16
  def exist?(node_or_node_id, db = Neo4j.started_db)
16
17
  id = node_or_node_id.kind_of?(Fixnum) ? node_or_node_id : node_or_node_id.id
17
18
  _load(id, db) != nil
@@ -6,7 +6,23 @@ module Neo4j::Mapping
6
6
  # The generated accessor is a simple wrapper around the #[] and
7
7
  # #[]= operators.
8
8
  #
9
+ # ==== Types
9
10
  # If a property is set to nil the property will be removed.
11
+ # A property can be of any primitive type (Boolean, String, Fixnum, Float) and does not
12
+ # even have to be the same.
13
+ #
14
+ # Example:
15
+ # class Foo
16
+ # include Neo4j::NodeMixin
17
+ # property :age
18
+ # end
19
+ #
20
+ # Example:
21
+ # foo = Foo.new
22
+ # foo.age = "hej" # first set it to string
23
+ # foo.age = 42 # change it to a Fixnum
24
+ #
25
+ # However, you can specify an type for the index, see Neo4j::Index::Indexer#index
10
26
  #
11
27
  # ==== Example
12
28
  # class Baaz; end
@@ -6,6 +6,7 @@ module Neo4j::Mapping
6
6
 
7
7
  # Specifies a relationship between two node classes.
8
8
  # Generates assignment and accessor methods for the given relationship.
9
+ # Both incoming and outgoing relationships can be declared, see Neo4j::Mapping::DeclRelationshipDsl
9
10
  #
10
11
  # ==== Example
11
12
  #
@@ -43,6 +44,7 @@ module Neo4j::Mapping
43
44
  # Specifies a relationship between two node classes.
44
45
  # Generates assignment and accessor methods for the given relationship
45
46
  # Old relationship is deleted when a new relationship is assigned.
47
+ # Both incoming and outgoing relationships can be declared, see Neo4j::Mapping::DeclRelationshipDsl
46
48
  #
47
49
  # ==== Example
48
50
  #
@@ -58,7 +60,7 @@ module Neo4j::Mapping
58
60
  #
59
61
  # ==== Returns
60
62
  #
61
- # Neo4j::Relationships::DeclRelationshipDsl
63
+ # Neo4j::Mapping::DeclRelationshipDsl
62
64
  #
63
65
  def has_one(rel_type, params = {})
64
66
  clazz = self
@@ -1,5 +1,6 @@
1
1
  module Neo4j::Mapping
2
2
  module ClassMethods
3
+ # Used to hold information about which relationships and properties has been declared.
3
4
  module Root
4
5
  #attr_reader :_decl_rels, :_decl_props
5
6
 
@@ -13,11 +14,13 @@ module Neo4j::Mapping
13
14
  end
14
15
 
15
16
 
17
+ # a hash of all relationships which has been declared with a has_n or has_one using Neo4j::Mapping::ClassMethods::Relationship
16
18
  def _decl_rels
17
19
  @@_all_decl_rels[self] ||= {}
18
20
  @_decl_props = @@_all_decl_rels[self]
19
21
  end
20
22
 
23
+ # a hash of all properties which has been declared with <tt>property</tt> using the Neo4j::Mapping::ClassMethods::Property
21
24
  def _decl_props
22
25
  @@_all_decl_props[self] ||= {}
23
26
  @_decl_props = @@_all_decl_props[self]
@@ -1,39 +1,43 @@
1
1
  module Neo4j::Mapping
2
2
  module ClassMethods
3
+ # Holds all defined rules and trigger them when an event is received.
4
+ #
5
+ # See Rule
6
+ #
3
7
  class Rules
4
8
  class << self
5
9
  def add(clazz, field, props, &block)
6
- clazz = clazz.to_s
7
- @rules ||= {}
10
+ clazz = clazz.to_s
11
+ @rules ||= {}
8
12
  # was there no ruls for this class AND is neo4j running ?
9
13
  if !@rules.include?(clazz) && Neo4j.running?
10
14
  # maybe Neo4j was started first and the rules was added later. Create rule nodes now
11
15
  create_rule_node_for(clazz)
12
16
  end
13
- @rules[clazz] ||= {}
14
- filter = block.nil? ? Proc.new { |*| true } : block
15
- @rules[clazz][field] = filter
16
- @triggers ||= {}
17
- @triggers[clazz] ||= {}
18
- trigger = props[:trigger].nil? ? [] : props[:trigger]
17
+ @rules[clazz] ||= {}
18
+ filter = block.nil? ? Proc.new { |*| true } : block
19
+ @rules[clazz][field] = filter
20
+ @triggers ||= {}
21
+ @triggers[clazz] ||= {}
22
+ trigger = props[:trigger].nil? ? [] : props[:trigger]
19
23
  @triggers[clazz][field] = trigger.respond_to?(:each) ? trigger : [trigger]
20
24
  end
21
-
22
- def inherit(parent_class, subclass)
23
- # copy all the rules
24
- @rules[parent_class.to_s].each_pair do |field, filter|
25
- subclass.rule field, &filter
26
- end if @rules[parent_class.to_s]
27
- end
25
+
26
+ def inherit(parent_class, subclass)
27
+ # copy all the rules
28
+ @rules[parent_class.to_s].each_pair do |field, filter|
29
+ subclass.rule field, &filter
30
+ end if @rules[parent_class.to_s]
31
+ end
28
32
 
29
33
  def trigger_other_rules(node)
30
- clazz = node[:_classname]
31
- @rules[clazz].keys.each do |field|
32
- rel_types = @triggers[clazz][field]
33
- rel_types.each do |rel_type|
34
- node.incoming(rel_type).each { |n| n.trigger_rules }
35
- end
36
- end
34
+ clazz = node[:_classname]
35
+ @rules[clazz].keys.each do |field|
36
+ rel_types = @triggers[clazz][field]
37
+ rel_types.each do |rel_type|
38
+ node.incoming(rel_type).each { |n| n.trigger_rules }
39
+ end
40
+ end
37
41
  end
38
42
 
39
43
  def fields_for(clazz)
@@ -70,17 +74,17 @@ module Neo4j::Mapping
70
74
  end
71
75
 
72
76
  def rule_for(clazz)
73
- if Neo4j.ref_node.rel?(clazz)
74
- Neo4j.ref_node._rel(:outgoing, clazz)._end_node
75
- else
76
- # this should be called if the rule node gets deleted
77
- create_rule_node_for(clazz)
78
- end
77
+ if Neo4j.ref_node.rel?(clazz)
78
+ Neo4j.ref_node._rel(:outgoing, clazz)._end_node
79
+ else
80
+ # this should be called if the rule node gets deleted
81
+ create_rule_node_for(clazz)
82
+ end
79
83
  end
80
84
 
81
85
 
82
86
  def on_relationship_created(rel, *)
83
- trigger_start_node = trigger?(rel._start_node)
87
+ trigger_start_node = trigger?(rel._start_node)
84
88
  trigger_end_node = trigger?(rel._end_node)
85
89
  # end or start node must be triggered by this event
86
90
  return unless trigger_start_node || trigger_end_node
@@ -89,52 +93,52 @@ module Neo4j::Mapping
89
93
 
90
94
 
91
95
  def on_property_changed(node, *)
92
- trigger_rules(node) if trigger?(node)
96
+ trigger_rules(node) if trigger?(node)
93
97
  end
94
98
 
95
99
  def trigger_rules(node)
96
100
  trigger_rules_for_class(node, node[:_classname])
97
- trigger_other_rules(node)
101
+ trigger_other_rules(node)
98
102
  end
99
-
100
- def trigger_rules_for_class(node, clazz)
101
- return if @rules[clazz].nil?
103
+
104
+ def trigger_rules_for_class(node, clazz)
105
+ return if @rules[clazz].nil?
102
106
 
103
107
  agg_node = rule_for(clazz)
104
108
  @rules[clazz].each_pair do |field, rule|
105
109
  if run_rule(rule, node)
106
110
  # is this node already included ?
107
- unless connected?(field, agg_node, node)
111
+ unless connected?(field, agg_node, node)
108
112
  agg_node.outgoing(field) << node
109
113
  end
110
114
  else
111
115
  # remove old ?
112
- break_connection(field, agg_node, node)
116
+ break_connection(field, agg_node, node)
113
117
  end
114
118
  end
115
-
116
- # recursively add relationships for all the parent classes with rules that also pass for this node
117
- if clazz = eval("#{clazz}.superclass")
118
- trigger_rules_for_class(node, clazz.to_s)
119
- end
120
- end
121
-
122
- # work out if two nodes are connected by a particular relationship
123
- # uses the end_node to start with because it's more likely to have less relationships to go through
124
- # (just the number of superclasses it has really)
125
- def connected?(relationship, start_node, end_node)
126
- end_node.incoming(relationship).each do |n|
127
- return true if n == start_node
128
- end
129
- false
130
- end
131
-
132
- # sever a direct one-to-one relationship if it exists
133
- def break_connection(relationship, start_node, end_node)
134
- end_node.rels(relationship).incoming.each do |r|
135
- return r.del if r.start_node == start_node
136
- end
137
- end
119
+
120
+ # recursively add relationships for all the parent classes with rules that also pass for this node
121
+ if clazz = eval("#{clazz}.superclass")
122
+ trigger_rules_for_class(node, clazz.to_s)
123
+ end
124
+ end
125
+
126
+ # work out if two nodes are connected by a particular relationship
127
+ # uses the end_node to start with because it's more likely to have less relationships to go through
128
+ # (just the number of superclasses it has really)
129
+ def connected?(relationship, start_node, end_node)
130
+ end_node.incoming(relationship).each do |n|
131
+ return true if n == start_node
132
+ end
133
+ false
134
+ end
135
+
136
+ # sever a direct one-to-one relationship if it exists
137
+ def break_connection(relationship, start_node, end_node)
138
+ end_node.rels(relationship).incoming.each do |r|
139
+ return r.del if r.start_node == start_node
140
+ end
141
+ end
138
142
 
139
143
  def run_rule(rule, node)
140
144
  if rule.arity != 1
@@ -147,6 +151,75 @@ module Neo4j::Mapping
147
151
  end
148
152
 
149
153
 
154
+ # Allows you to group nodes by providing a rule.
155
+ #
156
+ # === Example, finding all nodes of a certain class
157
+ # Just add a rule without a code block, then all nodes of that class will be grouped under the given key (<tt>all</tt>
158
+ # for the example below).
159
+ #
160
+ # class Person
161
+ # include Neo4j::NodeMixin
162
+ # rule :all
163
+ # end
164
+ #
165
+ # Then you can get all the nodes of type Person (and siblings) by
166
+ # Person.all.each {|x| ...}
167
+ #
168
+ # === Example, finding all nodes with a given condition on a property
169
+ #
170
+ # class Person
171
+ # include Neo4j::NodeMixin
172
+ # property :age
173
+ # rule(:old) { age > 10 }
174
+ # end
175
+ #
176
+ # Now we can find all nodes with a property <tt>age</tt> above 10.
177
+ #
178
+ # === Chain Rules
179
+ #
180
+ # class NewsStory
181
+ # include Neo4j::NodeMixin
182
+ # has_n :readers
183
+ # rule(:featured) { |node| node[:featured] == true }
184
+ # rule(:young_readers) { !readers.find{|user| !user.young?}}
185
+ # end
186
+ #
187
+ # You can combine two rules. Let say you want to find all stories which are featured and has young readers:
188
+ # NewsStory.featured.young_readers.each {...}
189
+ #
190
+ # === Trigger Other Rules
191
+ # You can let one rule trigger another rule.
192
+ # Let say you have readers of some magazine and want to know if the magazine has old or young readers.
193
+ # So when a reader change from young to old you want to trigger all the magazine that he reads (a but stupid example)
194
+ #
195
+ # Example
196
+ # class Reader
197
+ # include Neo4j::NodeMixin
198
+ # property :age
199
+ # rule(:young, :trigger => :readers) { age < 15 }
200
+ # end
201
+ #
202
+ # class NewsStory
203
+ # include Neo4j::NodeMixin
204
+ # has_n :readers
205
+ # rule(:young_readers) { !readers.find{|user| !user.young?}}
206
+ # end
207
+ #
208
+ # === Performance Considerations
209
+ # If you have many rules and many updates this can be a bit slow.
210
+ # In order to speed it up somewhat you can use the raw java node object instead by providing an argument in your block.
211
+ #
212
+ # Example:
213
+ #
214
+ # class Person
215
+ # include Neo4j::NodeMixin
216
+ # property :age
217
+ # rule(:old) {|node| node[:age] > 10 }
218
+ # end
219
+ #
220
+ # === Thread Safe ?
221
+ # Not sure...
222
+ #
150
223
  module Rule
151
224
 
152
225
  # Creates an rule node attached to the Neo4j.ref_node
@@ -168,13 +241,13 @@ module Neo4j::Mapping
168
241
  # p1.young? # => true
169
242
  #
170
243
  def rule(name, props = {}, &block)
171
- singelton = class << self;
244
+ singelton = class << self;
172
245
  self;
173
246
  end
174
-
247
+
175
248
  # define class methods
176
249
  singelton.send(:define_method, name) do
177
- agg_node = Rules.rule_for(self)
250
+ agg_node = Rules.rule_for(self)
178
251
  raise "no rule node for #{name} on #{self}" if agg_node.nil?
179
252
  traversal = agg_node.outgoing(name) # TODO possible to cache this object
180
253
  Rules.fields_for(self).each do |filter_name|
@@ -188,15 +261,15 @@ module Neo4j::Mapping
188
261
  # define instance methods
189
262
  self.send(:define_method, "#{name}?") do
190
263
  instance_eval &block
191
- end
264
+ end
192
265
 
193
266
  Rules.add(self, name, props, &block)
194
267
  end
195
-
268
+
196
269
  def inherit_rules_from(clazz)
197
- Rules.inherit(clazz, self)
270
+ Rules.inherit(clazz, self)
198
271
  end
199
-
272
+
200
273
  # This is typically used for RSpecs to clean up rule nodes created by the #rule method.
201
274
  # It also remove the given class method.
202
275
  def delete_rules
@@ -209,6 +282,8 @@ module Neo4j::Mapping
209
282
  Rules.delete(self)
210
283
  end
211
284
 
285
+ # Force to trigger the rules.
286
+ # You don't normally need that since it will be done automatically.
212
287
  def trigger_rules(node)
213
288
  Rules.trigger_rules(node)
214
289
  end