activerecord_reindex 0.1.2 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6508847b036e9191ba24d150c3ca6fd3aff0d147
4
- data.tar.gz: 1a73c90b46cb0b0f76ddad37e2451737b1f53112
3
+ metadata.gz: 3935a55283f2824e669f9f5b669b82ff4242779e
4
+ data.tar.gz: 848a4b8e5ceb4379846ce2119ff38292fbe56494
5
5
  SHA512:
6
- metadata.gz: 583b1b18878781670b7180cdb15453e7e0306b46dbc30f3f3655e638f2e7108e367818ed6b3b8d6b0f6c4f17db28214a1512607b6d8e39101cf89d894d27ebe2
7
- data.tar.gz: 6d5367d14ca8223e851c8d6a98b26102db0cf48db5562c8351c4f5c2a673679d08fcebec36e307db52a2e7cdf56be78ac615d7045b519e945846ce6e92dd40f8
6
+ metadata.gz: 6a35107ab674882f57ad17d89ada77a3fcd34ead94553633774abae201308b6cd112860275920bfeafbb0b2546199b34940164b9007348f12c824c31fa95d06b
7
+ data.tar.gz: 86da00f80b987fef72f7662104661b3ad43598d80b2fa7c4c93417a3ddd29b42a559940dff20424a46b38b75494eb91f20f1767c3d64baf0e0a32ac1de6498b2
@@ -1,21 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
  # author: Vadim Shaveiko <@vshaveyko>
3
3
  # Abstract adapter class
4
- # New adapters should implement :call method that will perform reindexing of the given record
5
- # Example:
6
- # def call(record)
7
- # reindex_it(record)
8
- # end
4
+ # New adapters should implement :_single_reindex and :_mass_reindex methods that will perform reindexing of the given records
5
+ #
9
6
  module ActiverecordReindex
10
7
  class Adapter
11
8
 
12
- # check if record of this class can be reindexed
9
+ #
10
+ # check if record of this class can be reindexed ==
13
11
  # check if klass inherits from elasticsearch-model base class
14
- # and have method required for reindexing
12
+ #
15
13
  def self._check_elasticsearch_connection(klass)
16
14
  klass < Elasticsearch::Model
17
15
  end
18
16
 
17
+ #
18
+ # updates index directly in elasticsearch through
19
+ # Elasticsearch::Model instance method
20
+ # if class not inherited from Elasticsearch::Model it skips since it cannot be reindexed
21
+ #
22
+ # ***nasty-stuff***
23
+ # hooking into update_document has sudden side-effect
24
+ # if associations defined two-way they will trigger reindex recursively and result in StackLevelTooDeep
25
+ # hence to prevent this we're passing request_record to adapter
26
+ # request record is record that initted reindex for current record as association
27
+ # we will skip it in associations reindex to prevent recursive reindex and StackLevelTooDeep error
28
+ #
29
+ def self.call(request_record, record: nil, records: nil)
30
+ if record
31
+ return unless _check_elasticsearch_connection(record.class)
32
+
33
+ _single_reindex(request_record, record)
34
+ elsif records && record = records.first
35
+ return unless _check_elasticsearch_connection(record.class)
36
+
37
+ _mass_reindex(request_record, record.class.name, records)
38
+ end
39
+ end
40
+
19
41
  end
20
42
 
21
43
  end
@@ -1,7 +1,6 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
3
  # author: Vadim Shaveiko <@vshaveyko>
4
-
5
4
  # Adds reindex option to associations
6
5
  # values accepted are true, :async. Default false.
7
6
  # If true it will add syncronous elasticsearch reindex callbacks on:
@@ -9,6 +8,8 @@
9
8
  # 2. record destroyed
10
9
  # 3. record index updated
11
10
  # if :async it will add async callbacks in same cases
11
+ require_relative 'reindex_hook'
12
+
12
13
  module ActiveRecord
13
14
  module Associations
14
15
  module Builder
@@ -60,7 +61,11 @@ module ActiveRecord
60
61
  def add_destroy_reindex_callback(model, reflection, async:)
61
62
  return if [:destroy, :delete_all].include? reflection.options[:dependent]
62
63
 
63
- model.after_commit on: :destroy, &callback(async, reflection)
64
+ destroy_callback = callback(async, reflection)
65
+
66
+ model.after_commit(on: :destroy) do
67
+ destroy_callback.call(self)
68
+ end
64
69
  end
65
70
 
66
71
  # add callback to reindex associations on update
@@ -70,12 +75,20 @@ module ActiveRecord
70
75
  def add_update_reindex_callback(model, reflection, async:)
71
76
  return if model < Elasticsearch::Model
72
77
 
73
- model.after_commit on: :update, &callback(async, reflection)
78
+ # for why it is needed see reindex_hook.rb
79
+ model.include ActiverecordReindex::ReindexHook
80
+
81
+ update_callback = callback(async, reflection)
82
+
83
+ model.after_commit(on: :update) do
84
+ next unless changed_index_relevant_attributes?
85
+ update_callback.call(self)
86
+ end
74
87
  end
75
88
 
76
89
  # callback methods defined in ActiveRecord::Base monkeypatch
77
90
  def callback(async, reflection)
78
- async ? -> { reindex_async(reflection) } : -> { reindex_sync(reflection) }
91
+ async ? ->(record) { record.reindex_async(reflection) } : ->(record) { record.reindex_sync(reflection) }
79
92
  end
80
93
 
81
94
  end
@@ -1,40 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
  # author: Vadim Shaveiko <@vshaveyko>
3
-
4
3
  require_relative 'adapter'
5
4
  module ActiverecordReindex
5
+ #
6
6
  # Asyncronouse reindex adapter
7
7
  # uses Jobs for reindexing records asyncronously
8
8
  # Using ActiveJob as dependency bcs activerecord is required for this so
9
9
  # in most cases it would be used with rails hence with ActiveJob
10
10
  # later can think about adding support for differnt job adapters
11
+ #
11
12
  class AsyncAdapter < Adapter
12
13
 
14
+ #
13
15
  # Job wrapper. Queues elastic_index queue for each reindex
16
+ #
14
17
  class UpdateJob < ::ActiveJob::Base
15
18
 
16
- # TODO: make queue name configurable
17
- queue_as :elastic_index
19
+ queue_as ActiverecordReindex.config.index_queue
18
20
 
19
21
  def perform(klass, id, request_record_klass, request_record_id)
20
22
  klass = klass.constantize
23
+
21
24
  request_record = request_record_klass.constantize.find(request_record_id)
25
+
22
26
  klass.find(id).__elasticsearch__.update_document(request_record: request_record)
23
27
  end
24
28
 
25
29
  end
26
30
 
31
+ class MassUpdateJob < ::ActiveJob::Base
32
+
33
+ queue_as ActiverecordReindex.config.index_queue
34
+
35
+ def perform(klass, ids, request_record_klass, request_record_id)
36
+ klass = klass.constantize
37
+
38
+ request_record = request_record_klass.constantize.find(request_record_id)
39
+
40
+ klass.find(ids).each do |record|
41
+ record.__elasticsearch__.update_document(request_record: request_record)
42
+ end
43
+ end
44
+
45
+ end
46
+
27
47
  class << self
28
48
 
29
- # ***nasty-stuff***
30
- # hooking into update_document has sudden side-effect
31
- # if associations defined two-way they will trigger reindex recursively and result in StackLevelTooDeep
32
- # hence to prevent this we're passing request_record to adapter
33
- # request record is record that initted reindex for current record as association
34
- # we will skip it in associations reindex to prevent recursive reindex and StackLevelTooDeep error
35
- def call(record, request_record)
36
- return unless _check_elasticsearch_connection(record.class)
37
- UpdateJob.perform_later(record.class.to_s, record.id, request_record.class.to_s, request_record.id)
49
+ private
50
+
51
+ #
52
+ # UpdateJob is default for this
53
+ # uses configured job class otherwise
54
+ #
55
+ def _single_reindex(request_record, record)
56
+ ActiverecordReindex.config.index_class
57
+ .perform_later(record.class.name,
58
+ record.id,
59
+ request_record.class.name,
60
+ request_record.id)
61
+ end
62
+
63
+ #
64
+ # MassUpdateJob is default for this
65
+ # uses configured job class otherwise
66
+ #
67
+ # used for saving time on creating jobs in realtime
68
+ # create one job that will reindex all records internally
69
+ #
70
+ def _mass_reindex(request_record, class_name, records)
71
+ ActiverecordReindex.config.mass_index_class
72
+ .perform_later(class_name,
73
+ records.ids,
74
+ request_record.class.name,
75
+ request_record.id)
38
76
  end
39
77
 
40
78
  end
@@ -14,7 +14,8 @@ module ActiveRecord
14
14
  super
15
15
  class << child
16
16
 
17
- attr_accessor :reindexer, :async_adapter, :sync_adapter, :sync_reindexable_reflections, :async_reindexable_reflections
17
+ attr_accessor :reindexer, :async_adapter, :sync_adapter, :sync_reindexable_reflections,
18
+ :async_reindexable_reflections, :reindex_attr_blacklist, :reindex_attr_whitelist
18
19
 
19
20
  end
20
21
 
@@ -25,7 +26,12 @@ module ActiveRecord
25
26
  child.reindexer = ActiverecordReindex::Reindexer.new
26
27
  # TODO: provide config for changing adapters
27
28
  # For now can set adapter through writers inside class
28
- child.async_adapter = ActiverecordReindex::AsyncAdapter
29
+ if ActiverecordReindex.config.async_reindex_only_in_production? && !Rails.env.production?
30
+ child.async_adapter = ActiverecordReindex::SyncAdapter
31
+ else
32
+ child.async_adapter = ActiverecordReindex::AsyncAdapter
33
+ end
34
+
29
35
  child.sync_adapter = ActiverecordReindex::SyncAdapter
30
36
  end
31
37
 
@@ -45,5 +51,26 @@ module ActiveRecord
45
51
  .call(self, reflection: reflection, skip_record: skip_record)
46
52
  end
47
53
 
54
+ def changed_index_relevant_attributes?
55
+ return true unless self.class.reindex_attr_blacklist || self.class.reindex_attr_whitelist
56
+ changed = changed_attributes.keys
57
+ wl = self.class.reindex_attr_whitelist&.map(&:to_sym)
58
+ bl = self.class.reindex_attr_blacklist&.map(&:to_sym)
59
+
60
+ if wl
61
+ whitelisted = wl & changed
62
+ else
63
+ whitelisted = changed
64
+ end
65
+
66
+ if bl
67
+ blacklisted = changed - bl
68
+ else
69
+ blacklisted = []
70
+ end
71
+
72
+ !(whitelisted - blacklisted).empty?
73
+ end
74
+
48
75
  end
49
76
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ # helpers for reindexing all required reflections on record's class
4
+ module ActiverecordReindex
5
+ module ReflectionReindex
6
+
7
+ def update_document_hook(request_record)
8
+ return unless _active_record_model?(self.class)
9
+ _reindex_reflections(self.class, request_record)
10
+ end
11
+
12
+ private
13
+
14
+ def _active_record_model?(klass)
15
+ klass < ActiveRecord::Base
16
+ end
17
+
18
+ def _reindex_reflections(klass, request_record)
19
+ klass.sync_reindexable_reflections.each do |reflection|
20
+ target.reindex_sync(reflection, skip_record: request_record)
21
+ end
22
+
23
+ klass.async_reindexable_reflections.each do |reflection|
24
+ target.reindex_async(reflection, skip_record: request_record)
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ module ActiverecordReindex
4
+ # Since recursive reindexing can go through models that does not
5
+ # exist in elastic and not inherited from Elsaticsearch::Model
6
+ # they will not have __elasticsearch__.update_document method
7
+ # those will stop recursive reindexing chain in its way
8
+ # To prevent this we emulate __elasticsearch__.update_document on them
9
+ # and continue reindex chain as is, without reindexing this records
10
+ module ReindexHook
11
+
12
+ def __elasticsearch__
13
+ @__elasticsearch__ ||= ActiverecordReindex::ElasticsearchProxy.new(self.class)
14
+ end
15
+
16
+ end
17
+
18
+ # Proxy to imitate missing __elasticsearch__ on models without
19
+ # Elasticsearch::Model included
20
+ require 'activerecord_reindex/reflection_reindex'
21
+ class ElasticsearchProxy
22
+
23
+ include ActiverecordReindex::ReflectionReindex
24
+
25
+ def initialize(klass)
26
+ @klass = klass
27
+ end
28
+
29
+ def update_document(*, request_record: nil)
30
+ # defined in ActiverecordReindex::ReflectionReindex
31
+ update_document_hook(request_record)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -3,61 +3,69 @@
3
3
  module ActiverecordReindex
4
4
  class Reindexer
5
5
 
6
+ #
6
7
  # chain strategy before actual executing
7
8
  # strategy can be either sync or async
8
9
  # corresponding to type of reindexing
9
10
  # additional strategies can be defined and specified by user
11
+ #
10
12
  def with_strategy(strategy)
11
13
  @strategy = strategy
12
14
  self
13
15
  end
14
16
 
17
+ #
15
18
  # reindex records associated with given record on given association
16
19
  # if association is collection(has_many, has_many_through, has_and_belongs_to_many)
17
20
  # get all associated recrods and reindex them
18
21
  # else
19
22
  # reindex given record associted one
23
+ #
20
24
  def call(record, reflection:, skip_record:)
21
25
  if reflection.collection?
22
26
  _reindex_collection(reflection, record, skip_record)
23
27
  else
24
- associated_record = record.public_send(reflection.name)
25
- return if associated_record == skip_record
26
- _update_index(associated_record, record)
28
+ _reindex_single(reflection, record, skip_record)
27
29
  end
28
30
  end
29
31
 
30
32
  private
31
33
 
32
- # TODO: add bulk reindex if need performance
34
+ #
33
35
  # raise if strategy was not specified or doesn't respond to call which is required for strategy
34
- # pass record to strategy and execute reindex
35
- # clear strategy to not mess up future reindexing
36
- def _update_index(associated_record, record)
37
- _check_strategy
38
-
39
- @strategy.call(associated_record, record)
40
-
41
- _clear_strategy
42
- end
43
-
36
+ #
44
37
  def _check_strategy
45
38
  raise ArgumentError, 'No strategy specified.' unless @strategy
46
39
  raise ArgumentError, "Strategy specified incorrect. Check if #{@strategy} responds to :call." unless @strategy.respond_to? :call
47
40
  end
48
41
 
42
+ # clear strategy to not mess up future reindexing
49
43
  def _clear_strategy
50
44
  @strategy = nil
51
45
  end
52
46
 
47
+ #
48
+ # TODO: got clearing collection bug, write test for it
49
+ #
50
+ def _reindex_single(reflection, record, skip_record)
51
+ _check_strategy
52
+
53
+ associated_record = record.public_send(reflection.name)
54
+ return if associated_record == skip_record
55
+ @strategy.call(record, record: associated_record)
56
+
57
+ _clear_strategy
58
+ end
59
+
53
60
  def _reindex_collection(reflection, record, skip_record)
54
- collection = record.public_send(reflection.name)
61
+ _check_strategy
55
62
 
63
+ collection = record.public_send(reflection.name)
56
64
  collection -= [skip_record] if reflection.klass == skip_record.class
57
65
 
58
- collection.each do |associated_record|
59
- _update_index(associated_record, record)
60
- end
66
+ @strategy.call(record, records: collection)
67
+
68
+ _clear_strategy
61
69
  end
62
70
 
63
71
  end
@@ -7,19 +7,22 @@ module ActiverecordReindex
7
7
 
8
8
  class << self
9
9
 
10
- # updates index directly in elasticsearch through
11
- # Elasticsearch::Model instance method
12
- # if class not inherited from Elasticsearch::Model it skips since it cannot be reindexing
13
- # TODO: show error\warning about trying to reindex record that is not connection to elastic
14
- # ***nasty-stuff***
15
- # hooking into update_document has sudden side-effect
16
- # if associations defined two-way they will trigger reindex recursively and result in StackLevelTooDeep
17
- # hence to prevent this we're passing request_record to adapter
18
- # request record is record that initted reindex for current record as association
19
- # we will skip it in associations reindex to prevent recursive reindex and StackLevelTooDeep error
20
- def call(record, request_record)
21
- return unless _check_elasticsearch_connection(record.class)
10
+ private
11
+
12
+ def _single_reindex(request_record, record)
13
+ _update_index_on_record(record, request_record)
14
+ end
15
+
16
+ def _mass_reindex(request_record, _class_name, records)
17
+ records.each do |record|
18
+ _update_index_on_record(record, request_record)
19
+ end
20
+ end
21
+
22
+ def _update_index_on_record(record, request_record)
22
23
  record.__elasticsearch__.update_document(request_record: request_record)
24
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
25
+ record.__elasticsearch__.index_document
23
26
  end
24
27
 
25
28
  end
@@ -1,36 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
  # author: Vadim Shaveiko <@vshaveyko>
3
+ require 'activerecord_reindex/reflection_reindex'
4
+
3
5
  module Elasticsearch
4
6
  module Model
5
7
  module Indexing
6
8
  module InstanceMethods
7
9
 
10
+ include ActiverecordReindex::ReflectionReindex
11
+
8
12
  alias original_update_document update_document
9
13
 
14
+ #
10
15
  # monkey patch update_document method from elasticsearch gem
11
16
  # use +super+ and hook on reindex to reindex associations
17
+ #
12
18
  # for why request_record needed here and what it is see sync_adapter.rb
19
+ #
13
20
  def update_document(*args, request_record: nil)
14
- if _active_record_model?(self.class)
15
- _reindex_reflections(self.class, request_record)
16
- end
17
- original_update_document(*args)
18
- end
19
-
20
- private
21
+ # defined in ActiverecordReindex::ReflectionReindex
22
+ update_document_hook(request_record)
21
23
 
22
- def _active_record_model?(klass)
23
- klass < ActiveRecord::Base
24
- end
25
-
26
- def _reindex_reflections(klass, request_record)
27
- klass.sync_reindexable_reflections.each do |reflection|
28
- target.reindex_sync(reflection, skip_record: request_record)
29
- end
30
-
31
- klass.async_reindexable_reflections.each do |reflection|
32
- target.reindex_async(reflection, skip_record: request_record)
33
- end
24
+ original_update_document(*args)
34
25
  end
35
26
 
36
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  # author: Vadim Shaveiko <@vshaveyko>
3
3
  module ActiverecordReindex
4
- VERSION = '0.1.2'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -2,6 +2,49 @@
2
2
  # author: Vadim Shaveiko <@vshaveyko>
3
3
  require 'activerecord_reindex/version'
4
4
 
5
+ module ActiverecordReindex
6
+
7
+ class Config
8
+
9
+ attr_accessor :index_queue, :index_class, :mass_index_class, :async_reindex_only_in_production
10
+
11
+ def initialize
12
+ @index_queue = :elastic_index
13
+ @async_reindex_only_in_production = false
14
+ end
15
+
16
+ def async_reindex_only_in_production?
17
+ @async_reindex_only_in_production
18
+ end
19
+
20
+ def index_class
21
+ @index_class || ActiverecordReindex::AsyncAdapter::UpdateJob
22
+ end
23
+
24
+ def mass_index_class
25
+ @mass_index_class || ActiverecordReindex::AsyncAdapter::MassUpdateJob
26
+ end
27
+
28
+ end
29
+
30
+ class << self
31
+
32
+ def configure
33
+ yield config
34
+ end
35
+
36
+ def config
37
+ @_configuration ||= Config.new
38
+ end
39
+
40
+ def reset_configuration
41
+ @_configuration = nil
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+
5
48
  # monkey patch active record associations
6
49
  require 'active_record'
7
50
  require 'active_job'
@@ -13,6 +56,3 @@ require 'activerecord_reindex/association_reflection'
13
56
  # monkey patch elasticsearch/model
14
57
  require 'elasticsearch/model'
15
58
  require 'activerecord_reindex/update_document_monkey_patch'
16
-
17
- module ActiverecordReindex
18
- end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord_reindex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - vs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-20 00:00:00.000000000 Z
11
+ date: 2017-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: rails
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.0'
27
- - !ruby/object:Gem::Dependency
28
- name: activejob
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: elasticsearch-model
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +52,8 @@ files:
66
52
  - lib/activerecord_reindex/association_reflection.rb
67
53
  - lib/activerecord_reindex/async_adapter.rb
68
54
  - lib/activerecord_reindex/base.rb
55
+ - lib/activerecord_reindex/reflection_reindex.rb
56
+ - lib/activerecord_reindex/reindex_hook.rb
69
57
  - lib/activerecord_reindex/reindexer.rb
70
58
  - lib/activerecord_reindex/sync_adapter.rb
71
59
  - lib/activerecord_reindex/update_document_monkey_patch.rb
@@ -90,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
78
  version: '0'
91
79
  requirements: []
92
80
  rubyforge_project:
93
- rubygems_version: 2.5.1
81
+ rubygems_version: 2.6.8
94
82
  signing_key:
95
83
  specification_version: 4
96
84
  summary: Add Elasticsearch reindex option to ActiveRecord associations