searchkick 2.0.3 → 2.0.4

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: 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