searchkick 2.0.3 → 2.0.4

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: 63b182684f14e8e5c0b30e0505a188d7bbc06f85
4
- data.tar.gz: 9227d3877f3b6cf944a3413fe4478ec17393bc0c
3
+ metadata.gz: 275674a7d9ef11e2faba8426a3a85172d84cf16c
4
+ data.tar.gz: 3134c16ed33df803187e4b8b75dd3f8167745e62
5
5
  SHA512:
6
- metadata.gz: '08f71c79a49fa5c0c43d4d1d489b0040bfec789edd6c3e3737b48fd749a7dc1ee4d7b2337aa0e57db35a29a20c14ce13c5a0b653441151f64f2241b59f69a4f5'
7
- data.tar.gz: 9f864b9b4d890a1a220364abd8e6d0376d06e90d853d34201630f3599bc580a5fee3cf41604b79cbed7bf25ee5524352056119cd9c4a8b0883017476639523da
6
+ metadata.gz: bf4ab3f4fb5557051f9e5bbc25fd9aaf26f3c5403e473e86f6108f5418a53cc3ee391ec0cf204cc717ca0df7f96ba5e83deeb42bb725bc6025f6258c067d9c7d
7
+ data.tar.gz: dba920c1264cf007eb5a4759c83f3bf0269a8b36bf381b4e4f6c8b23089abfb1a90c373a3ae6853f4145bae751b1181f7bcd77fb06cd839cd45c8e350c0f22ea
data/.travis.yml CHANGED
@@ -3,6 +3,7 @@ language: ruby
3
3
  rvm: 2.3.1
4
4
  services:
5
5
  - mongodb
6
+ - redis-server
6
7
  before_install:
7
8
  - ./test/ci/before_install.sh
8
9
  script: RUBYOPT=W0 bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 2.0.4
2
+
3
+ - Added support for queuing updates [experimental]
4
+ - Added `refresh_interval` option to `reindex`
5
+ - Prefer `search_index` over `searchkick_index`
6
+
1
7
  ## 2.0.3
2
8
 
3
9
  - Added `async` option to `reindex` [experimental]
data/Gemfile CHANGED
@@ -8,3 +8,4 @@ gem "activerecord", "~> 5.0.0"
8
8
  gem "gemoji-parser"
9
9
  gem "typhoeus"
10
10
  gem "activejob"
11
+ gem "redis"
data/README.md CHANGED
@@ -1185,6 +1185,34 @@ And use:
1185
1185
  Searchkick.reindex_status(index_name)
1186
1186
  ```
1187
1187
 
1188
+ ### Queues [master, experimental]
1189
+
1190
+ You can also queue updates and do them in bulk for better performance. First, set up Redis in an initializer.
1191
+
1192
+ ```ruby
1193
+ Searchkick.redis = Redis.new
1194
+ ```
1195
+
1196
+ And ask your models to queue updates.
1197
+
1198
+ ```ruby
1199
+ class Product < ActiveRecord::Base
1200
+ searchkick callbacks: :queue
1201
+ end
1202
+ ```
1203
+
1204
+ Then, set up a background job to run.
1205
+
1206
+ ```ruby
1207
+ Searchkick::ProcessQueueJob.perform_later(class_name: "Product")
1208
+ ```
1209
+
1210
+ You can check the queue length with:
1211
+
1212
+ ```ruby
1213
+ Product.searchkick_index.reindex_queue.length
1214
+ ```
1215
+
1188
1216
  For more tips, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/blog/found-keeping-elasticsearch-in-sync).
1189
1217
 
1190
1218
  ## Advanced
data/lib/searchkick.rb CHANGED
@@ -5,6 +5,7 @@ require "searchkick/version"
5
5
  require "searchkick/index_options"
6
6
  require "searchkick/index"
7
7
  require "searchkick/indexer"
8
+ require "searchkick/reindex_queue"
8
9
  require "searchkick/results"
9
10
  require "searchkick/query"
10
11
  require "searchkick/model"
@@ -21,6 +22,8 @@ rescue LoadError
21
22
  end
22
23
  if defined?(ActiveJob)
23
24
  require "searchkick/bulk_reindex_job"
25
+ require "searchkick/process_queue_job"
26
+ require "searchkick/process_batch_job"
24
27
  require "searchkick/reindex_v2_job"
25
28
  end
26
29
 
@@ -142,6 +145,25 @@ module Searchkick
142
145
  end
143
146
  end
144
147
 
148
+ # private
149
+ def self.load_records(records, ids)
150
+ records =
151
+ if records.respond_to?(:primary_key)
152
+ # ActiveRecord
153
+ records.where(records.primary_key => ids) if records.primary_key
154
+ elsif records.respond_to?(:queryable)
155
+ # Mongoid 3+
156
+ records.queryable.for_ids(ids)
157
+ elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
158
+ # Nobrainer
159
+ records.unscoped.where(:id.in => ids)
160
+ end
161
+
162
+ raise Searchkick::Error, "Not sure how to load records" if !records
163
+
164
+ records
165
+ end
166
+
145
167
  # private
146
168
  def self.indexer
147
169
  Thread.current[:searchkick_indexer] ||= Searchkick::Indexer.new
@@ -6,7 +6,7 @@ module Searchkick
6
6
  klass = class_name.constantize
7
7
  index = index_name ? Searchkick::Index.new(index_name) : klass.searchkick_index
8
8
  record_ids ||= min_id..max_id
9
- index.import_scope(klass.where(klass.primary_key => record_ids), method_name: method_name, batch: true, batch_id: batch_id)
9
+ index.import_scope(Searchkick.load_records(klass, record_ids), method_name: method_name, batch: true, batch_id: batch_id)
10
10
  end
11
11
  end
12
12
  end
@@ -38,11 +38,22 @@ module Searchkick
38
38
  client.indices.get_settings index: name
39
39
  end
40
40
 
41
+ def refresh_interval
42
+ settings.values.first["settings"]["index"]["refresh_interval"]
43
+ end
44
+
41
45
  def update_settings(settings)
42
46
  client.indices.put_settings index: name, body: settings
43
47
  end
44
48
 
45
- def promote(new_name)
49
+ def promote(new_name, update_refresh_interval: false)
50
+ if update_refresh_interval
51
+ new_index = Searchkick::Index.new(new_name)
52
+ settings = options[:settings] || {}
53
+ refresh_interval = (settings[:index] && settings[:index][:refresh_interval]) || "1s"
54
+ new_index.update_settings(index: {refresh_interval: refresh_interval})
55
+ end
56
+
46
57
  old_indices =
47
58
  begin
48
59
  client.indices.get_alias(name: name).keys
@@ -135,6 +146,12 @@ module Searchkick
135
146
  search_model(record.class, like_text, options)
136
147
  end
137
148
 
149
+ # queue
150
+
151
+ def reindex_queue
152
+ Searchkick::ReindexQueue.new(name)
153
+ end
154
+
138
155
  # search
139
156
 
140
157
  def search_model(searchkick_klass, term = "*", **options, &block)
@@ -191,7 +208,7 @@ module Searchkick
191
208
 
192
209
  # https://gist.github.com/jarosan/3124884
193
210
  # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
194
- def reindex_scope(scope, import: true, resume: false, retain: false, async: false)
211
+ def reindex_scope(scope, import: true, resume: false, retain: false, async: false, refresh_interval: nil)
195
212
  if resume
196
213
  index_name = all_indices.sort.last
197
214
  raise Searchkick::Error, "No index to resume" unless index_name
@@ -199,7 +216,9 @@ module Searchkick
199
216
  else
200
217
  clean_indices unless retain
201
218
 
202
- index = create_index(index_options: scope.searchkick_index_options)
219
+ index_options = scope.searchkick_index_options
220
+ index_options.deep_merge!(settings: {index: {refresh_interval: refresh_interval}}) if refresh_interval
221
+ index = create_index(index_options: index_options)
203
222
  end
204
223
 
205
224
  # check if alias exists
@@ -209,12 +228,12 @@ module Searchkick
209
228
 
210
229
  # get existing indices to remove
211
230
  unless async
212
- promote(index.name)
231
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
213
232
  clean_indices unless retain
214
233
  end
215
234
  else
216
235
  delete if exists?
217
- promote(index.name)
236
+ promote(index.name, update_refresh_interval: !refresh_interval.nil?)
218
237
 
219
238
  # import after promotion
220
239
  index.import_scope(scope, resume: resume, async: async, full: true) if import
@@ -238,23 +257,27 @@ module Searchkick
238
257
  import_or_update scope.to_a, method_name, async
239
258
  Searchkick.redis.srem(batches_key, batch_id) if batch_id && Searchkick.redis
240
259
  elsif full && async
241
- # TODO expire Redis key
242
- primary_key = scope.primary_key
243
- starting_id = scope.minimum(primary_key)
244
- max_id = scope.maximum(primary_key)
245
- batches_count = ((max_id - starting_id + 1) / batch_size.to_f).ceil
246
-
247
- batches_count.times do |i|
248
- batch_id = i + 1
249
- min_id = starting_id + (i * batch_size)
250
- Searchkick::BulkReindexJob.perform_later(
251
- class_name: scope.model_name.name,
252
- min_id: min_id,
253
- max_id: min_id + batch_size - 1,
254
- index_name: name,
255
- batch_id: batch_id
256
- )
257
- Searchkick.redis.sadd(batches_key, batch_id) if Searchkick.redis
260
+ if scope.respond_to?(:primary_key)
261
+ # TODO expire Redis key
262
+ primary_key = scope.primary_key
263
+ starting_id = scope.minimum(primary_key) || 0
264
+ max_id = scope.maximum(primary_key) || 0
265
+ batches_count = ((max_id - starting_id + 1) / batch_size.to_f).ceil
266
+
267
+ batches_count.times do |i|
268
+ batch_id = i + 1
269
+ min_id = starting_id + (i * batch_size)
270
+ Searchkick::BulkReindexJob.perform_later(
271
+ class_name: scope.model_name.name,
272
+ min_id: min_id,
273
+ max_id: min_id + batch_size - 1,
274
+ index_name: name,
275
+ batch_id: batch_id
276
+ )
277
+ Searchkick.redis.sadd(batches_key, batch_id) if Searchkick.redis
278
+ end
279
+ else
280
+ raise Searchkick::Error, "async option only supported for ActiveRecord"
258
281
  end
259
282
  elsif scope.respond_to?(:find_in_batches)
260
283
  if resume
@@ -35,6 +35,7 @@ module Searchkick
35
35
  index = index.call if index.respond_to? :call
36
36
  Searchkick::Index.new(index, searchkick_options)
37
37
  end
38
+ alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
38
39
 
39
40
  def enable_search_callbacks
40
41
  class_variable_set :@@searchkick_callbacks, true
@@ -84,11 +85,36 @@ module Searchkick
84
85
  after_destroy callback_name, if: proc { self.class.search_callbacks? }
85
86
  end
86
87
 
87
- def reindex(method_name = nil, refresh: false, async: false)
88
- if async
88
+ def reindex(method_name = nil, refresh: false, async: false, mode: nil)
89
+ klass_options = self.class.searchkick_index.options
90
+
91
+ if mode.nil?
92
+ mode =
93
+ if async
94
+ :async
95
+ elsif Searchkick.callbacks_value
96
+ Searchkick.callbacks_value
97
+ elsif klass_options.key?(:callbacks) && klass_options[:callbacks] != :async
98
+ # TODO remove 2nd condition in next major version
99
+ klass_options[:callbacks]
100
+ end
101
+ end
102
+
103
+ case mode
104
+ when :queue
105
+ if method_name
106
+ raise Searchkick::Error, "Partial reindex not supported with queue option"
107
+ else
108
+ self.class.searchkick_index.reindex_queue.push(id.to_s)
109
+ end
110
+ when :async
89
111
  if method_name
90
112
  # TODO support Mongoid and NoBrainer and non-id primary keys
91
- Searchkick::BulkReindexJob.perform_later(class_name: self.class.name, record_ids: [id.to_s], method_name: method_name ? method_name.to_s : nil)
113
+ Searchkick::BulkReindexJob.perform_later(
114
+ class_name: self.class.name,
115
+ record_ids: [id.to_s],
116
+ method_name: method_name ? method_name.to_s : nil
117
+ )
92
118
  else
93
119
  self.class.searchkick_index.reindex_record_async(self)
94
120
  end
@@ -0,0 +1,23 @@
1
+ module Searchkick
2
+ class ProcessBatchJob < ActiveJob::Base
3
+ queue_as :searchkick
4
+
5
+ def perform(class_name:, record_ids:)
6
+ klass = class_name.constantize
7
+ scope = Searchkick.load_records(klass, record_ids)
8
+ scope = scope.search_import if scope.respond_to?(:search_import)
9
+ records = scope.select(&:should_index?)
10
+
11
+ # determine which records to delete
12
+ delete_ids = record_ids - records.map { |r| r.id.to_s }
13
+ delete_records = delete_ids.map { |id| m = klass.new; m.id = id; m }
14
+
15
+ # bulk reindex
16
+ index = klass.searchkick_index
17
+ Searchkick.callbacks(:bulk) do
18
+ index.bulk_index(records)
19
+ index.bulk_delete(delete_records)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Searchkick
2
+ class ProcessQueueJob < ActiveJob::Base
3
+ queue_as :searchkick
4
+
5
+ def perform(class_name:)
6
+ model = class_name.constantize
7
+
8
+ limit = 1000
9
+ record_ids = Searchkick::ReindexQueue.new(model.searchkick_index.name).reserve(limit: limit)
10
+ if record_ids.any?
11
+ Searchkick::ProcessBatchJob.perform_later(
12
+ class_name: model.name,
13
+ record_ids: record_ids
14
+ )
15
+ # TODO when moving to reliable queuing, mark as complete
16
+
17
+ if record_ids.size == limit
18
+ Searchkick::ProcessQueueJob.perform_later(class_name: class_name)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module Searchkick
2
+ class ReindexQueue
3
+ attr_reader :name
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def push(record_id)
10
+ redis.lpush(redis_key, record_id)
11
+ end
12
+
13
+ # TODO use reliable queuing
14
+ def reserve(limit: 1000)
15
+ record_ids = Set.new
16
+ while record_ids.size < limit && record_id = redis.rpop(redis_key)
17
+ record_ids << record_id
18
+ end
19
+ record_ids.to_a
20
+ end
21
+
22
+ def clear
23
+ redis.del(redis_key)
24
+ end
25
+
26
+ def length
27
+ redis.llen(redis_key)
28
+ end
29
+
30
+ private
31
+
32
+ def redis
33
+ Searchkick.redis
34
+ end
35
+
36
+ def redis_key
37
+ "searchkick:reindex_queue:#{name}"
38
+ end
39
+ end
40
+ end
@@ -206,21 +206,7 @@ module Searchkick
206
206
  end
207
207
  end
208
208
 
209
- records =
210
- if records.respond_to?(:primary_key)
211
- # ActiveRecord
212
- records.where(records.primary_key => ids) if records.primary_key
213
- elsif records.respond_to?(:queryable)
214
- # Mongoid 3+
215
- records.queryable.for_ids(ids)
216
- elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
217
- # Nobrainer
218
- records.unscoped.where(:id.in => ids)
219
- end
220
-
221
- raise Searchkick::Error, "Not sure how to load records" if !records
222
-
223
- records
209
+ Searchkick.load_records(records, ids)
224
210
  end
225
211
 
226
212
  def base_field(k)
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "2.0.3"
2
+ VERSION = "2.0.4"
3
3
  end
@@ -24,4 +24,36 @@ class CallbacksTest < Minitest::Test
24
24
  Product.searchkick_index.refresh
25
25
  assert_search "product", ["Product A", "Product B"]
26
26
  end
27
+
28
+ def test_queue
29
+ skip unless defined?(ActiveJob) && defined?(Redis)
30
+
31
+ reindex_queue = Product.searchkick_index.reindex_queue
32
+ reindex_queue.clear
33
+
34
+ Searchkick.callbacks(:queue) do
35
+ store_names ["Product A", "Product B"]
36
+ end
37
+ Product.searchkick_index.refresh
38
+ assert_search "product", [], load: false
39
+ assert_equal 2, reindex_queue.length
40
+
41
+ Searchkick::ProcessQueueJob.perform_later(class_name: "Product")
42
+ Product.searchkick_index.refresh
43
+ assert_search "product", ["Product A", "Product B"], load: false
44
+ assert_equal 0, reindex_queue.length
45
+
46
+ Searchkick.callbacks(:queue) do
47
+ Product.where(name: "Product B").destroy_all
48
+ Product.create!(name: "Product C")
49
+ end
50
+ Product.searchkick_index.refresh
51
+ assert_search "product", ["Product A", "Product B"], load: false
52
+ assert_equal 2, reindex_queue.length
53
+
54
+ Searchkick::ProcessQueueJob.perform_later(class_name: "Product")
55
+ Product.searchkick_index.refresh
56
+ assert_search "product", ["Product A", "Product C"], load: false
57
+ assert_equal 0, reindex_queue.length
58
+ end
27
59
  end
@@ -5,3 +5,4 @@ gemspec path: "../../"
5
5
 
6
6
  gem "mongoid", "~> 6.0.0"
7
7
  gem "activejob"
8
+ gem "redis"
@@ -5,3 +5,4 @@ gemspec path: "../../"
5
5
 
6
6
  gem "nobrainer", ">= 0.21.0"
7
7
  gem "activejob"
8
+ gem "redis"
data/test/reindex_test.rb CHANGED
@@ -39,4 +39,17 @@ class ReindexTest < Minitest::Test
39
39
  Product.searchkick_index.promote(reindex[:index_name])
40
40
  assert_search "product", ["Product A"]
41
41
  end
42
+
43
+ def test_refresh_interval
44
+ reindex = Product.reindex(refresh_interval: "30s", async: true, import: false)
45
+ index = Searchkick::Index.new(reindex[:index_name])
46
+ assert_nil Product.search_index.refresh_interval
47
+ assert_equal "30s", index.refresh_interval
48
+
49
+ Product.search_index.promote(index.name, update_refresh_interval: true)
50
+ assert_equal "1s", index.refresh_interval
51
+ assert_equal "1s", Product.search_index.refresh_interval
52
+ ensure
53
+ Product.reindex
54
+ end
42
55
  end
data/test/test_helper.rb CHANGED
@@ -14,6 +14,8 @@ File.delete("elasticsearch.log") if File.exist?("elasticsearch.log")
14
14
  Searchkick.client.transport.logger = Logger.new("elasticsearch.log")
15
15
  Searchkick.search_timeout = 5
16
16
 
17
+ Searchkick.redis = Redis.new if defined?(Redis)
18
+
17
19
  puts "Running against Elasticsearch #{Searchkick.server_version}"
18
20
 
19
21
  I18n.config.enforce_available_locales = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.3
4
+ version: 2.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-13 00:00:00.000000000 Z
11
+ date: 2017-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -118,7 +118,10 @@ files:
118
118
  - lib/searchkick/logging.rb
119
119
  - lib/searchkick/middleware.rb
120
120
  - lib/searchkick/model.rb
121
+ - lib/searchkick/process_batch_job.rb
122
+ - lib/searchkick/process_queue_job.rb
121
123
  - lib/searchkick/query.rb
124
+ - lib/searchkick/reindex_queue.rb
122
125
  - lib/searchkick/reindex_v2_job.rb
123
126
  - lib/searchkick/results.rb
124
127
  - lib/searchkick/tasks.rb