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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -1
  4. data/README.md +5 -0
  5. data/gemfiles/rails41.gemfile +11 -0
  6. data/gemfiles/rails41.gemfile.lock +109 -0
  7. data/lib/elasticsearch/model/extensions/association_path_finding/association_path_finder.rb +19 -0
  8. data/lib/elasticsearch/model/extensions/association_path_finding/mapping_node.rb +68 -0
  9. data/lib/elasticsearch/model/extensions/association_path_finding/shortest_path.rb +108 -0
  10. data/lib/elasticsearch/model/extensions/batch_updating.rb +9 -52
  11. data/lib/elasticsearch/model/extensions/batch_updating/batch_updater.rb +68 -0
  12. data/lib/elasticsearch/model/extensions/configuration.rb +33 -5
  13. data/lib/elasticsearch/model/extensions/dependency_tracking.rb +17 -21
  14. data/lib/elasticsearch/model/extensions/dependency_tracking/dependency_tracker.rb +52 -0
  15. data/lib/elasticsearch/model/extensions/mapping_reflection.rb +7 -80
  16. data/lib/elasticsearch/model/extensions/mapping_reflection/mapping_reflector.rb +107 -0
  17. data/lib/elasticsearch/model/extensions/outer_document_updating.rb +0 -21
  18. data/lib/elasticsearch/model/extensions/partial_updating.rb +16 -116
  19. data/lib/elasticsearch/model/extensions/partial_updating/partial_updater.rb +149 -0
  20. data/lib/elasticsearch/model/extensions/proxy.rb +0 -0
  21. data/lib/elasticsearch/model/extensions/update_callback.rb +3 -2
  22. data/lib/elasticsearch/model/extensions/version.rb +1 -1
  23. data/spec/batch_updating/batch_updater_spec.rb +50 -0
  24. data/spec/batch_updating_spec.rb +134 -0
  25. data/spec/integration_spec.rb +350 -4
  26. data/spec/partial_updating_spec.rb +36 -6
  27. data/spec/setup/articles_with_comments.rb +2 -4
  28. data/spec/setup/articles_with_comments_and_delayed_jobs.rb +2 -4
  29. data/spec/setup/authors_and_books_with_tags.rb +183 -0
  30. data/spec/setup/items_and_categories.rb +0 -0
  31. data/spec/setup/sqlite.rb +1 -1
  32. data/spec/setup/undefine.rb +6 -6
  33. data/spec/spec_helper.rb +6 -0
  34. metadata +20 -4
  35. data/lib/elasticsearch/model/extensions/mapping_node.rb +0 -65
  36. 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 = child_class.path_from(@parent_class)
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
- # TODO 勝手にインスタンスの状態を書き換えていて、相当いまいち。インスタンス外に出す。
62
- child_class.instance_variable_set :@nested_object_fields, @parent_class.nested_object_fields_for(parent_to_child_path).map(&:to_s)
63
- child_class.instance_variable_set :@has_dependent_fields, @parent_class.has_dependent_fields?(field_to_update) ||
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
- self.class.each_dependent_attribute_for(changes) do |a|
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
- module ClassMethods
18
- def tracks_attributes_dependencies(dependencies)
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
- def each_dependent_attribute_for(changed_attributes)
23
- const_get('DEPENDENT_CUSTOM_ATTRIBUTES').each do |attributes, dependent_attributes|
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
- def has_dependent_fields?(field)
33
- const_get('DEPENDENT_CUSTOM_ATTRIBUTES').any? do |from, to|
34
- from.include? field.to_s
35
- end
26
+ module ClassMethods
27
+ # @return [DependencyTracker]
28
+ def __dependency_tracker__
29
+ @__dependency_tracker__
36
30
  end
37
31
 
38
- def has_association_named?(table_name)
39
- reflect_on_all_associations.any? { |a| a.name == table_name }
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
- # @param [Class] destination_class
11
- def path_in_mapping_to_class(destination_class, current_properties: nil, current_class: nil, visited_classes: nil)
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