activerecord_reindex 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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