elasticsearch-model-extensions 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,68 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module BatchUpdating
|
5
|
+
class BatchUpdater
|
6
|
+
def initialize(klass)
|
7
|
+
@klass = klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def klass
|
11
|
+
@klass
|
12
|
+
end
|
13
|
+
|
14
|
+
def reconnect!
|
15
|
+
klass.connection.reconnect!
|
16
|
+
klass.__elasticsearch__.client = Elasticsearch::Client.new(host: klass.elasticsearch_hosts)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Array] records
|
20
|
+
def update_index_in_batch(records, index: nil, type: nil, client: nil)
|
21
|
+
client ||= klass.__elasticsearch__.client
|
22
|
+
index ||= klass.index_name
|
23
|
+
type ||= klass.document_type
|
24
|
+
|
25
|
+
if records.size > 1
|
26
|
+
response = client.bulk \
|
27
|
+
index: index,
|
28
|
+
type: type,
|
29
|
+
body: records.map { |r| { index: { _id: r.id, data: r.as_indexed_json } } }
|
30
|
+
|
31
|
+
one_or_more_errors_occurred = response["errors"]
|
32
|
+
|
33
|
+
if one_or_more_errors_occurred
|
34
|
+
if defined? ::Rails
|
35
|
+
::Rails.logger.warn "One or more error(s) occurred while updating the index #{records} for the type #{type}\n#{JSON.pretty_generate(response)}"
|
36
|
+
else
|
37
|
+
warn "One or more error(s) occurred while updating the index #{records} for the type #{type}\n#{JSON.pretty_generate(response)}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
else
|
41
|
+
records.each do |r|
|
42
|
+
client.index index: index, type: type, id: r.id, body: r.as_indexed_json
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def split_ids_into(chunk_num, min:nil, max:nil)
|
48
|
+
min ||= klass.minimum(:id)
|
49
|
+
max ||= klass.maximum(:id)
|
50
|
+
chunk_num.times.inject([]) do |r,i|
|
51
|
+
chunk_size = ((max-min+1)/chunk_num.to_f).ceil
|
52
|
+
first = chunk_size * i
|
53
|
+
|
54
|
+
last = if i == chunk_num - 1
|
55
|
+
max
|
56
|
+
else
|
57
|
+
chunk_size * (i + 1) - 1
|
58
|
+
end
|
59
|
+
|
60
|
+
r << (first..last)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'association_path_finding/association_path_finder'
|
2
|
+
|
1
3
|
module Elasticsearch
|
2
4
|
module Model
|
3
5
|
module Extensions
|
@@ -39,13 +41,40 @@ module Elasticsearch
|
|
39
41
|
to_hash[:block]
|
40
42
|
end
|
41
43
|
|
44
|
+
# TODO Document what is in the Array
|
45
|
+
# @return [Array]
|
46
|
+
def nested_object_fields
|
47
|
+
@nested_object_fields
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Boolean]
|
51
|
+
def has_dependent_fields?
|
52
|
+
@has_dependent_fields
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [ActiveRecord::Base] record the updated record we are unsure
|
56
|
+
# whether it must be also updated in elasticsearch or not
|
57
|
+
# @return [Boolean] true if we have to update the document for the record indexed in Elasticsearch
|
58
|
+
def index_update_required?(record)
|
59
|
+
previous_changes = record.previous_changes
|
60
|
+
|
61
|
+
defined?(record.index_update_required?) && record.index_update_required? ||
|
62
|
+
(previous_changes.keys & nested_object_fields).size > 0 ||
|
63
|
+
(previous_changes.size > 0 && has_dependent_fields?)
|
64
|
+
end
|
65
|
+
|
42
66
|
private
|
43
67
|
|
68
|
+
# @return [Elasticsearch::Model::Extensions::AssociationPathFinding::AssociationPathFinder]
|
69
|
+
def association_path_finder
|
70
|
+
@association_path_finder ||= Elasticsearch::Model::Extensions::AssociationPathFinding::AssociationPathFinder.new
|
71
|
+
end
|
72
|
+
|
44
73
|
def build_hash
|
45
74
|
child_class = @active_record_class
|
46
75
|
|
47
76
|
field_to_update = @field_to_update || begin
|
48
|
-
path =
|
77
|
+
path = association_path_finder.find_path(from: @parent_class, to: child_class)
|
49
78
|
parent_to_child_path = path.map(&:name)
|
50
79
|
|
51
80
|
# a has_a b has_a cという関係のとき、cが更新されたらaのフィールドbをupdateする必要がある。
|
@@ -58,10 +87,9 @@ module Elasticsearch
|
|
58
87
|
|
59
88
|
puts "#{child_class.name} updates #{@parent_class.name}'s #{field_to_update}"
|
60
89
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
(path.first.destination.through_class == child_class && @parent_class.has_association_named?(field_to_update) && @parent_class.has_document_field_named?(field_to_update))
|
90
|
+
@nested_object_fields = @parent_class.__mapping_reflector__.nested_object_fields_for(parent_to_child_path).map(&:to_s)
|
91
|
+
@has_dependent_fields = @parent_class.__dependency_tracker__.has_dependent_fields?(field_to_update) ||
|
92
|
+
(path.first.destination.through_class == child_class && @parent_class.__dependency_tracker__.has_association_named?(field_to_update) && @parent_class.__mapping_reflector__.has_document_field_named?(field_to_update))
|
65
93
|
|
66
94
|
custom_if = @if
|
67
95
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative 'dependency_tracking/dependency_tracker'
|
2
|
+
|
1
3
|
module Elasticsearch
|
2
4
|
module Model
|
3
5
|
module Extensions
|
@@ -5,40 +7,34 @@ module Elasticsearch
|
|
5
7
|
def self.included(base)
|
6
8
|
base.extend ClassMethods
|
7
9
|
|
10
|
+
dependency_tracker = DependencyTracker.new(base)
|
11
|
+
|
8
12
|
base.class_eval do
|
9
13
|
before_validation do
|
10
|
-
|
14
|
+
dependency_tracker.each_dependent_attribute_for(changes) do |a|
|
11
15
|
attribute_will_change! a
|
12
16
|
end
|
13
17
|
end
|
14
18
|
end
|
15
|
-
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
const_set 'DEPENDENT_CUSTOM_ATTRIBUTES', dependencies.dup.freeze
|
20
|
-
end
|
20
|
+
# TODO Assert that @__dependency_tracker__ is nil to prevent users from facing terrible bugs
|
21
|
+
# trying to include this module multiple times
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
dependent_attributes.each do |dependent_attribute|
|
25
|
-
attributes.each do |a|
|
26
|
-
yield dependent_attribute if changed_attributes.include? a
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
23
|
+
base.instance_variable_set :@__dependency_tracker__, dependency_tracker
|
24
|
+
end
|
31
25
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
26
|
+
module ClassMethods
|
27
|
+
# @return [DependencyTracker]
|
28
|
+
def __dependency_tracker__
|
29
|
+
@__dependency_tracker__
|
36
30
|
end
|
37
31
|
|
38
|
-
|
39
|
-
|
32
|
+
# @param [Hash[Array<String>, Array<String>]] dependencies
|
33
|
+
def tracks_attributes_dependencies(dependencies)
|
34
|
+
__dependency_tracker__.dependent_custom_attributes = dependencies.dup.freeze
|
40
35
|
end
|
41
36
|
end
|
37
|
+
|
42
38
|
end
|
43
39
|
end
|
44
40
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module DependencyTracking
|
5
|
+
class DependencyTracker
|
6
|
+
def initialize(base)
|
7
|
+
@base = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def base
|
11
|
+
@base
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Hash<Array<String>, Array<String>>]
|
15
|
+
def dependent_custom_attributes
|
16
|
+
@dependent_custom_attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Hash<Array<String>, Array<String>>] new_value
|
20
|
+
def dependent_custom_attributes=(new_value)
|
21
|
+
@dependent_custom_attributes = new_value
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Array<String>] changed_attributes
|
25
|
+
def each_dependent_attribute_for(changed_attributes)
|
26
|
+
dependent_custom_attributes.each do |attributes, dependent_attributes|
|
27
|
+
dependent_attributes.each do |dependent_attribute|
|
28
|
+
attributes.each do |a|
|
29
|
+
yield dependent_attribute if changed_attributes.include? a
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [String|Symbol] field
|
36
|
+
def has_dependent_fields?(field)
|
37
|
+
dependent_custom_attributes.any? do |from, to|
|
38
|
+
from.include? field.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [Symbol] table_name
|
43
|
+
def has_association_named?(table_name)
|
44
|
+
# TODO call `reflect_on_all_associations` through a proxy object
|
45
|
+
base.reflect_on_all_associations.any? { |a| a.name == table_name }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,94 +1,21 @@
|
|
1
|
+
require_relative 'mapping_reflection/mapping_reflector'
|
2
|
+
|
1
3
|
module Elasticsearch
|
2
4
|
module Model
|
3
5
|
module Extensions
|
4
6
|
module MappingReflection
|
5
7
|
def self.included(base)
|
6
8
|
base.extend ClassMethods
|
9
|
+
|
10
|
+
base.instance_variable_set :@__mapping_reflector__, MappingReflector.new(base)
|
7
11
|
end
|
8
12
|
|
9
13
|
module ClassMethods
|
10
|
-
|
11
|
-
|
12
|
-
current_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
13
|
-
visited_classes ||= []
|
14
|
-
current_class ||= self
|
15
|
-
|
16
|
-
# Recurse only on associations
|
17
|
-
current_properties.keys.each do |key|
|
18
|
-
association_found = current_class.reflect_on_all_associations.find { |a| a.name == key }
|
19
|
-
|
20
|
-
next unless association_found
|
21
|
-
next if visited_classes.include? association_found.klass
|
22
|
-
|
23
|
-
if association_found.klass == destination_class
|
24
|
-
return [key]
|
25
|
-
else
|
26
|
-
suffix_found = path_in_mapping_to_class(
|
27
|
-
destination_class,
|
28
|
-
current_properties: current_properties[key][:properties],
|
29
|
-
current_class: association_found.klass,
|
30
|
-
visited_classes: visited_classes.dup.append(association_found.klass)
|
31
|
-
)
|
32
|
-
|
33
|
-
if suffix_found
|
34
|
-
return [key] + suffix_found
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
nil
|
40
|
-
end
|
41
|
-
|
42
|
-
# @param [Symbol] nested_object_name
|
43
|
-
# @return [Array<Symbol>]
|
44
|
-
def path_in_mapping_to(nested_object_name, root_properties: nil)
|
45
|
-
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
46
|
-
|
47
|
-
keys = root_properties.keys
|
48
|
-
|
49
|
-
keys.each do |key|
|
50
|
-
if key == nested_object_name
|
51
|
-
return [key]
|
52
|
-
end
|
53
|
-
|
54
|
-
next if root_properties[key][:type] != 'object'
|
55
|
-
|
56
|
-
suffix = path_in_mapping_to(nested_object_name, root_properties: root_properties[key][:properties])
|
57
|
-
|
58
|
-
if suffix.include? nested_object_name
|
59
|
-
return [key] + suffix
|
60
|
-
end
|
61
|
-
end
|
62
|
-
[]
|
63
|
-
end
|
64
|
-
|
65
|
-
def document_field_named(field_name)
|
66
|
-
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
67
|
-
root_properties[field_name]
|
68
|
-
end
|
69
|
-
|
70
|
-
def has_document_field_named?(field_name)
|
71
|
-
!! document_field_named(field_name)
|
72
|
-
end
|
73
|
-
|
74
|
-
# @param [Array<Symbol>] path
|
75
|
-
def nested_object_fields_for(path, root_properties: nil)
|
76
|
-
root_properties ||= mappings.to_hash[:"#{self.document_type}"][:properties]
|
77
|
-
|
78
|
-
keys = root_properties.keys
|
79
|
-
|
80
|
-
suffix, *postfix = path
|
81
|
-
|
82
|
-
return root_properties.keys if suffix.nil?
|
83
|
-
|
84
|
-
keys.each do |key|
|
85
|
-
if key == suffix
|
86
|
-
result = nested_object_fields_for(postfix, root_properties: root_properties[key][:properties])
|
87
|
-
return result if result
|
88
|
-
end
|
89
|
-
end
|
14
|
+
def __mapping_reflector__
|
15
|
+
@__mapping_reflector__
|
90
16
|
end
|
91
17
|
end
|
18
|
+
|
92
19
|
end
|
93
20
|
end
|
94
21
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Extensions
|
4
|
+
module MappingReflection
|
5
|
+
class MappingReflector
|
6
|
+
# @param [Class] base A class extending ActiveRecord::Base
|
7
|
+
def initialize(base)
|
8
|
+
@base = base
|
9
|
+
end
|
10
|
+
|
11
|
+
def base
|
12
|
+
@base
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Class] destination_class
|
16
|
+
def path_in_mapping_to_class(destination_class, current_properties: nil, current_class: nil, visited_classes: nil)
|
17
|
+
current_properties ||= default_root_properties
|
18
|
+
visited_classes ||= []
|
19
|
+
current_class ||= self
|
20
|
+
|
21
|
+
# Recurse only on associations
|
22
|
+
current_properties.keys.each do |key|
|
23
|
+
association_found = current_class.reflect_on_all_associations.find { |a| a.name == key }
|
24
|
+
|
25
|
+
next unless association_found
|
26
|
+
next if visited_classes.include? association_found.klass
|
27
|
+
|
28
|
+
if association_found.klass == destination_class
|
29
|
+
return [key]
|
30
|
+
else
|
31
|
+
suffix_found = path_in_mapping_to_class(
|
32
|
+
destination_class,
|
33
|
+
current_properties: current_properties[key][:properties],
|
34
|
+
current_class: association_found.klass,
|
35
|
+
visited_classes: visited_classes.dup.append(association_found.klass)
|
36
|
+
)
|
37
|
+
|
38
|
+
if suffix_found
|
39
|
+
return [key] + suffix_found
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param [Symbol] nested_object_name
|
48
|
+
# @return [Array<Symbol>]
|
49
|
+
def path_in_mapping_to(nested_object_name, root_properties: nil)
|
50
|
+
root_properties ||= default_root_properties
|
51
|
+
|
52
|
+
keys = root_properties.keys
|
53
|
+
|
54
|
+
keys.each do |key|
|
55
|
+
if key == nested_object_name
|
56
|
+
return [key]
|
57
|
+
end
|
58
|
+
|
59
|
+
next if root_properties[key][:type] != 'object'
|
60
|
+
|
61
|
+
suffix = path_in_mapping_to(nested_object_name, root_properties: root_properties[key][:properties])
|
62
|
+
|
63
|
+
if suffix.include? nested_object_name
|
64
|
+
return [key] + suffix
|
65
|
+
end
|
66
|
+
end
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
|
70
|
+
def has_document_field_named?(field_name)
|
71
|
+
!! document_field_named(field_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param [Array<Symbol>] path
|
75
|
+
def nested_object_fields_for(path, root_properties: nil)
|
76
|
+
root_properties ||= default_root_properties
|
77
|
+
|
78
|
+
keys = root_properties.keys
|
79
|
+
|
80
|
+
suffix, *postfix = path
|
81
|
+
|
82
|
+
return root_properties.keys if suffix.nil?
|
83
|
+
|
84
|
+
keys.each do |key|
|
85
|
+
if key == suffix
|
86
|
+
result = nested_object_fields_for(postfix, root_properties: root_properties[key][:properties])
|
87
|
+
return result if result
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
def default_root_properties
|
95
|
+
base.mappings.to_hash[:"#{base.document_type}"][:properties]
|
96
|
+
end
|
97
|
+
|
98
|
+
def document_field_named(field_name)
|
99
|
+
root_properties ||= default_root_properties
|
100
|
+
root_properties[field_name]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|