pacer-neo4j2 2.0.1-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,406 @@
1
+ module Pacer
2
+ module Core::Graph
3
+ module VerticesRoute
4
+ def path_to(to_v, opts = {})
5
+ route = self
6
+ route = make_pairs(to_v) if to_v.is_a? Enumerable
7
+ route.chain_route({transform: :path_finder, element_type: :path, max_hits: 1, target: to_v}.merge opts)
8
+ end
9
+
10
+ def paths_to(to_v, opts = {})
11
+ route = self
12
+ route = make_pairs(to_v) if to_v.is_a? Enumerable
13
+ route.chain_route({transform: :path_finder, element_type: :path, target: to_v}.merge opts)
14
+ end
15
+ end
16
+
17
+ module PathRoute
18
+ def expand(opts = {})
19
+ chain_route({transform: :path_finder, element_type: :path}.merge opts)
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+ module Transform
26
+ module PathFinder
27
+ import org.neo4j.graphalgo.CommonEvaluators
28
+ import org.neo4j.graphalgo.GraphAlgoFactory
29
+ import org.neo4j.graphdb.Direction
30
+ import org.neo4j.kernel.Traversal
31
+ import org.neo4j.graphdb.DynamicRelationshipType
32
+ include Pacer::Neo4j2::Algo
33
+
34
+ def after_initialize
35
+ fail Pacer::ClientError, 'graph must be neo4j' unless graph.vendor == 'neo4j'
36
+ end
37
+
38
+ def help(opt = nil)
39
+ case opt
40
+ when nil
41
+ puts <<HELP
42
+ Finds paths between pairs of vertices. The algorithm used depends on the
43
+ options specified. All supported path finding algorithms including in Neo4j 1.8
44
+ are included, and all of their documented usages are possible.
45
+
46
+ USAGE:
47
+
48
+ vertices.path_to(targets, options = {})
49
+ Find the first path from each vertex to each target vertex
50
+
51
+ vertices.paths_to(targets, options = {})
52
+ Find multiple paths from each vertex to each target vertex
53
+
54
+ paths.expand(options = {})
55
+ Find multiple paths from the first vertex in each path to the last vertex
56
+ in each path.
57
+
58
+ All options are optional!
59
+
60
+ These methods only work on Neo4j graphs.
61
+
62
+ More details:
63
+
64
+ help :options for simple path algorithms and other options
65
+ find_all, cyclical, length, max_depth, max_hits
66
+
67
+ help :cost for Dijkstra and aStar
68
+ cost, cost_property, cost_default
69
+
70
+ help :estimate for aStar
71
+ estimate, estimate_default, lat_property, long_property
72
+
73
+ help :expansion customize how any path is expanded
74
+ in_labels, out_labels, both_labels, expander, forward, reverse
75
+
76
+ HELP
77
+ when :path
78
+ puts <<HELP
79
+ Details for path: expander: proc { |path, state| edges }
80
+
81
+ #end_v Returns the end vertex of this path.
82
+ #start_v Returns the start vertex of this path.
83
+ #length Returns the length of this path.
84
+ #path Iterates through both the vertices and edges of this path in
85
+ order.
86
+ #end_e Returns the last edge in this path.
87
+ #v Returns all the vertices in this path starting from the start
88
+ vertex going forward towards the end vertex.
89
+ #e Returns all the edges in between the vertices which this path
90
+ consists of.
91
+ #reverse_v Like #v but reversed.
92
+ #reverse_e Like #e but reversed.
93
+
94
+ The following methods all proxy to the vertex returned by #end_v: and behave
95
+ exactly like standard Pacer Vertex methods.
96
+
97
+ The iterators can be combined with the + operator.
98
+
99
+ Fast edge iterators:
100
+ #out_edges(*args)
101
+ #in_edges(*args)
102
+ #both_edges(*args)
103
+
104
+ Fast vertex iterators:
105
+ #out_vertices(*args)
106
+ #in_vertices(*args)
107
+ #both_vertices(*args)
108
+
109
+ Edge routes:
110
+ #out_e(*args)
111
+ #in_e(*args)
112
+ #both_e(*args)
113
+
114
+ Vertex routes:
115
+ #out(*args)
116
+ #in(*args)
117
+ #both(*args)
118
+
119
+ HELP
120
+ when :expansion
121
+ puts <<HELP
122
+ Path expansion options:
123
+
124
+ By default, all edges will be followed. By specifying expansion rules you can
125
+ limit which paths are attempted. All algorithms use the same expanders so
126
+ these options do not effect the algorithm selection.
127
+
128
+ in_labels: label | [labels] only follow : in edges : with the given label(s)
129
+ out_labels: : out edges :
130
+ both_labels: : edges :
131
+ These options can be combined.
132
+
133
+ Expanders search forward from the start vertex and backwards from the target
134
+ vertex. Either expander
135
+
136
+ expander: Proc | PathExpander Custom rule for forward search
137
+ If no reverse is specified, will be used for reverse too.
138
+ forward: synonym for the expander option
139
+ reverse: Proc | PathExpander Custom rule for the reverse search
140
+
141
+ proc { |path, state| edges }:
142
+ path is a Pacer::Neo4j2::Algo::PathWrapper - help(:path) for details
143
+ The proc must simply return an Enumerable of edges that the
144
+
145
+ HELP
146
+ when :options
147
+ puts <<HELP
148
+ Simple options:
149
+
150
+ find_all: Boolean Find all non-cyclical paths.
151
+ Algorithm: allSimplePaths
152
+
153
+ cyclical: Boolean Find all paths including cyclical ones.
154
+ Algorithm: allPaths
155
+
156
+ length: Number Number of edges that the path contains.
157
+ Algorithm: pathsWithLength
158
+ Returns only paths of the specified length.
159
+
160
+ max_depth: Number Number of edges to search in a potential path.
161
+ Default: 5
162
+ Limits how many edges will be traversed searching for a path. Higher
163
+ numbers can take exponentially longer, I think. Does not apply to aStar,
164
+ Dijkstra, or pathsWithLength algorithms.
165
+
166
+ Required for find_all, cyclical, and shortest path algorithms.
167
+
168
+ max_hits: Number Maximum number of paths to find for each pair of vertices.
169
+ #path_to defaults this to 1. All algorithms use this but only
170
+ some support it natively in Neo4j's implementations.
171
+
172
+
173
+ HELP
174
+ when :cost
175
+ puts <<HELP
176
+ Cost options:
177
+
178
+ Specifying these chooses the Dijkstra algorithm unless an estimate is also
179
+ specified.
180
+
181
+ cost: Proc | CostEvaluator Calculate the cost for this edge.
182
+ Must return a Numeric unless cost_default is set.
183
+ proc { |edge, direction| Float }:
184
+ direction is either :in or :out.
185
+
186
+ cost_property: String get the cost from the given edge property
187
+ cost_default: Float default if the property isn't there
188
+
189
+ HELP
190
+ when :estimate
191
+ puts <<HELP
192
+ Estimate options
193
+ Specifying these together with cost chooses the a* / aStar algorithm.
194
+
195
+ estimate:
196
+ Must return a Numeric unless estimate_default is set.
197
+ proc { |vertex, goal_vertex| Float }
198
+
199
+ estimate_default: Float only works with the proc estimate
200
+
201
+ lat_property: String latitude property name
202
+ long_property: String longitude property name
203
+ Use latitude and longitude if all estimated vertices have the necessary
204
+ properties.
205
+
206
+ HELP
207
+ else
208
+ super
209
+ end
210
+ description
211
+ end
212
+
213
+ def method
214
+ if has_cost?
215
+ if has_estimate?
216
+ :aStar
217
+ else
218
+ :dijkstra
219
+ end
220
+ elsif cyclical and max_depth
221
+ :all
222
+ elsif find_all and max_depth
223
+ :all_simple
224
+ elsif length
225
+ :with_length
226
+ elsif max_depth
227
+ if max_hits
228
+ :shortest_with_max_hits
229
+ else
230
+ :shortest
231
+ end
232
+ end
233
+ end
234
+
235
+ attr_accessor :target
236
+
237
+ # specify one or many edge labels that the path may take in the given direction
238
+ attr_accessor :in_labels, :out_labels, :both_labels
239
+
240
+ # note that expander procs *must* return edge(s) that are connected to the end_v of the given path
241
+ #
242
+ # expander yields: { |path, state| path.is_a? Pacer::Neo4j2::Algo::PathWrapper }
243
+ attr_accessor :expander, :forward, :reverse
244
+
245
+ # use dijkstra unless the below estimate properties are set
246
+ #
247
+ # note that cost proc must return a Numeric unless cost_default is set
248
+ #
249
+ # cost yields: { |edge, direction| [:in, :out, :both].include? direction }
250
+ attr_accessor :cost, :cost_property, :cost_default
251
+
252
+ def set_cost(property = nil, default = nil, &block)
253
+ self.cost_property = property
254
+ self.cost_default = default
255
+ self.cost = block
256
+ self
257
+ end
258
+
259
+ # use the aStar algorithm
260
+ #
261
+ # note that estimate proc must return a Numeric unless estimate_default is set
262
+ #
263
+ # estimate yields: { |vertex, goal_vertex| }
264
+ attr_accessor :estimate, :lat_property, :long_property, :estimate_default
265
+
266
+ def set_estimate(lat = nil, long = nil, &block)
267
+ self.lat_property = lat
268
+ self.long_property = long
269
+ self.estimate = block
270
+ self
271
+ end
272
+
273
+ # use pathsWithLength
274
+ #
275
+ # Return only paths of the given length
276
+ attr_accessor :length
277
+
278
+ # default to shortest_path unless find_all is set
279
+ attr_writer :max_depth
280
+ def max_depth
281
+ @max_depth || 5
282
+ end
283
+
284
+ # use shortestPath
285
+ attr_accessor :max_hits
286
+
287
+ # Possible values:
288
+ attr_accessor :find_all
289
+ attr_accessor :cyclical
290
+
291
+ protected
292
+
293
+ def attach_pipe(end_pipe)
294
+ if back and back.element_type == :path
295
+ p = PathFromPathPipe.new build_algo, graph, max_hits
296
+ else
297
+ p = PathPipe.new build_algo, graph, target, max_hits
298
+ end
299
+ p.setStarts end_pipe
300
+ attach_length_filter(p) if length and method != :with_length
301
+ p
302
+ end
303
+
304
+ def attach_length_filter(end_pipe)
305
+ pipe = Pacer::Pipes::BlockFilterPipe.new(self, proc { |p| p.length == length }, false)
306
+ pipe.set_starts end_pipe if end_pipe
307
+ pipe
308
+ end
309
+
310
+ def inspect_string
311
+ if back and back.element_type == :path
312
+ "expand[#{method}](max_depth: #{ max_depth })"
313
+ else
314
+ "paths_to[#{method}](#{ target.inspect }, max_depth: #{ max_depth })"
315
+ end
316
+ end
317
+
318
+ private
319
+
320
+ def has_cost?
321
+ cost or cost_property or cost_default
322
+ end
323
+
324
+ def has_estimate?
325
+ estimate or (lat_property and long_property) or estimate_default
326
+ end
327
+
328
+ def build_algo
329
+ case method
330
+ when :aStar
331
+ GraphAlgoFactory.aStar build_expander, build_cost, build_estimate
332
+ when :dijkstra
333
+ GraphAlgoFactory.dijkstra build_expander, build_cost
334
+ when :with_length
335
+ GraphAlgoFactory.pathsWithLength build_expander, length
336
+ when :all
337
+ GraphAlgoFactory.allPaths build_expander, max_depth
338
+ when :all_simple
339
+ GraphAlgoFactory.allSimplePaths build_expander, max_depth
340
+ when :shortest_with_max_hits
341
+ GraphAlgoFactory.shortestPath build_expander, max_depth, max_hits
342
+ when :shortest
343
+ GraphAlgoFactory.shortestPath build_expander, max_depth
344
+ when nil
345
+ fail Pacer::ClientError, "Could not choose a path algorithm"
346
+ else
347
+ fail Pacer::LogicError, "Unable to build algo for #{ method }"
348
+ end
349
+ end
350
+
351
+ def build_expander
352
+ if forward.is_a? Proc and reverse.is_a? Proc
353
+ BlockPathExpander.new forward, reverse, graph, max_depth
354
+ elsif expander.is_a? Proc
355
+ BlockPathExpander.new expander, expander, graph, max_depth
356
+ elsif expander
357
+ expander
358
+ else
359
+ e = Traversal.emptyExpander
360
+ [*out_labels].each do |label|
361
+ e = e.add DynamicRelationshipType.withName(label.to_s), Direction::OUTGOING
362
+ end
363
+ [*in_labels].each do |label|
364
+ e = e.add DynamicRelationshipType.withName(label.to_s), Direction::INCOMING
365
+ end
366
+ [*both_labels].each do |label|
367
+ e = e.add DynamicRelationshipType.withName(label.to_s), Direction::BOTH
368
+ end
369
+ e
370
+ end
371
+ end
372
+
373
+ def build_cost
374
+ if cost.is_a? Proc
375
+ BlockCostEvaluator.new cost, graph, cost_default
376
+ elsif cost
377
+ cost
378
+ elsif cost_property
379
+ if cost_default
380
+ CommonEvaluators.doubleCostEvaluator cost_property.to_s, cost_default.to_f
381
+ else
382
+ CommonEvaluators.doubleCostEvaluator cost_property.to_s
383
+ end
384
+ elsif cost_default
385
+ CommonEvaluators.doubleCostEvaluator ' not a property ', cost_default.to_f
386
+ else
387
+ fail Pacer::LogicError, "could not build cost"
388
+ end
389
+ end
390
+
391
+ def build_estimate
392
+ if estimate.is_a? Proc
393
+ BlockEstimateEvaluator.new estimate, graph, estimate_default
394
+ elsif estimate
395
+ estimate
396
+ elsif lat_property and long_property
397
+ CommonEvaluators.geoEstimateEvaluator lat_property.to_s, long_property.to_s
398
+ elsif estimate_default
399
+ BlockEstimateEvaluator.new proc { estimate_default }, graph, estimate_default
400
+ else
401
+ fail Pacer::LogicError, "could not build estimate"
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,42 @@
1
+ module Pacer
2
+ module Neo4j2
3
+ class BlueprintsGraph < com.tinkerpop.blueprints.impls.neo4j2.Neo4j2Graph
4
+ attr_accessor :allow_auto_tx, :allow_auto_read_tx
5
+
6
+ # Threadlocal tx_depth is set in Pacer's graph_transaction_mixin.rb
7
+ def tx_depth
8
+ graphs = Thread.current[:graphs] ||= {}
9
+ tgi = graphs[object_id] ||= {}
10
+ tgi[:tx_depth] || 0
11
+ end
12
+
13
+ # Threadlocal read_tx_depth is set in Pacer's graph_transaction_mixin.rb
14
+ def read_tx_depth
15
+ graphs = Thread.current[:graphs] ||= {}
16
+ tgi = graphs[object_id] ||= {}
17
+ depth = tgi[:read_tx_depth] || 0
18
+ if depth == 0
19
+ tgi[:tx_depth] || 0 # Reads are allowed in any type of tx.
20
+ else
21
+ depth
22
+ end
23
+ end
24
+
25
+ def autoStartTransaction(for_write)
26
+ if for_write
27
+ if allow_auto_tx or tx_depth != 0
28
+ super
29
+ else
30
+ raise Pacer::TransactionError, "Can't mutate the graph outside a transaction block"
31
+ end
32
+ else
33
+ if allow_auto_read_tx or read_tx_depth != 0
34
+ super
35
+ else
36
+ raise Pacer::TransactionError, "Can't read the graph outside a transaction or read_transaction block"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,243 @@
1
+ require 'set'
2
+
3
+ module Pacer
4
+ module Neo4j2
5
+ class Graph < PacerGraph
6
+ JDate = java.util.Date
7
+ import java.text.SimpleDateFormat
8
+
9
+ # I'm not sure exactly what this impacts but if it is false, many Pacer tests fail.
10
+ #
11
+ # Presumably Neo4j is faster with it set to false.
12
+ def safe_transactions=(b)
13
+ blueprints_graph.setCheckElementsInTransaction b
14
+ end
15
+
16
+ def safe_transactions
17
+ blueprints_graph.getCheckElementsInTransaction
18
+ end
19
+
20
+ def allow_auto_tx=(b)
21
+ blueprints_graph.allow_auto_tx = b
22
+ end
23
+
24
+ def allow_auto_tx
25
+ blueprints_graph.allow_auto_tx
26
+ end
27
+
28
+ def allow_auto_read_tx=(b)
29
+ blueprints_graph.allow_auto_read_tx = b
30
+ end
31
+
32
+ def allow_auto_read_tx
33
+ blueprints_graph.allow_auto_read_tx
34
+ end
35
+
36
+ def cypher(query)
37
+ [query].to_route(element_type: :string, graph: self).cypher
38
+ end
39
+
40
+ def key_index_cache(type, name, size = :undefined)
41
+ indexer = lucene_auto_index(type)
42
+ if size == :undefined
43
+ indexer.getCacheCapacity name
44
+ else
45
+ indexer.setCacheCapacity name, size
46
+ end
47
+ end
48
+
49
+ # When a Neo4J graph is restarted, the ids of any elements that were deleted
50
+ # will be reused. Running this code immediately after starting the graph
51
+ # prevents Neo4J from reusing those IDs.
52
+ def prevent_id_reuse!
53
+ {
54
+ edges: prevent_edge_id_reuse!,
55
+ vertices: prevent_vertex_id_reuse!
56
+ }
57
+ end
58
+
59
+ # This works by simply creating IDs until the ID of a new element is greater than
60
+ # either the max existing ID, or the min_new_id argument.
61
+ def prevent_vertex_id_reuse!(min_new_id = nil)
62
+ min_new_id ||= v.element_ids.max
63
+ return unless min_new_id
64
+ g = blueprints_graph
65
+ n = 0
66
+ transaction do |_, rollback|
67
+ begin
68
+ n += 1
69
+ v = g.addVertex(nil)
70
+ end while v.getId < min_new_id
71
+ rollback.call
72
+ end
73
+ n
74
+ end
75
+
76
+ def prevent_edge_id_reuse!(min_new_id = nil)
77
+ min_new_id ||= e.element_ids.max
78
+ return unless min_new_id
79
+ g = blueprints_graph
80
+ n = 0
81
+ transaction do |_, rollback|
82
+ v1 = g.addVertex nil
83
+ v2 = g.addVertex nil
84
+ begin
85
+ n += 1
86
+ e = g.addEdge(nil, v1, v2, "temp")
87
+ end while e.getId < min_new_id
88
+ rollback.call
89
+ end
90
+ n
91
+ end
92
+
93
+ def neo_graph
94
+ blueprints_graph.raw_graph
95
+ end
96
+
97
+ def reopen_read_transaction
98
+ blueprints_graph.autoStartTransaction(false) if in_read_transaction?
99
+ end
100
+
101
+ def on_commit(&block)
102
+ return unless block
103
+ TransactionEventHandler.new(self).tap do |h|
104
+ h.on_commit = block
105
+ neo_graph.registerTransactionEventHandler h
106
+ end
107
+ end
108
+
109
+ # This is actually only called if the commit fails and then it internally tries to
110
+ # rollback. It seems that it's actually possible for it to fail to rollback here, too...
111
+ #
112
+ # An exception in before_commit can definitely trigger this.
113
+ #
114
+ # Regular rollbacks do not get seen by the transaction system and no callback happens.
115
+ def on_commit_failed(&block)
116
+ return unless block
117
+ TransactionEventHandler.new(self).tap do |h|
118
+ h.on_commit_failed = block
119
+ neo_graph.registerTransactionEventHandler h
120
+ end
121
+ end
122
+
123
+ def before_commit(&block)
124
+ return unless block
125
+ TransactionEventHandler.new(self).tap do |h|
126
+ h.before_commit = block
127
+ neo_graph.registerTransactionEventHandler h
128
+ end
129
+ end
130
+
131
+ def drop_handler(h)
132
+ neo_graph.unregisterTransactionEventHandler h
133
+ end
134
+
135
+ # Creates a Blueprints key index without doing a rebuild.
136
+ def create_key_index_fast(name, type = :vertex)
137
+ raise "Invalid index type #{ type }" unless [:vertex, :edge].include? type
138
+ keys = (key_indices(type) + [name.to_s]).to_a
139
+ neo_settings = neo_graph.getNodeManager.getGraphProperties
140
+ iz = neo_graph.index.getNodeAutoIndexer
141
+ prop = ((type == :vertex) ? "Vertex:indexed_keys" : "Edge:indexed_keys")
142
+ transaction do
143
+ create_vertex.delete! # this forces Blueprints to actually start the transaction
144
+ neo_settings.setProperty prop, keys.to_java(:string)
145
+ keys.each do |key|
146
+ iz.startAutoIndexingProperty key
147
+ end
148
+ end
149
+ end
150
+
151
+
152
+ private
153
+
154
+ def index_properties(type, filters)
155
+ filters.properties.select { |k, v| key_indices(type).include?(k) and not v.nil? }
156
+ end
157
+
158
+ def lucene_set(k, v)
159
+ statements = v.map { |x| "#{k}:#{lucene_value(x)}" }
160
+ "(#{ statements.join(' OR ') })"
161
+ end
162
+
163
+ def lucene_range(k, v)
164
+ if v.min and v.max
165
+ encoded = encode_property(v.min)
166
+ if encoded.is_a? JDate
167
+ "#{k}:[#{lucene_value v.min} TO #{lucene_value v.max}]"
168
+ else
169
+ "#{k}:[#{lucene_value v.min} TO #{lucene_value v.max}]"
170
+ end
171
+ end
172
+ end
173
+
174
+ def build_query(type, filters)
175
+ indexed = index_properties type, filters
176
+ if indexed.any?
177
+ indexed.map do |k, v|
178
+ k = k.to_s.gsub '/', '\\/'
179
+ if v.is_a? Range
180
+ lucene_range(k, v)
181
+ elsif v.class.name == 'RangeSet'
182
+ s = v.ranges.map { |r| lucene_range(k, r) }.join " OR "
183
+ "(#{s})"
184
+ elsif v.is_a? Set
185
+ lucene_set(k, v)
186
+ else
187
+ "#{k}:#{lucene_value v}"
188
+ end
189
+ end.compact.join " AND "
190
+ else
191
+ nil
192
+ end
193
+ end
194
+
195
+ def lucene_value(v)
196
+ s = encode_property(v)
197
+ if s.is_a? JDate
198
+ f = SimpleDateFormat.new 'yyyyMMddHHmmssSSS'
199
+ f.format s
200
+ elsif s
201
+ if s.is_a? String
202
+ s.inspect
203
+ else s
204
+ s
205
+ end
206
+ else
207
+ 'NULL'
208
+ end
209
+ end
210
+
211
+ def lucene_auto_index(type)
212
+ if type == :vertex
213
+ indexer = neo_graph.index.getNodeAutoIndexer
214
+ elsif type == :edge
215
+ indexer = neo_graph.index.getRelationshipAutoIndexer
216
+ end
217
+ indexer.getAutoIndex
218
+ end
219
+
220
+ def indexed_route(element_type, filters, block)
221
+ if search_manual_indices
222
+ super
223
+ else
224
+ filters.graph = self
225
+ filters.use_lookup!
226
+ query = build_query(element_type, filters)
227
+ if query
228
+ route = lucene query, element_type: element_type, extensions: filters.extensions, wrapper: filters.wrapper
229
+ filters.remove_property_keys key_indices(element_type)
230
+ if filters.any?
231
+ Pacer::Route.property_filter(route, filters, block)
232
+ else
233
+ route
234
+ end
235
+ elsif filters.route_modules.any?
236
+ mod = filters.route_modules.shift
237
+ Pacer::Route.property_filter(mod.route(self), filters, block)
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end