esse-async_indexing 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +35 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +168 -0
  6. data/LICENSE +21 -0
  7. data/README.md +262 -0
  8. data/Rakefile +4 -0
  9. data/docker-compose.yml +11 -0
  10. data/lib/esse/async_indexing/actions/batch_delete.rb +15 -0
  11. data/lib/esse/async_indexing/actions/batch_import.rb +16 -0
  12. data/lib/esse/async_indexing/actions/batch_import_all.rb +11 -0
  13. data/lib/esse/async_indexing/actions/batch_update.rb +32 -0
  14. data/lib/esse/async_indexing/actions/bulk_update_lazy_document_attribute.rb +20 -0
  15. data/lib/esse/async_indexing/actions/coerce_index_repository.rb +28 -0
  16. data/lib/esse/async_indexing/actions/delete_document.rb +16 -0
  17. data/lib/esse/async_indexing/actions/import_batch_id.rb +21 -0
  18. data/lib/esse/async_indexing/actions/index_document.rb +20 -0
  19. data/lib/esse/async_indexing/actions/update_document.rb +20 -0
  20. data/lib/esse/async_indexing/actions/update_lazy_document_attribute.rb +14 -0
  21. data/lib/esse/async_indexing/actions/upsert_document.rb +25 -0
  22. data/lib/esse/async_indexing/actions.rb +21 -0
  23. data/lib/esse/async_indexing/active_record.rb +102 -0
  24. data/lib/esse/async_indexing/active_record_callbacks/callback.rb +27 -0
  25. data/lib/esse/async_indexing/active_record_callbacks/lazy_update_attribute.rb +26 -0
  26. data/lib/esse/async_indexing/active_record_callbacks/on_create.rb +15 -0
  27. data/lib/esse/async_indexing/active_record_callbacks/on_destroy.rb +15 -0
  28. data/lib/esse/async_indexing/active_record_callbacks/on_update.rb +27 -0
  29. data/lib/esse/async_indexing/adapters/adapter.rb +29 -0
  30. data/lib/esse/async_indexing/adapters/faktory.rb +114 -0
  31. data/lib/esse/async_indexing/adapters/sidekiq.rb +94 -0
  32. data/lib/esse/async_indexing/adapters.rb +12 -0
  33. data/lib/esse/async_indexing/cli/async_import.rb +58 -0
  34. data/lib/esse/async_indexing/cli.rb +32 -0
  35. data/lib/esse/async_indexing/config.rb +27 -0
  36. data/lib/esse/async_indexing/configuration/base.rb +65 -0
  37. data/lib/esse/async_indexing/configuration/faktory.rb +6 -0
  38. data/lib/esse/async_indexing/configuration/sidekiq.rb +12 -0
  39. data/lib/esse/async_indexing/configuration.rb +45 -0
  40. data/lib/esse/async_indexing/errors.rb +12 -0
  41. data/lib/esse/async_indexing/jobs/bulk_update_lazy_document_attribute_job.rb +7 -0
  42. data/lib/esse/async_indexing/jobs/document_delete_by_id_job.rb +7 -0
  43. data/lib/esse/async_indexing/jobs/document_index_by_id_job.rb +7 -0
  44. data/lib/esse/async_indexing/jobs/document_update_by_id_job.rb +7 -0
  45. data/lib/esse/async_indexing/jobs/document_upsert_by_id_job.rb +7 -0
  46. data/lib/esse/async_indexing/jobs/import_all_job.rb +7 -0
  47. data/lib/esse/async_indexing/jobs/import_batch_id_job.rb +34 -0
  48. data/lib/esse/async_indexing/jobs/update_lazy_document_attribute_job.rb +7 -0
  49. data/lib/esse/async_indexing/testing.rb +79 -0
  50. data/lib/esse/async_indexing/version.rb +7 -0
  51. data/lib/esse/async_indexing/worker.rb +85 -0
  52. data/lib/esse/async_indexing/workers/faktory.rb +28 -0
  53. data/lib/esse/async_indexing/workers/shared_class_methods.rb +26 -0
  54. data/lib/esse/async_indexing/workers/sidekiq.rb +28 -0
  55. data/lib/esse/async_indexing/workers.rb +48 -0
  56. data/lib/esse/async_indexing.rb +72 -0
  57. data/lib/esse/plugins/async_indexing.rb +106 -0
  58. data/lib/esse-async-indexing.rb +3 -0
  59. data/lib/esse-async_indexing.rb +3 -0
  60. metadata +244 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a7931a646008cb93875d4e0fc47a552c2a19e3f9089c5e500e3cbb822f32f9bd
4
+ data.tar.gz: 374d59d804ca1b37e30f853b765d5b6ea0b975a97335d45249fa6e9f019d171c
5
+ SHA512:
6
+ metadata.gz: fb5ef232b141e0b2ee7f629f139376f4ef169da0b7078fdf6b5e74937219c096f065e5d0b982004ab0fe50b0b5363fd09dad62a1fab110f59d84d0e7f7072561
7
+ data.tar.gz: 7b09c24955e14df91435ef2a4ce39796c36ddb7ee0e9c173a40fbad0e7d11cf86c67b891454555f5814ebaf56994042a361de7c2fbd9ab093a7709a946aed0c2
data/.rubocop.yml ADDED
@@ -0,0 +1,35 @@
1
+ inherit_mode:
2
+ merge:
3
+ - Exclude
4
+
5
+ require:
6
+ - rubocop-performance
7
+ - rubocop-rspec
8
+ - standard/cop/block_single_line_braces
9
+
10
+ inherit_gem:
11
+ standard: config/base.yml
12
+
13
+ AllCops:
14
+ TargetRubyVersion: 2.5
15
+ SuggestExtensions: false
16
+ Exclude:
17
+ - "db/**/*"
18
+ - "tmp/**/*"
19
+ - "vendor/**/*"
20
+ NewCops: enable
21
+
22
+ RSpec/MultipleExpectations:
23
+ Enabled: false
24
+
25
+ RSpec/ExampleLength:
26
+ Enabled: false
27
+
28
+ Rspec/MultipleMemoizedHelpers:
29
+ Enabled: false
30
+
31
+ RSpec/MessageSpies:
32
+ Enabled: false
33
+
34
+ RSpec/StubbedMock:
35
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.0.2 - 2024-08-02
8
+ * Include sidekiq and faktory jobs to perform async indexing of documents
9
+ * Create Active Model async indexing callbacks
10
+ * many bug fixes and improvements
11
+
12
+ ## 0.0.1 - 2024-07-15
13
+ The first release of the esse-async_indexing plugin
14
+ * Added: Initial implementation of the plugin
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "esse"
6
+ gem "esse-rspec"
7
+ gem "esse-active_record", ">= 0.3.5", require: false
8
+ gem "timecop"
9
+ gem "faktory_worker_ruby", require: false
10
+ gem "opensearch-ruby", require: false
11
+
12
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,168 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ esse-async_indexing (0.0.2)
5
+ esse (>= 0.3.4)
6
+ esse-redis_storage (>= 0.1.0)
7
+ multi_json
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (7.1.3.4)
13
+ activesupport (= 7.1.3.4)
14
+ activerecord (7.1.3.4)
15
+ activemodel (= 7.1.3.4)
16
+ activesupport (= 7.1.3.4)
17
+ timeout (>= 0.4.0)
18
+ activesupport (7.1.3.4)
19
+ base64
20
+ bigdecimal
21
+ concurrent-ruby (~> 1.0, >= 1.0.2)
22
+ connection_pool (>= 2.2.5)
23
+ drb
24
+ i18n (>= 1.6, < 2)
25
+ minitest (>= 5.1)
26
+ mutex_m
27
+ tzinfo (~> 2.0)
28
+ addressable (2.8.7)
29
+ public_suffix (>= 2.0.2, < 7.0)
30
+ ast (2.4.2)
31
+ base64 (0.2.0)
32
+ bigdecimal (3.1.8)
33
+ coderay (1.1.3)
34
+ concurrent-ruby (1.3.3)
35
+ connection_pool (2.4.1)
36
+ crack (1.0.0)
37
+ bigdecimal
38
+ rexml
39
+ diff-lcs (1.5.1)
40
+ drb (2.2.1)
41
+ esse (0.3.4)
42
+ multi_json
43
+ thor (>= 0.19)
44
+ esse-active_record (0.3.5)
45
+ activerecord (>= 4.2, < 8)
46
+ esse (>= 0.3.0)
47
+ esse-redis_storage (0.1.0)
48
+ esse (>= 0.3.2)
49
+ multi_json (>= 1.0.0)
50
+ redis (>= 4.0.0)
51
+ esse-rspec (0.0.6)
52
+ esse (>= 0.2.4)
53
+ rspec (>= 3)
54
+ faktory_worker_ruby (2.0.0)
55
+ connection_pool (~> 2.2, >= 2.2.2)
56
+ faraday (2.8.1)
57
+ base64
58
+ faraday-net_http (>= 2.0, < 3.1)
59
+ ruby2_keywords (>= 0.0.4)
60
+ faraday-net_http (3.0.2)
61
+ hashdiff (1.1.0)
62
+ i18n (1.14.5)
63
+ concurrent-ruby (~> 1.0)
64
+ json (2.7.2)
65
+ language_server-protocol (3.17.0.3)
66
+ lint_roller (1.1.0)
67
+ method_source (1.1.0)
68
+ minitest (5.24.1)
69
+ multi_json (1.15.0)
70
+ mutex_m (0.2.0)
71
+ opensearch-ruby (3.4.0)
72
+ faraday (>= 1.0, < 3)
73
+ multi_json (>= 1.0)
74
+ parallel (1.25.1)
75
+ parser (3.3.4.0)
76
+ ast (~> 2.4.1)
77
+ racc
78
+ pry (0.14.2)
79
+ coderay (~> 1.1)
80
+ method_source (~> 1.0)
81
+ public_suffix (5.1.1)
82
+ racc (1.8.0)
83
+ rainbow (3.1.1)
84
+ redis (5.2.0)
85
+ redis-client (>= 0.22.0)
86
+ redis-client (0.22.2)
87
+ connection_pool
88
+ regexp_parser (2.9.2)
89
+ rexml (3.3.2)
90
+ strscan
91
+ rspec (3.13.0)
92
+ rspec-core (~> 3.13.0)
93
+ rspec-expectations (~> 3.13.0)
94
+ rspec-mocks (~> 3.13.0)
95
+ rspec-core (3.13.0)
96
+ rspec-support (~> 3.13.0)
97
+ rspec-expectations (3.13.1)
98
+ diff-lcs (>= 1.2.0, < 2.0)
99
+ rspec-support (~> 3.13.0)
100
+ rspec-mocks (3.13.1)
101
+ diff-lcs (>= 1.2.0, < 2.0)
102
+ rspec-support (~> 3.13.0)
103
+ rspec-support (3.13.1)
104
+ rubocop (1.64.1)
105
+ json (~> 2.3)
106
+ language_server-protocol (>= 3.17.0)
107
+ parallel (~> 1.10)
108
+ parser (>= 3.3.0.2)
109
+ rainbow (>= 2.2.2, < 4.0)
110
+ regexp_parser (>= 1.8, < 3.0)
111
+ rexml (>= 3.2.5, < 4.0)
112
+ rubocop-ast (>= 1.31.1, < 2.0)
113
+ ruby-progressbar (~> 1.7)
114
+ unicode-display_width (>= 2.4.0, < 3.0)
115
+ rubocop-ast (1.31.3)
116
+ parser (>= 3.3.1.0)
117
+ rubocop-performance (1.21.1)
118
+ rubocop (>= 1.48.1, < 2.0)
119
+ rubocop-ast (>= 1.31.1, < 2.0)
120
+ rubocop-rspec (3.0.3)
121
+ rubocop (~> 1.61)
122
+ ruby-progressbar (1.13.0)
123
+ ruby2_keywords (0.0.5)
124
+ standard (1.37.0)
125
+ language_server-protocol (~> 3.17.0.2)
126
+ lint_roller (~> 1.0)
127
+ rubocop (~> 1.64.0)
128
+ standard-custom (~> 1.0.0)
129
+ standard-performance (~> 1.4)
130
+ standard-custom (1.0.2)
131
+ lint_roller (~> 1.0)
132
+ rubocop (~> 1.50)
133
+ standard-performance (1.4.0)
134
+ lint_roller (~> 1.1)
135
+ rubocop-performance (~> 1.21.0)
136
+ strscan (3.1.0)
137
+ thor (1.3.1)
138
+ timecop (0.9.10)
139
+ timeout (0.4.1)
140
+ tzinfo (2.0.6)
141
+ concurrent-ruby (~> 1.0)
142
+ unicode-display_width (2.5.0)
143
+ webmock (3.23.1)
144
+ addressable (>= 2.8.0)
145
+ crack (>= 0.3.2)
146
+ hashdiff (>= 0.4.0, < 2.0.0)
147
+
148
+ PLATFORMS
149
+ x86_64-linux
150
+
151
+ DEPENDENCIES
152
+ esse
153
+ esse-active_record (>= 0.3.5)
154
+ esse-async_indexing!
155
+ esse-rspec
156
+ faktory_worker_ruby
157
+ opensearch-ruby
158
+ pry
159
+ rspec
160
+ rubocop
161
+ rubocop-performance
162
+ rubocop-rspec
163
+ standard
164
+ timecop
165
+ webmock
166
+
167
+ BUNDLED WITH
168
+ 2.3.22
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Marcos G. Zimmermann
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.
data/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # Esse Async Indexing
2
+
3
+ This gem provides a way to [Esse](https://github.com/marcosgz/esse) index documents asynchronously using [Faktory](https://github.com/contribsys/faktory_worker_ruby) or [Sidekiq](https://github.com/sidekiq/sidekiq).
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'esse-async_indexing'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ ```bash
17
+ $ bundle install
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ ```ruby
23
+ Esse.configure do |config|
24
+ config.redis = ConnectionPool.new(size: 10, timeout: 5) do
25
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://0.0.0.0:6379'))
26
+ end
27
+
28
+ # Setup Sidekiq
29
+ require 'sidekiq'
30
+ config.async_indexing.sidekiq do |sidekiq|
31
+ sidekiq.namespace = "sidekiq"
32
+ sidekiq.redis = ConnectionPool.new(size: 10, timeout: 5) do
33
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://0.0.0.0:6379'))
34
+ end
35
+ end
36
+
37
+ # Faktory
38
+ require 'faktory_worker_ruby'
39
+ config.async_indexing.faktory # No need to setup redis connection
40
+ end
41
+ ```
42
+
43
+ Optional worker configuration:
44
+
45
+ ```ruby
46
+ Esse.configure do |config|
47
+ config.async_indexing.sidekiq.workers = {
48
+ "Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob" => { queue: "indexing" },
49
+ "Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob" => { queue: "indexing" },
50
+ "Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob" => { queue: "indexing" },
51
+ "Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob" => { queue: "indexing" },
52
+ "Esse::AsyncIndexing::Jobs::ImportAllJob" => { queue: "batch_indexing", retry: 3 },
53
+ "Esse::AsyncIndexing::Jobs::ImportBatchIdJob" => { queue: "batch_indexing", retry: 3 },
54
+ "Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob" => { queue: "batch_indexing", retry: 3 },
55
+ "Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob" => { queue: "indexing" },
56
+ }
57
+ # or if you are using Faktory
58
+ config.async_indexing.faktory.workers = {
59
+ "Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob" => { queue: "indexing" },
60
+ "Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob" => { queue: "indexing" },
61
+ "Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob" => { queue: "indexing" },
62
+ "Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob" => { queue: "indexing" },
63
+ "Esse::AsyncIndexing::Jobs::ImportAllJob" => { queue: "batch_indexing", retry: 3 },
64
+ "Esse::AsyncIndexing::Jobs::ImportBatchIdJob" => { queue: "batch_indexing", retry: 3 },
65
+ "Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob" => { queue: "batch_indexing", retry: 3 },
66
+ "Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob" => { queue: "indexing" },
67
+ }
68
+ end
69
+ ```
70
+
71
+ ## Index Configuration
72
+
73
+ To enable async indexing for an index, you need to add the `:async_indexing` plugin to the index. And the index collection must implement the `#each_batch_ids` method that yields an array of document ids.
74
+
75
+ ```ruby
76
+ class GeosIndex < Esse::Index
77
+ plugin :async_indexing
78
+
79
+ repository :city do
80
+ collection Collections::CityCollection
81
+ document Documents::CityDocument
82
+ end
83
+ end
84
+
85
+ class GeosIndex::Collections::CityCollection < Esse::Collection
86
+ def each
87
+ # implement the each method as usual
88
+ end
89
+
90
+ def each_batch_ids
91
+ ::City.select(:id).except(:includes, :preload).find_in_batches(**batch_options) do |rows|
92
+ yield(rows.map(&:id))
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## CLI Commands
99
+
100
+ This gem includes the `async_import` command to import documents asynchronously.
101
+
102
+ ```bash
103
+ $ bundle exec esse index help async_import
104
+ $ bundle exec esse index async_import GeosIndex --suffix="20240101" --service="sidekiq" --repo="city"
105
+ ```
106
+
107
+
108
+ ## Workers/Jobs
109
+
110
+ The gem provides a few jobs to index, update, upsert and delete document or batch of documents with given ids. The sidekiq or faktory worker does not need to live in the same application that enqueues the job. The worker can be in a separate application that only runs the worker process. This gem has its own DSL to push jobs.
111
+
112
+ But for make sure to require the jobs in the worker application by calling `install!`
113
+
114
+ ```ruby
115
+ Esse::AsyncIndexing::Workers.install!(:faktory)
116
+ Esse::AsyncIndexing::Workers.install!(:sidekiq)
117
+ ```
118
+
119
+
120
+ ### Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob
121
+
122
+ Fetch a document from `GeosIndex.repo(:city)` collection using the given id and index it
123
+
124
+ ```ruby
125
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::DocumentIndexByIdJob", service: :sidekiq).with_args("GeosIndex", "city", city.id, suffix: "20240101")
126
+ .push
127
+ ```
128
+
129
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
130
+
131
+ ### Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob
132
+
133
+ Fetch a document from `GeosIndex.repo(:city)` collection using the given id and update it
134
+
135
+ ```ruby
136
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::DocumentUpdateByIdJob", service: :sidekiq).with_args("GeosIndex", "city", city.id, suffix: "20240101")
137
+ ```
138
+
139
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
140
+
141
+ ### Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob
142
+
143
+ Fetch a document from `GeosIndex.repo(:city)` collection using the given id and upsert it
144
+
145
+ ```ruby
146
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::DocumentUpsertByIdJob", service: :sidekiq).with_args("GeosIndex", "city", city.id, suffix: "20240101")
147
+ ```
148
+
149
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
150
+
151
+
152
+ ### Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob
153
+
154
+ Delete a document from the index using the given id
155
+
156
+ ```ruby
157
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::DocumentDeleteByIdJob", service: :sidekiq).with_args("GeosIndex", "city", city.id, suffix: "20240101")
158
+ ```
159
+
160
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
161
+
162
+ ### Esse::AsyncIndexing::Jobs::ImportAllJob
163
+
164
+ Import all documents from the `GeosIndex.repo(:city)` collection where `state_abbr` is "IL"
165
+
166
+ ```ruby
167
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::ImportAllJob", service: :sidekiq).with_args("GeosIndex", "city", context: {state_abbr: "IL"}, suffix: "20240101")
168
+ ```
169
+
170
+ **Note:** Suffix and import context are optional, just an example of how to pass additional arguments to the job.
171
+
172
+ ### Esse::AsyncIndexing::Jobs::ImportBatchIdJob
173
+
174
+ Import a batch of documents from the `GeosIndex.repo(:city)` collection using a batch_id generated by the [esse-redis_storage](https://github.com/marcosgz/esse-redis_storage) gem. This is the job that the `async_import` command uses.
175
+
176
+ ```ruby
177
+ batch_id = Esse::RedisStorage::Queue.for(repo: GeosIndex.repo(:city)).enqueue(values: big_list_of_uuids)
178
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::ImportBatchIdJob", service: :sidekiq).with_args("GeosIndex", "city", batch_id, suffix: "20240101")
179
+ ```
180
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
181
+
182
+ ### Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob
183
+
184
+ Update a lazy attribute of a document from the index using the given enqueued batch_id.
185
+
186
+ ```ruby
187
+ batch_id = Esse::RedisStorage::Queue.for(repo: GeosIndex.repo(:city), attribute_name: "total_schools").enqueue(values: big_list_of_uuids)
188
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::BulkUpdateLazyDocumentAttributeJob", service: :sidekiq).with_args("GeosIndex", "city", "total_schools", batch_id, suffix: "20240101")
189
+ ```
190
+
191
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
192
+
193
+ ### Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob
194
+
195
+ Update a lazy attribute of a document from the index using the given id
196
+
197
+ ```ruby
198
+ Esse::AsyncIndexing.worker("Esse::AsyncIndexing::Jobs::UpdateLazyDocumentAttributeJob", service: :sidekiq).with_args("GeosIndex", "city", "total_schools", [city.id], suffix: "20240101")
199
+ ```
200
+
201
+ **Note:** Suffix is optional, just an example of how to pass additional arguments to the job.
202
+
203
+ ### Custom Jobs
204
+
205
+ To implement a custom job :import, :index, :update or :delete documents, you need to define them using the `async_indexing_job` method in the index repository.
206
+
207
+ ```ruby
208
+ class GeosIndex < Esse::Index
209
+ plugin :async_indexing
210
+
211
+ repository :city do
212
+ collection Collections::CityCollection
213
+ document Documents::CityDocument
214
+ async_indexing_job(:import) do |service:, repo:, operation:, ids:, **kwargs|
215
+ GeosCityImportJob.perform_later(ids, **kwargs)
216
+ end
217
+ async_indexing_job(:index, :update, :delete) do |service:, repo:, operation:, id:, **kwargs|
218
+ GeosCityUpsertJob.perform_later(id, **kwargs)
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## Extras
225
+
226
+ You may want to use `async_indexing_callback` or `async_update_lazy_attribute_callback` callbacks along with the ActiveRecord models to automatically index, update, upsert or delete documents or attributes when the model is created, updated or destroyed.
227
+
228
+ This functionality require the [esse-active_record](https://github.com/marcosgz/esse-active_record) gem to be installed. Then require the `esse/asyn_indexing/active_record` file in the initializer.
229
+
230
+ ```ruby
231
+ require 'esse/async_indexing/active_record'
232
+ ```
233
+
234
+ Now you can use the `async_index_callback` or `async_update_lazy_attribute_callback` in the ActiveRecord models.
235
+
236
+ ```diff
237
+ class City < ApplicationRecord
238
+ - include Esse::ActiveRecord::Model
239
+ + include Esse::AsyncIndexing::ActiveRecord::Model
240
+
241
+ belongs_to :state, optional: true
242
+
243
+ - index_callback('geos_index:city') { id }
244
+ - update_lazy_attribute_callback('geos_index:state', 'cities_count', if: :state_id?) { state_id }
245
+ + async_index_callback('geos_index:city', service_name: :sidekiq) { id }
246
+ + async_update_lazy_attribute_callback('geos_index:state', 'cities_count', if: :state_id?, service_name: :sidekiq) { state_id }
247
+ end
248
+ ```
249
+
250
+ ## Development
251
+
252
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake none` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
253
+
254
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
255
+
256
+ ## Contributing
257
+
258
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcosgz/esse-async_indexing.
259
+
260
+ ## License
261
+
262
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
@@ -0,0 +1,11 @@
1
+ services:
2
+ redis:
3
+ image: redis
4
+ command: redis-server
5
+ ports:
6
+ - 6379:6379
7
+ faktory:
8
+ image: contribsys/faktory
9
+ ports:
10
+ - 7419:7419
11
+ - 7420:7420
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class BatchDelete
5
+ def self.call(index_class_name, repo_name, ids, options = {})
6
+ docs = Esse::LazyDocumentHeader.coerce_each(ids)
7
+ return if docs.empty?
8
+
9
+ index_class, _repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
10
+ bulk_opts = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
11
+ index_class.bulk(**bulk_opts, delete: docs.map(&:to_doc))
12
+ docs.size
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class BatchImport
5
+ def self.call(index_class_name, repo_name, ids, options = {})
6
+ ids = Array(ids)
7
+ return if ids.empty?
8
+
9
+ _index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
10
+ kwargs = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
11
+ kwargs[:context] ||= {}
12
+ kwargs[:context][:id] = ids
13
+ repo_class.import(**kwargs)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class BatchImportAll
5
+ def self.call(index_class_name, repo_name, options = {})
6
+ _index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
7
+ kwargs = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
8
+ repo_class.import(**kwargs)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class BatchUpdate
5
+ DOC_ARGS = %i[lazy_attributes context]
6
+
7
+ def self.call(index_class_name, repo_name, ids, options = {})
8
+ ids = Array(ids)
9
+ return if ids.empty?
10
+
11
+ index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
12
+ bulk_opts = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
13
+ find_opts = {}
14
+ if (context = bulk_opts.delete(:context))
15
+ find_opts.merge!(context)
16
+ end
17
+ if (lazy_attributes = bulk_opts.delete(:lazy_attributes))
18
+ find_opts[:lazy_attributes] = lazy_attributes
19
+ end
20
+ find_opts[:id] = ids
21
+
22
+ count = 0
23
+ repo_class.each_serialized_batch(**find_opts) do |batch|
24
+ index_class.cluster.may_update_type!(bulk_opts)
25
+ index_class.bulk(**bulk_opts, update: batch)
26
+ count += batch.size
27
+ end
28
+
29
+ count
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class BulkUpdateLazyDocumentAttribute
5
+ def self.call(index_class_name, repo_name, attr_name, batch_id, options = {})
6
+ _index_class, repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
7
+ queue = Esse::RedisStorage::Queue.for(repo: repo_class, attribute_name: attr_name)
8
+
9
+ kwargs = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
10
+
11
+ attr_name = repo_class.lazy_document_attributes.keys.find { |key| key.to_s == attr_name.to_s }
12
+ updated_ids = []
13
+ queue.fetch(batch_id) do |ids|
14
+ updated_ids = ids
15
+ repo_class.update_documents_attribute(attr_name, ids, **kwargs)
16
+ end
17
+ updated_ids
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Esse::AsyncIndexing::Actions::CoerceIndexRepository
4
+ def self.call(index_class_name, repo_name)
5
+ index_class = begin
6
+ Object.const_get(index_class_name)
7
+ rescue NameError
8
+ raise(ArgumentError, "Index class #{index_class_name} not found")
9
+ end
10
+
11
+ repo_class = index_class.repo_hash[repo_name.to_s]
12
+ repo_class ||= if repo_name.include?("::")
13
+ index_class.repo_hash.map { |_, v| [v.to_s, v] }.to_h[repo_name]
14
+ end
15
+ if repo_class.nil?
16
+ raise ArgumentError, <<~MSG
17
+ No repo named "#{repo_name}" found in #{index_class_name}. Use the `repository` method to define one:
18
+
19
+ repository :#{repo_name} do
20
+ # collection ...
21
+ # document ...
22
+ end
23
+ MSG
24
+ end
25
+
26
+ [index_class, repo_class]
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse::AsyncIndexing::Actions
4
+ class DeleteDocument
5
+ DOC_ARGS = %i[lazy_attributes]
6
+
7
+ def self.call(index_class_name, repo_name, document_id, options = {})
8
+ index_class, _repo_class = CoerceIndexRepository.call(index_class_name, repo_name)
9
+ bulk_opts = Esse::HashUtils.deep_transform_keys(options, &:to_sym)
10
+ bulk_opts.delete_if { |k, _| DOC_ARGS.include?(k) }
11
+
12
+ index_class.delete(id: document_id, **bulk_opts)
13
+ :deleted
14
+ end
15
+ end
16
+ end