elasticsearch-model-extensions 0.2.2 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/Gemfile +2 -1
- data/README.md +5 -0
- data/gemfiles/rails41.gemfile +11 -0
- data/gemfiles/rails41.gemfile.lock +109 -0
- data/lib/elasticsearch/model/extensions/association_path_finding/association_path_finder.rb +19 -0
- data/lib/elasticsearch/model/extensions/association_path_finding/mapping_node.rb +68 -0
- data/lib/elasticsearch/model/extensions/association_path_finding/shortest_path.rb +108 -0
- data/lib/elasticsearch/model/extensions/batch_updating.rb +9 -52
- data/lib/elasticsearch/model/extensions/batch_updating/batch_updater.rb +68 -0
- data/lib/elasticsearch/model/extensions/configuration.rb +33 -5
- data/lib/elasticsearch/model/extensions/dependency_tracking.rb +17 -21
- data/lib/elasticsearch/model/extensions/dependency_tracking/dependency_tracker.rb +52 -0
- data/lib/elasticsearch/model/extensions/mapping_reflection.rb +7 -80
- data/lib/elasticsearch/model/extensions/mapping_reflection/mapping_reflector.rb +107 -0
- data/lib/elasticsearch/model/extensions/outer_document_updating.rb +0 -21
- data/lib/elasticsearch/model/extensions/partial_updating.rb +16 -116
- data/lib/elasticsearch/model/extensions/partial_updating/partial_updater.rb +149 -0
- data/lib/elasticsearch/model/extensions/proxy.rb +0 -0
- data/lib/elasticsearch/model/extensions/update_callback.rb +3 -2
- data/lib/elasticsearch/model/extensions/version.rb +1 -1
- data/spec/batch_updating/batch_updater_spec.rb +50 -0
- data/spec/batch_updating_spec.rb +134 -0
- data/spec/integration_spec.rb +350 -4
- data/spec/partial_updating_spec.rb +36 -6
- data/spec/setup/articles_with_comments.rb +2 -4
- data/spec/setup/articles_with_comments_and_delayed_jobs.rb +2 -4
- data/spec/setup/authors_and_books_with_tags.rb +183 -0
- data/spec/setup/items_and_categories.rb +0 -0
- data/spec/setup/sqlite.rb +1 -1
- data/spec/setup/undefine.rb +6 -6
- data/spec/spec_helper.rb +6 -0
- metadata +20 -4
- data/lib/elasticsearch/model/extensions/mapping_node.rb +0 -65
- data/lib/elasticsearch/model/extensions/shortest_path.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdacb9b2813cbdf61813171273ec16c3626a40cd
|
4
|
+
data.tar.gz: a47ded66adef66a3ac2cba15e802ee5443ed50f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2373b26f5f54d52b89ad22270cf7e076cafd31eea7f69042f0cf695fda8bfb3676d89be631c6f649819dbe17698ee8d7053e55dc1f263b143b8a9cd72333bec
|
7
|
+
data.tar.gz: b354472253b3a6b61501aaecf67afc31cd69e99a6b3ce4b6e1651153c7ab09b2e4780444c98da1a944351a4171fe02b21e3a8967d95a07a39674ddb95381ed60
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in elasticsearch-model-extensions.gemspec
|
4
|
-
gemspec
|
4
|
+
gemspec path: File.dirname(__FILE__)
|
5
5
|
|
6
6
|
group :test do
|
7
7
|
gem 'rspec', '~> 3.1.0'
|
8
8
|
gem 'database_cleaner'
|
9
9
|
gem 'coveralls', require: false
|
10
|
+
gem 'parallel'
|
10
11
|
end
|
11
12
|
|
12
13
|
group :test, :development do
|
data/README.md
CHANGED
@@ -34,6 +34,11 @@ With Elasticsearch installed, you can run examples configured with an ES instanc
|
|
34
34
|
(A bunch of log output here)
|
35
35
|
irb(main):001:0> Article.search('Comment1')
|
36
36
|
|
37
|
+
To run the same example with Rails 4.1.x:
|
38
|
+
|
39
|
+
$ BUNDLE_GEMFILE=gemfiles/rails41.gemfile bundle exec irb -I spec -r example/articles_with_comments
|
40
|
+
|
41
|
+
|
37
42
|
## Contributing
|
38
43
|
|
39
44
|
1. Fork it ( https://github.com/[my-github-username]/elasticsearch-model-extensions/fork )
|
@@ -0,0 +1,11 @@
|
|
1
|
+
eval_gemfile File.expand_path(File.join(File.dirname(__FILE__), '../Gemfile'))
|
2
|
+
|
3
|
+
ignored_gems = %w(activerecord)
|
4
|
+
|
5
|
+
dependencies.delete_if do |g|
|
6
|
+
ignored_gems.include?(g.name)
|
7
|
+
end
|
8
|
+
|
9
|
+
group :test, :development do
|
10
|
+
gem 'activerecord', '~> 4.1.4'
|
11
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
PATH
|
2
|
+
remote: /Users/mumoshu/Repositories/elasticsearch-model-extensions
|
3
|
+
specs:
|
4
|
+
elasticsearch-model-extensions (0.2.2)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
activemodel (4.1.8)
|
10
|
+
activesupport (= 4.1.8)
|
11
|
+
builder (~> 3.1)
|
12
|
+
activerecord (4.1.8)
|
13
|
+
activemodel (= 4.1.8)
|
14
|
+
activesupport (= 4.1.8)
|
15
|
+
arel (~> 5.0.0)
|
16
|
+
activesupport (4.1.8)
|
17
|
+
i18n (~> 0.6, >= 0.6.9)
|
18
|
+
json (~> 1.7, >= 1.7.7)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
thread_safe (~> 0.1)
|
21
|
+
tzinfo (~> 1.1)
|
22
|
+
ansi (1.4.3)
|
23
|
+
arel (5.0.1.20140414130214)
|
24
|
+
builder (3.2.2)
|
25
|
+
coveralls (0.7.2)
|
26
|
+
multi_json (~> 1.3)
|
27
|
+
rest-client (= 1.6.7)
|
28
|
+
simplecov (>= 0.7)
|
29
|
+
term-ansicolor (= 1.2.2)
|
30
|
+
thor (= 0.18.1)
|
31
|
+
database_cleaner (1.3.0)
|
32
|
+
delayed_job (4.0.6)
|
33
|
+
activesupport (>= 3.0, < 5.0)
|
34
|
+
delayed_job_active_record (4.0.3)
|
35
|
+
activerecord (>= 3.0, < 5.0)
|
36
|
+
delayed_job (>= 3.0, < 4.1)
|
37
|
+
diff-lcs (1.2.5)
|
38
|
+
docile (1.1.5)
|
39
|
+
elasticsearch (1.0.6)
|
40
|
+
elasticsearch-api (= 1.0.6)
|
41
|
+
elasticsearch-transport (= 1.0.6)
|
42
|
+
elasticsearch-api (1.0.6)
|
43
|
+
multi_json
|
44
|
+
elasticsearch-extensions (0.0.17)
|
45
|
+
ansi
|
46
|
+
ruby-prof
|
47
|
+
elasticsearch-model (0.1.6)
|
48
|
+
activesupport (> 3)
|
49
|
+
elasticsearch (> 0.4)
|
50
|
+
hashie
|
51
|
+
elasticsearch-transport (1.0.6)
|
52
|
+
faraday
|
53
|
+
multi_json
|
54
|
+
faraday (0.9.0)
|
55
|
+
multipart-post (>= 1.2, < 3)
|
56
|
+
hashie (3.3.2)
|
57
|
+
i18n (0.7.0)
|
58
|
+
json (1.8.1)
|
59
|
+
mime-types (2.4.3)
|
60
|
+
minitest (5.5.0)
|
61
|
+
multi_json (1.10.1)
|
62
|
+
multipart-post (2.0.0)
|
63
|
+
parallel (1.3.3)
|
64
|
+
rake (10.4.2)
|
65
|
+
rest-client (1.6.7)
|
66
|
+
mime-types (>= 1.16)
|
67
|
+
rspec (3.1.0)
|
68
|
+
rspec-core (~> 3.1.0)
|
69
|
+
rspec-expectations (~> 3.1.0)
|
70
|
+
rspec-mocks (~> 3.1.0)
|
71
|
+
rspec-core (3.1.7)
|
72
|
+
rspec-support (~> 3.1.0)
|
73
|
+
rspec-expectations (3.1.2)
|
74
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
75
|
+
rspec-support (~> 3.1.0)
|
76
|
+
rspec-mocks (3.1.3)
|
77
|
+
rspec-support (~> 3.1.0)
|
78
|
+
rspec-support (3.1.2)
|
79
|
+
ruby-prof (0.15.2)
|
80
|
+
simplecov (0.9.1)
|
81
|
+
docile (~> 1.1.0)
|
82
|
+
multi_json (~> 1.0)
|
83
|
+
simplecov-html (~> 0.8.0)
|
84
|
+
simplecov-html (0.8.0)
|
85
|
+
sqlite3 (1.3.10)
|
86
|
+
term-ansicolor (1.2.2)
|
87
|
+
tins (~> 0.8)
|
88
|
+
thor (0.18.1)
|
89
|
+
thread_safe (0.3.4)
|
90
|
+
tins (0.13.2)
|
91
|
+
tzinfo (1.2.2)
|
92
|
+
thread_safe (~> 0.1)
|
93
|
+
|
94
|
+
PLATFORMS
|
95
|
+
ruby
|
96
|
+
|
97
|
+
DEPENDENCIES
|
98
|
+
activerecord (~> 4.1.4)
|
99
|
+
bundler (~> 1.7)
|
100
|
+
coveralls
|
101
|
+
database_cleaner
|
102
|
+
delayed_job_active_record (~> 4.0.1)
|
103
|
+
elasticsearch-extensions
|
104
|
+
elasticsearch-model
|
105
|
+
elasticsearch-model-extensions!
|
106
|
+
parallel
|
107
|
+
rake (~> 10.0)
|
108
|
+
rspec (~> 3.1.0)
|
109
|
+
sqlite3
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'mapping_node'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
module AssociationPathFinding
|
7
|
+
class AssociationPathFinder
|
8
|
+
def find_path(from:, to:)
|
9
|
+
MappingNode.
|
10
|
+
from_class(from).
|
11
|
+
breadth_first_search { |e| e.destination.relates_to_class?(to) }.
|
12
|
+
first
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative 'shortest_path'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Model
|
5
|
+
module Extensions
|
6
|
+
module AssociationPathFinding
|
7
|
+
class MappingNode < ShortestPath::Node
|
8
|
+
def self.from_class(klass)
|
9
|
+
name = klass.document_type.intern
|
10
|
+
|
11
|
+
new(klass: klass, name: name, mapping: klass.mapping.to_hash[name])
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(klass:, name:, mapping:, through_class:nil)
|
15
|
+
@klass = klass
|
16
|
+
@name = name
|
17
|
+
@mapping = mapping
|
18
|
+
@through_class = through_class
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
@name
|
23
|
+
end
|
24
|
+
|
25
|
+
def relates_to_class?(klass)
|
26
|
+
@klass == klass || @through_class == klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def klass
|
30
|
+
@klass
|
31
|
+
end
|
32
|
+
|
33
|
+
def through_class
|
34
|
+
@through_class
|
35
|
+
end
|
36
|
+
|
37
|
+
def each(&block)
|
38
|
+
associations = @klass.reflect_on_all_associations
|
39
|
+
|
40
|
+
props = @mapping[:properties]
|
41
|
+
fields = props.keys
|
42
|
+
|
43
|
+
edges = fields.map { |f|
|
44
|
+
a = associations.find { |a| a.name == f }
|
45
|
+
|
46
|
+
if a && a.options[:polymorphic] != true
|
47
|
+
through_class = if a.options[:through]
|
48
|
+
a.options[:through].to_s.classify.constantize
|
49
|
+
end
|
50
|
+
|
51
|
+
dest = MappingNode.new(klass: a.class_name.constantize, name: f.to_s.pluralize.intern, mapping: props[f], through_class: through_class)
|
52
|
+
|
53
|
+
edge_class.new(name: f, destination: dest)
|
54
|
+
end
|
55
|
+
}.reject(&:nil?)
|
56
|
+
|
57
|
+
if block.nil?
|
58
|
+
edges
|
59
|
+
else
|
60
|
+
edges.each(&block)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module AssociationPathFinding
|
5
|
+
class ShortestPath
|
6
|
+
MAX_DEPTH = 5
|
7
|
+
|
8
|
+
class Node
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def each(&block)
|
12
|
+
raise "A required method #{self.class}#each is not implemented."
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
raise "A required method #{self.class}#name is not implemented."
|
17
|
+
end
|
18
|
+
|
19
|
+
def each_with_name(&block)
|
20
|
+
iterator = each.lazy.map do |edge|
|
21
|
+
[edge, edge.name]
|
22
|
+
end
|
23
|
+
|
24
|
+
if block.nil?
|
25
|
+
iterator
|
26
|
+
else
|
27
|
+
iterator.each(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
name.hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def eql?(other)
|
36
|
+
self.class == other.class && (name.eql? other.name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def edge_class
|
40
|
+
ShortestPath::Edge
|
41
|
+
end
|
42
|
+
|
43
|
+
def breadth_first_search(&block)
|
44
|
+
ShortestPath.breadth_first_search self, &block
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Edge
|
49
|
+
def initialize(name:, destination:)
|
50
|
+
@name = name
|
51
|
+
@destination = destination
|
52
|
+
end
|
53
|
+
|
54
|
+
def name
|
55
|
+
@name
|
56
|
+
end
|
57
|
+
|
58
|
+
def destination
|
59
|
+
@destination
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
def breadth_first_search(node, &block)
|
65
|
+
original_paths = node.each.map { |e| [e] }
|
66
|
+
paths = original_paths
|
67
|
+
|
68
|
+
depth = 0
|
69
|
+
|
70
|
+
loop {
|
71
|
+
a = paths.select { |p|
|
72
|
+
if block.call(p.last)
|
73
|
+
p
|
74
|
+
end
|
75
|
+
}
|
76
|
+
|
77
|
+
return a if a.size != 0
|
78
|
+
raise RuntimeError, 'Maximum depth exceeded while calculating the shortest path' if depth >= Elasticsearch::Model::Extensions::ShortestPath::MAX_DEPTH
|
79
|
+
|
80
|
+
paths = paths.flat_map { |p|
|
81
|
+
p.last.destination.each.map { |e|
|
82
|
+
p + [e]
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
depth += 1
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def depth_first_search(node, &block)
|
91
|
+
node.each.select do |edge|
|
92
|
+
if block.call(edge)
|
93
|
+
[[edge]]
|
94
|
+
else
|
95
|
+
depth_first_search(edge.destination, &block).map do |path|
|
96
|
+
[edge] + path
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
extend ClassMethods
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require_relative 'mapping_reflection'
|
2
|
+
require_relative 'batch_updating/batch_updater'
|
2
3
|
|
3
4
|
module Elasticsearch
|
4
5
|
module Model
|
@@ -27,29 +28,15 @@ module Elasticsearch
|
|
27
28
|
end
|
28
29
|
|
29
30
|
module ClassMethods
|
30
|
-
def
|
31
|
-
|
32
|
-
max ||= maximum(:id)
|
33
|
-
chunk_num.times.inject([]) do |r,i|
|
34
|
-
chunk_size = ((max-min+1)/chunk_num.to_f).ceil
|
35
|
-
first = chunk_size * i
|
36
|
-
|
37
|
-
last = if i == chunk_num - 1
|
38
|
-
max
|
39
|
-
else
|
40
|
-
chunk_size * (i + 1) - 1
|
41
|
-
end
|
42
|
-
|
43
|
-
r << (first..last)
|
44
|
-
end
|
31
|
+
def __batch_updater__
|
32
|
+
@__batch_updater__ ||= ::Elasticsearch::Model::Extensions::BatchUpdating::BatchUpdater.new(self)
|
45
33
|
end
|
46
34
|
|
47
35
|
def update_index_in_parallel(parallelism:, index: nil, type: nil, min: nil, max: nil, batch_size:DEFAULT_BATCH_SIZE)
|
48
36
|
klass = self
|
49
37
|
|
50
|
-
Parallel.each(
|
51
|
-
|
52
|
-
@elasticsearch_reconnected ||= klass.__elasticsearch__.client = Elasticsearch::Client.new(host: klass.elasticsearch_hosts)
|
38
|
+
Parallel.each(__batch_updater__.split_ids_into(parallelism, min: min, max: max), in_processes: parallelism) do |id_range|
|
39
|
+
__batch_updater__.reconnect!
|
53
40
|
klass.for_indexing.update_index_for_ids_in_range id_range, index: index, type: type, batch_size: batch_size
|
54
41
|
end
|
55
42
|
|
@@ -74,36 +61,6 @@ module Elasticsearch
|
|
74
61
|
|
75
62
|
records_in_scope.update_index_in_batches(batch_size: batch_size, index: index, type: type)
|
76
63
|
end
|
77
|
-
|
78
|
-
# @param [Array] records
|
79
|
-
def update_index_in_batch(records, index: nil, type: nil, client: nil)
|
80
|
-
klass = self
|
81
|
-
|
82
|
-
client ||= klass.__elasticsearch__.client
|
83
|
-
index ||= klass.index_name
|
84
|
-
type ||= klass.document_type
|
85
|
-
|
86
|
-
if records.size > 1
|
87
|
-
response = client.bulk \
|
88
|
-
index: index,
|
89
|
-
type: type,
|
90
|
-
body: records.map { |r| { index: { _id: r.id, data: r.as_indexed_json } } }
|
91
|
-
|
92
|
-
one_or_more_errors_occurred = response["errors"]
|
93
|
-
|
94
|
-
if one_or_more_errors_occurred
|
95
|
-
if defined? ::Rails
|
96
|
-
::Rails.logger.warn "One or more error(s) occurred while updating the index #{records} for the type #{type}\n#{JSON.pretty_generate(response)}"
|
97
|
-
else
|
98
|
-
warn "One or more error(s) occurred while updating the index #{records} for the type #{type}\n#{JSON.pretty_generate(response)}"
|
99
|
-
end
|
100
|
-
end
|
101
|
-
else
|
102
|
-
records.each do |r|
|
103
|
-
client.index index: index, type: type, id: r.id, body: r.as_indexed_json
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
64
|
end
|
108
65
|
|
109
66
|
module Association
|
@@ -123,16 +80,16 @@ module Elasticsearch
|
|
123
80
|
|
124
81
|
conditions = record_id.gteq(from).and(record_id.lteq(to))
|
125
82
|
|
126
|
-
update_index_in_batches(batch_size: batch_size, index: index, type: type
|
83
|
+
where(conditions).update_index_in_batches(batch_size: batch_size, index: index, type: type)
|
127
84
|
end
|
128
85
|
|
129
86
|
def update_index_for_ids_in_range(range, index: nil, type: nil, batch_size: DEFAULT_BATCH_SIZE)
|
130
87
|
update_index_for_ids_from(range.first, to: range.last, type: type, index: index, batch_size: batch_size)
|
131
88
|
end
|
132
89
|
|
133
|
-
def update_index_in_batches(batch_size: DEFAULT_BATCH_SIZE,
|
134
|
-
find_in_batches(batch_size: batch_size
|
135
|
-
|
90
|
+
def update_index_in_batches(batch_size: DEFAULT_BATCH_SIZE, index: nil, type: nil)
|
91
|
+
find_in_batches(batch_size: batch_size) do |records|
|
92
|
+
__batch_updater__.update_index_in_batch(records, index: index, type: type)
|
136
93
|
end
|
137
94
|
end
|
138
95
|
end
|