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