activerecord_reindex 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3eeb09396b487cf9fbd9d02ecb4f78ff27523665
4
+ data.tar.gz: 5b410be8a9139992259f0d87ff0c0d0bbb7db2f4
5
+ SHA512:
6
+ metadata.gz: c63d9c04a190fe046eaca49262b6b327d6cb1f5108d1a6f1a78d8dfb13ce3ee77276b6ffae21b01232077cbe65a4671c896bd286719297d40fb6f09b14f1d241
7
+ data.tar.gz: 7359249c0e579f8889dd3d37c41b0e6eb1533fb451e5521fcb357ee924d1a3c1cdd560ec479aeb316376c73bcc3a54e5278ac609576812cf01e27d6bf7488d83
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Health24
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ require 'activerecord_reindex/version'
4
+
5
+ # monkey patch active record associations
6
+ require 'active_record'
7
+ require 'active_job'
8
+
9
+ require 'activerecord_reindex/base'
10
+ require 'activerecord_reindex/association'
11
+ require 'activerecord_reindex/association_reflection'
12
+
13
+ # monkey patch elasticsearch/model
14
+ require 'elasticsearch/model'
15
+ require 'activerecord_reindex/update_document_monkey_patch'
16
+
17
+ module ActiverecordReindex
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
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
9
+ module ActiverecordReindex
10
+ class Adapter
11
+
12
+ # check if record of this class can be reindexed
13
+ # check if klass inherits from elasticsearch-model base class
14
+ # and have method required for reindexing
15
+ def self._check_elasticsearch_connection(klass)
16
+ klass < Elasticsearch::Model
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ # author: Vadim Shaveiko <@vshaveyko>
4
+
5
+ # Adds reindex option to associations
6
+ # values accepted are true, :async. Default false.
7
+ # If true it will add syncronous elasticsearch reindex callbacks on:
8
+ # 1. record updated
9
+ # 2. record destroyed
10
+ # 3. record index updated
11
+ # if :async it will add async callbacks in same cases
12
+ module ActiveRecord
13
+ module Associations
14
+ module Builder
15
+ class Association
16
+
17
+ class << self
18
+
19
+ alias original_valid_options valid_options
20
+
21
+ # This method monkey patches ActiveRecord valid_options to add one more valid option :reindex
22
+ # Examples:
23
+ # belongs_to :tag, reindex: true
24
+ # belongs_to :tagging, reindex: :async
25
+ # has_many :tags, reindex: async
26
+ # has_many :tags, through: :taggings, reindex: true
27
+ def valid_options(*args)
28
+ original_valid_options(*args) + [:reindex]
29
+ end
30
+
31
+ alias original_define_callbacks define_callbacks
32
+
33
+ # This method monkeypatches ActiveRecord define_callbacks to
34
+ # add reindex callbacks if corresponding option specified
35
+ # if reindex; true - add syncronous callback to reindex associated records
36
+ # if reindex: :async - add asyncronous callback to reindex associated records
37
+ def define_callbacks(model, reflection)
38
+ original_define_callbacks(model, reflection)
39
+ if reflection.reindex_sync?
40
+ add_reindex_callback(model, reflection, async: false)
41
+ model.sync_reindexable_reflections += [reflection]
42
+ elsif reflection.reindex_async?
43
+ add_reindex_callback(model, reflection, async: true)
44
+ model.async_reindexable_reflections += [reflection]
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # manages adding of callbacks considering async option
51
+ def add_reindex_callback(model, reflection, async:)
52
+ add_destroy_reindex_callback(model, reflection, async: async)
53
+
54
+ add_update_reindex_callback(model, reflection, async: async)
55
+ end
56
+
57
+ # add callback to reindex associated records on destroy
58
+ # if association has dependent: :destroy or dependent: :delete_all
59
+ # we skip this callback since destroyed records should reindex themselves
60
+ def add_destroy_reindex_callback(model, reflection, async:)
61
+ return if [:destroy, :delete_all].include? reflection.options[:dependent]
62
+
63
+ model.after_commit on: :destroy, &callback(async, reflection)
64
+ end
65
+
66
+ # add callback to reindex associations on update
67
+ # if model inherited from Elasticsearch::Model it means it have own index in elasticsearch
68
+ # and therefore should reindex itself on update those triggering update_document hook
69
+ # to prevent double reindex we're not adding update callback on such models
70
+ def add_update_reindex_callback(model, reflection, async:)
71
+ return if model < Elasticsearch::Model
72
+
73
+ model.after_commit on: :update, &callback(async, reflection)
74
+ end
75
+
76
+ # callback methods defined in ActiveRecord::Base monkeypatch
77
+ def callback(async, reflection)
78
+ async ? -> { reindex_async(reflection) } : -> { reindex_sync(reflection) }
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ # author: Vadim Shaveiko <@vshaveyko>
4
+ # Add helper methods to Activerecord Reflection
5
+ # for quick access to reindex options
6
+ module ActiveRecord
7
+ module Reflection
8
+ class AssociationReflection
9
+
10
+ def reindex_sync?
11
+ @options.fetch(:reindex, false) == true
12
+ end
13
+
14
+ def reindex_async?
15
+ @options.fetch(:reindex, false) == :async
16
+ end
17
+
18
+ end
19
+
20
+ class ThroughReflection
21
+
22
+ def reindex_sync?
23
+ @delegate_reflection.options.fetch(:reindex, false) == true
24
+ end
25
+
26
+ def reindex_async?
27
+ @delegate_reflection.options.fetch(:reindex, false) == :async
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+
4
+ # Asyncronouse reindex adapter
5
+ # uses Jobs for reindexing records asyncronously
6
+ # Using ActiveJob as dependency bcs activerecord is required for this so
7
+ # in most cases it would be used with rails hence with ActiveJob
8
+ # later can think about adding support for differnt job adapters
9
+ require_relative 'adapter'
10
+ module ActiverecordReindex
11
+ class AsyncAdapter < Adapter
12
+
13
+ # Job wrapper. Queues elastic_index queue for each reindex
14
+ class UpdateJob < ::ActiveJob::Base
15
+
16
+ # TODO: make queue name configurable
17
+ queue_as :elastic_index
18
+
19
+ def perform(klass, id, request_record_klass, request_record_id)
20
+ klass = klass.constantize
21
+ request_record = request_record_klass.constantize.find(request_record_id)
22
+ klass.find(id).__elasticsearch__.update_document(request_record: request_record)
23
+ end
24
+
25
+ end
26
+
27
+ class << self
28
+
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, record.id, request_record.class, request_record.id)
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ require_relative 'async_adapter'
4
+ require_relative 'sync_adapter'
5
+ require_relative 'reindexer'
6
+
7
+ # ActiveRecord::Base extension to provide methods for
8
+ # reindexing callbacks
9
+ # this methods requested in callbacks in association.rb
10
+ module ActiveRecord
11
+ class Base
12
+
13
+ def self.inherited(child)
14
+ super
15
+ class << child
16
+
17
+ attr_accessor :reindexer, :async_adapter, :sync_adapter, :sync_reindexable_reflections, :async_reindexable_reflections
18
+
19
+ end
20
+
21
+ # Init default values to prevent undefined method for nilClass error
22
+ child.sync_reindexable_reflections = []
23
+ child.async_reindexable_reflections = []
24
+
25
+ child.reindexer = ActiverecordReindex::Reindexer.new
26
+ # TODO: provide config for changing adapters
27
+ # For now can set adapter through writers inside class
28
+ child.async_adapter = ActiverecordReindex::AsyncAdapter
29
+ child.sync_adapter = ActiverecordReindex::SyncAdapter
30
+ end
31
+
32
+ def reindex_async(reflection, skip_record: nil)
33
+ _reindex(reflection, strategy: self.class.async_adapter, skip_record: skip_record)
34
+ end
35
+
36
+ def reindex_sync(reflection, skip_record: nil)
37
+ _reindex(reflection, strategy: self.class.sync_adapter, skip_record: skip_record)
38
+ end
39
+
40
+ private
41
+
42
+ def _reindex(reflection, strategy:, skip_record:)
43
+ self.class.reindexer
44
+ .with_strategy(strategy)
45
+ .call(self, reflection: reflection, skip_record: skip_record)
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ module ActiverecordReindex
4
+ class Reindexer
5
+
6
+ # chain strategy before actual executing
7
+ # strategy can be either sync or async
8
+ # corresponding to type of reindexing
9
+ # additional strategies can be defined and specified by user
10
+ def with_strategy(strategy)
11
+ @strategy = strategy
12
+ self
13
+ end
14
+
15
+ # reindex records associated with given record on given association
16
+ # if association is collection(has_many, has_many_through, has_and_belongs_to_many)
17
+ # get all associated recrods and reindex them
18
+ # else
19
+ # reindex given record associted one
20
+ def call(record, reflection:, skip_record:)
21
+ if reflection.collection?
22
+ _reindex_collection(reflection, record, skip_record)
23
+ else
24
+ associated_record = record.public_send(reflection.name)
25
+ return if associated_record == skip_record
26
+ _update_index(associated_record, record)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # TODO: add bulk reindex if need performance
33
+ # 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
+
44
+ def _check_strategy
45
+ raise ArgumentError, 'No strategy specified.' unless @strategy
46
+ raise ArgumentError, "Strategy specified incorrect. Check if #{@strategy} responds to :call." unless @strategy.respond_to? :call
47
+ end
48
+
49
+ def _clear_strategy
50
+ @strategy = nil
51
+ end
52
+
53
+ def _reindex_collection(reflection, record, skip_record)
54
+ collection = record.public_send(reflection.name)
55
+
56
+ collection -= [skip_record] if reflection.klass == skip_record.class
57
+
58
+ collection.each do |associated_record|
59
+ _update_index(associated_record, record)
60
+ end
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ # Reindexes records syncronously
4
+ require_relative 'adapter'
5
+ module ActiverecordReindex
6
+ class SyncAdapter < Adapter
7
+
8
+ class << self
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)
22
+ record.__elasticsearch__.update_document(request_record: request_record)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ module Elasticsearch
4
+ module Model
5
+ module Indexing
6
+ module InstanceMethods
7
+
8
+ alias original_update_document update_document
9
+
10
+ # monkey patch update_document method from elasticsearch gem
11
+ # use +super+ and hook on reindex to reindex associations
12
+ # for why request_record needed here and what it is see sync_adapter.rb
13
+ 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
+
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
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ # author: Vadim Shaveiko <@vshaveyko>
3
+ module ActiverecordReindex
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord_reindex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - vs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '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
+ - !ruby/object:Gem::Dependency
42
+ name: elasticsearch-model
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - vshaveyko@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - lib/activerecord_reindex.rb
64
+ - lib/activerecord_reindex/adapter.rb
65
+ - lib/activerecord_reindex/association.rb
66
+ - lib/activerecord_reindex/association_reflection.rb
67
+ - lib/activerecord_reindex/async_adapter.rb
68
+ - lib/activerecord_reindex/base.rb
69
+ - lib/activerecord_reindex/reindexer.rb
70
+ - lib/activerecord_reindex/sync_adapter.rb
71
+ - lib/activerecord_reindex/update_document_monkey_patch.rb
72
+ - lib/activerecord_reindex/version.rb
73
+ homepage: https://github.com/Health24/activerecord_reindex
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubyforge_project:
93
+ rubygems_version: 2.5.1
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Add Elasticsearch reindex option to ActiveRecord associations
97
+ test_files: []