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