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
@@ -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
|