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
@@ -1,4 +1,3 @@
1
- require_relative 'mapping_node'
2
1
  require_relative 'update_callback'
3
2
  require_relative 'destroy_callback'
4
3
 
@@ -10,11 +9,6 @@ module Elasticsearch
10
9
  klass.extend ClassMethods
11
10
  end
12
11
 
13
- def index_update_required?
14
- (previous_changes.keys & self.class.nested_object_fields).size > 0 ||
15
- (previous_changes.size > 0 && self.class.has_dependent_fields?)
16
- end
17
-
18
12
  class Update
19
13
  def initialize(from:, to:)
20
14
  @parent_class = from
@@ -96,21 +90,6 @@ module Elasticsearch
96
90
  end
97
91
 
98
92
  module ClassMethods
99
- def nested_object_fields
100
- @nested_object_fields
101
- end
102
-
103
- def has_dependent_fields?
104
- @has_dependent_fields
105
- end
106
-
107
- def path_from(from)
108
- Elasticsearch::Model::Extensions::MappingNode.
109
- from_class(from).
110
- breadth_first_search { |e| e.destination.relates_to_class?(self) }.
111
- first
112
- end
113
-
114
93
  def initialize_active_record!(active_record_class, parent_class: parent_class, delayed:, only_if: -> r { true }, records_to_update_documents: nil, field_to_update: nil, block: block)
115
94
  config = Elasticsearch::Model::Extensions::Configuration.new(active_record_class, parent_class: parent_class, delayed: delayed, only_if: binding.local_variable_get(:only_if), records_to_update_documents: records_to_update_documents,
116
95
  field_to_update: field_to_update,
@@ -1,141 +1,41 @@
1
+ require_relative 'partial_updating/partial_updater'
2
+
1
3
  module Elasticsearch
2
4
  module Model
3
5
  module Extensions
4
6
  module PartialUpdating
5
7
  def self.included(klass)
6
8
  klass.extend ClassMethods
7
- end
8
-
9
- def self.build_as_json_options(klass:, props: )
10
- indexed_attributes = props.keys
11
- associations = klass.reflect_on_all_associations.select { |a| %i| has_one has_many belongs_to |.include? a.macro }
12
- association_names = associations.map(&:name)
13
- persisted_attributes = klass.attribute_names.map(&:intern)
14
9
 
15
- nested_attributes = indexed_attributes & association_names
16
- method_attributes = indexed_attributes - persisted_attributes - nested_attributes
17
- only_attributes = indexed_attributes - nested_attributes
18
-
19
- options = {
20
- root: false
21
- }
22
-
23
- if only_attributes.size > 1
24
- options[:only] = only_attributes
25
- elsif only_attributes.size == 1
26
- options[:only] = only_attributes.first
27
- end
28
-
29
- if method_attributes.size > 1
30
- options[:methods] = method_attributes
31
- elsif method_attributes.size == 1
32
- options[:methods] = method_attributes.first
33
- end
34
-
35
- nested_attributes.each do |n|
36
- a = associations.find { |a| a.name == n.intern }
37
- nested_klass = a.class_name.constantize
38
- nested_prop = props[n]
39
- if nested_prop.present?
40
- options[:include] ||= {}
41
- options[:include][n] = build_as_json_options(
42
- klass: nested_klass,
43
- props: nested_prop[:properties]
44
- )
45
- end
46
- end
47
-
48
- options
10
+ klass.instance_variable_set :@__partial_updater__, PartialUpdater.new(klass)
49
11
  end
50
12
 
51
13
  def as_indexed_json(options={})
52
- as_json(options.merge(self.class.as_json_options))
53
- end
54
-
55
- # @param [Array<Symbol>] changed_attributes
56
- # @param [Proc<Symbol, Hash>] json_options
57
- def build_partial_document_for_update(*changed_attributes, json_options: nil)
58
- changes = {}
59
-
60
- json_options ||= -> field { self.class.partial_as_json_options(field) || {} }
61
-
62
- self.class.each_field_to_update_according_to_changed_fields(changed_attributes) do |field_to_update|
63
- options = json_options.call field_to_update
64
-
65
- json = __send__(:"#{field_to_update}").as_json(options)
66
-
67
- changes[field_to_update] = json
68
- end
69
-
70
- changes
14
+ as_json(options.merge(self.class.__partial_updater__.as_json_options))
71
15
  end
72
16
 
73
17
  def partially_update_document(*changed_attributes)
74
18
  if changed_attributes.empty?
75
19
  __elasticsearch__.index_document
76
20
  else
77
- begin
78
- partial_document = build_partial_document_for_update(*changed_attributes)
79
- rescue => e
80
- if defined? ::Rails
81
- ::Rails.logger.error "Error in #partially_update_document: #{e.message}\n#{e.backtrace.join("\n")}"
82
- else
83
- warn "Error in #partially_update_document: #{e.message}\n#{e.backtrace.join("\n")}"
84
- end
85
- end
21
+ partial_updater = self.class.__partial_updater__
86
22
 
87
- update_document(partial_document)
88
- end
89
- end
90
-
91
- # @param [Hash] partial_document
92
- def update_document(partial_document)
93
- klass = self.class
94
-
95
- __elasticsearch__.client.update(
96
- { index: klass.index_name,
97
- type: klass.document_type,
98
- id: self.id,
99
- body: { doc: partial_document } }
100
- ) if partial_document.size > 0
101
- end
102
-
103
- module ClassMethods
104
- def as_json_options
105
- @as_json_options ||= Elasticsearch::Model::Extensions::PartialUpdating.build_as_json_options(
106
- klass: self,
107
- props: self.mappings.to_hash[self.document_type.intern][:properties]
23
+ partial_document = partial_updater.build_partial_document_for_update_with_error_logging(
24
+ record: self,
25
+ changed_attributes: changed_attributes
108
26
  )
109
- end
110
-
111
- def partial_as_json_options(field)
112
- as_json_options[:include][field]
113
- end
114
27
 
115
- def each_field_to_update_according_to_changed_fields(changed_fields)
116
- root_mapping_properties = mappings.to_hash[:"#{document_type}"][:properties]
117
-
118
- changed_fields.each do |changed_field|
119
- field_mapping = root_mapping_properties[:"#{changed_field}"]
120
-
121
- next unless field_mapping
122
-
123
- yield changed_field
124
- end
125
-
126
- each_dependent_attribute_for(changed_fields.map(&:to_s)) do |a|
127
- a_sym = a.intern
128
-
129
- yield a_sym
28
+ if partial_document
29
+ partial_updater.update_document(id: self.id, doc: partial_document)
130
30
  end
131
31
  end
132
32
 
133
- def fields_to_update_according_to_changed_fields(changed_fields)
134
- fields = []
135
- each_field_to_update_according_to_changed_fields changed_fields do |field_to_update|
136
- fields << field_to_update
137
- end
138
- fields
33
+ true
34
+ end
35
+
36
+ module ClassMethods
37
+ def __partial_updater__
38
+ @__partial_updater__
139
39
  end
140
40
  end
141
41
 
@@ -0,0 +1,149 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Extensions
4
+ module PartialUpdating
5
+ class PartialUpdater
6
+ def initialize(base)
7
+ @base = base
8
+ end
9
+
10
+ def base
11
+ @base
12
+ end
13
+
14
+ def build_as_json_options(klass:, props: )
15
+ indexed_attributes = props.keys
16
+ associations = klass.reflect_on_all_associations.select { |a| %i| has_one has_many belongs_to |.include? a.macro }
17
+ association_names = associations.map(&:name)
18
+ persisted_attributes = klass.attribute_names.map(&:intern)
19
+
20
+ nested_attributes = indexed_attributes & association_names
21
+ method_attributes = indexed_attributes - persisted_attributes - nested_attributes
22
+ only_attributes = indexed_attributes - nested_attributes
23
+
24
+ options = {
25
+ root: false
26
+ }
27
+
28
+ nested_attributes.map!(&:to_s)
29
+ method_attributes.map!(&:to_s)
30
+ only_attributes.map!(&:to_s)
31
+
32
+ if only_attributes.size > 1
33
+ options[:only] = only_attributes
34
+ elsif only_attributes.size == 1
35
+ options[:only] = only_attributes.first
36
+ end
37
+
38
+ if method_attributes.size > 1
39
+ options[:methods] = method_attributes
40
+ elsif method_attributes.size == 1
41
+ options[:methods] = method_attributes.first
42
+ end
43
+
44
+ nested_attributes.each do |n|
45
+ n_as_sym = n.intern
46
+ a = associations.find { |a| a.name == n_as_sym }
47
+ nested_klass = a.class_name.constantize
48
+ nested_prop = props[n_as_sym]
49
+ if nested_prop.present?
50
+ options[:include] ||= {}
51
+ options[:include][n] = build_as_json_options(
52
+ klass: nested_klass,
53
+ props: nested_prop[:properties]
54
+ )
55
+ end
56
+ end
57
+
58
+ options
59
+ end
60
+
61
+ def as_json_options
62
+ @as_json_options ||= build_as_json_options(
63
+ klass: base,
64
+ props: base.mappings.to_hash[base.document_type.intern][:properties]
65
+ )
66
+ end
67
+
68
+ def partial_as_json_options(field)
69
+ as_json_options[:include][field.to_s]
70
+ end
71
+
72
+ def each_field_to_update_according_to_changed_fields(changed_fields)
73
+ root_mapping_properties = base.mappings.to_hash[:"#{base.document_type}"][:properties]
74
+
75
+ changed_fields.each do |changed_field|
76
+ field_mapping = root_mapping_properties[:"#{changed_field}"]
77
+
78
+ next unless field_mapping
79
+
80
+ yield changed_field
81
+ end
82
+
83
+ base.__dependency_tracker__.each_dependent_attribute_for(changed_fields.map(&:to_s)) do |a|
84
+ a_sym = a.intern
85
+
86
+ yield a_sym
87
+ end
88
+ end
89
+
90
+ def fields_to_update_according_to_changed_fields(changed_fields)
91
+ fields = []
92
+ each_field_to_update_according_to_changed_fields changed_fields do |field_to_update|
93
+ fields << field_to_update
94
+ end
95
+ fields
96
+ end
97
+
98
+ def build_partial_document_for_update_with_error_logging(record:, changed_attributes:, json_options: nil)
99
+ begin
100
+ build_partial_document_for_update(
101
+ record: record,
102
+ changed_attributes: changed_attributes,
103
+ json_options: json_options
104
+ )
105
+ rescue => e
106
+ if defined? ::Rails
107
+ ::Rails.logger.error "Error in #build_partial_document_for_update_with_error_logging: #{e.message}\n#{e.backtrace.join("\n")}"
108
+ else
109
+ warn "Error in #build_partial_document_for_update_with_error_logging: #{e.message}\n#{e.backtrace.join("\n")}"
110
+ end
111
+
112
+ nil
113
+ end
114
+ end
115
+
116
+ # @param [ActiveRecord::Base] record
117
+ # @param [Array<Symbol>] changed_attributes
118
+ # @param [Proc<Symbol, Hash>] json_options
119
+ def build_partial_document_for_update(record:, changed_attributes:, json_options: nil)
120
+ changes = {}
121
+
122
+ json_options ||= -> field { partial_as_json_options(field) || {} }
123
+
124
+ each_field_to_update_according_to_changed_fields(changed_attributes) do |field_to_update|
125
+ options = json_options.call field_to_update
126
+
127
+ json = record.__send__(:"#{field_to_update}").as_json(options)
128
+
129
+ changes[field_to_update] = json
130
+ end
131
+
132
+ changes
133
+ end
134
+
135
+ # @param [Hash] doc
136
+ def update_document(id:, doc:)
137
+ base.__elasticsearch__.client.update(
138
+ { index: base.index_name,
139
+ type: base.document_type,
140
+ id: id,
141
+ body: { doc: doc } }
142
+ ) if doc.size > 0
143
+ end
144
+
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
File without changes
@@ -9,9 +9,10 @@ module Elasticsearch
9
9
  records_to_update_documents = config.records_to_update_documents
10
10
  only_if = config.only_if
11
11
  callback = self
12
+ _config = config
12
13
 
13
14
  record.instance_eval do
14
- return unless only_if.call(self) && index_update_required?
15
+ return unless only_if.call(self) && _config.index_update_required?(self)
15
16
 
16
17
  target = records_to_update_documents.call(self)
17
18
 
@@ -39,4 +40,4 @@ module Elasticsearch
39
40
  end
40
41
  end
41
42
  end
42
- end
43
+ end
@@ -1,7 +1,7 @@
1
1
  module Elasticsearch
2
2
  module Model
3
3
  module Extensions
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,50 @@
1
+ require 'elasticsearch/model/extensions/all'
2
+
3
+ RSpec.describe Elasticsearch::Model::Extensions::BatchUpdating::BatchUpdater do
4
+ before(:each) do
5
+ load 'setup/articles_with_comments.rb'
6
+ end
7
+
8
+ after :each do
9
+ ActiveRecord::Schema.define(:version => 2) do
10
+ drop_table :comments
11
+ drop_table :articles
12
+ end
13
+ end
14
+
15
+ let(:batch_updater) {
16
+ Article.__batch_updater__
17
+ }
18
+
19
+ describe '#split_ids_into' do
20
+ subject {
21
+ batch_updater.split_ids_into(2, min: 0, max: 3)
22
+ }
23
+
24
+ specify {
25
+ expect(subject).to eq([0..1, 2..3])
26
+ }
27
+ end
28
+
29
+ context 'the index dropped' do
30
+ before(:each) do
31
+ Article.__elasticsearch__.create_index! force: true
32
+ Article.__elasticsearch__.refresh_index!
33
+ end
34
+
35
+ def number_of_articles_about_coding
36
+ Article.search('Coding').records.size
37
+ end
38
+
39
+ describe '.update_index_in_parallel' do
40
+ subject {
41
+ batch_updater.update_index_in_batch(Article.all.to_ary)
42
+ Article.__elasticsearch__.refresh_index!
43
+ }
44
+
45
+ specify {
46
+ expect { subject }.to change { number_of_articles_about_coding }.by(2)
47
+ }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,134 @@
1
+ require 'elasticsearch/model/extensions/all'
2
+
3
+ require 'parallel'
4
+
5
+ RSpec.describe Elasticsearch::Model::Extensions::BatchUpdating do
6
+ before(:each) do
7
+ load 'setup/articles_with_comments.rb'
8
+ end
9
+
10
+ after(:each) do
11
+ ActiveRecord::Schema.define(:version => 2) do
12
+ drop_table :comments
13
+ drop_table :articles
14
+ end
15
+ end
16
+
17
+ describe '.with_indexed_tables_included' do
18
+ subject {
19
+ Article.with_indexed_tables_included
20
+ }
21
+
22
+ context 'without a `with_indexed_tables_included` implementation' do
23
+ specify {
24
+ expect { subject }.to raise_error
25
+ }
26
+ end
27
+
28
+ context 'with a `with_indexed_tables_included` implementation' do
29
+ before(:each) do
30
+ Article.class_eval do
31
+ def self.with_indexed_tables_included
32
+ includes(:comments)
33
+ end
34
+ end
35
+ end
36
+
37
+ specify {
38
+ expect { subject }.to_not raise_error
39
+ }
40
+ end
41
+ end
42
+
43
+ describe '.elasticsearch_hosts' do
44
+ subject {
45
+ Article.elasticsearch_hosts
46
+ }
47
+
48
+ context 'without a `elasticsearch_hosts` implementation' do
49
+ specify {
50
+ expect { subject }.to raise_error
51
+ }
52
+
53
+ specify {
54
+ expect { Article.__batch_updater__.reconnect! }.to raise_error
55
+ }
56
+ end
57
+
58
+ context 'with a `elasticsearch_hosts` implementation' do
59
+ before(:each) do
60
+ Article.class_eval do
61
+ def self.elasticsearch_hosts
62
+ 'http://localhost:9250'
63
+ end
64
+ end
65
+ end
66
+
67
+ specify {
68
+ expect { subject }.to_not raise_error
69
+ }
70
+ end
71
+ end
72
+
73
+ context 'the index dropped' do
74
+ before(:each) do
75
+ Article.__elasticsearch__.create_index! force: true
76
+ Article.__elasticsearch__.refresh_index!
77
+
78
+ Article.class_eval do
79
+ def self.elasticsearch_hosts
80
+ listened_port = (ENV['TEST_CLUSTER_PORT'] || 9250)
81
+ "http://localhost:#{listened_port}/"
82
+ end
83
+
84
+ def self.with_indexed_tables_included
85
+ includes(:comments)
86
+ end
87
+ end
88
+ end
89
+
90
+ shared_examples 'indexing all articles' do
91
+ def number_of_articles_about_coding
92
+ Article.search('Coding').records.size
93
+ end
94
+
95
+ specify {
96
+ expect { subject; Article.__elasticsearch__.refresh_index! }.to change { number_of_articles_about_coding }.by(2)
97
+ }
98
+ end
99
+
100
+ describe '.update_index_in_parallel' do
101
+ subject {
102
+ Article.update_index_in_parallel(parallelism: 2)
103
+ }
104
+
105
+ it_behaves_like 'indexing all articles'
106
+ end
107
+
108
+ describe '.update_index_in_batches' do
109
+ subject {
110
+ Article.update_index_in_batches
111
+ }
112
+
113
+ it_behaves_like 'indexing all articles'
114
+ end
115
+
116
+ describe 'Association/Extension' do
117
+ describe '.update_index_for_ids_in_range' do
118
+ subject {
119
+ Article.for_indexing.update_index_for_ids_in_range(Article.minimum(:id)..Article.maximum(:id))
120
+ }
121
+
122
+ it_behaves_like 'indexing all articles'
123
+ end
124
+
125
+ describe '.update_index_in_batches' do
126
+ subject {
127
+ Article.for_indexing.update_index_in_batches
128
+ }
129
+
130
+ it_behaves_like 'indexing all articles'
131
+ end
132
+ end
133
+ end
134
+ end