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 +4 -4
- data/.travis.yml +1 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/README.md +28 -0
- data/lib/searchkick.rb +22 -0
- data/lib/searchkick/bulk_reindex_job.rb +1 -1
- data/lib/searchkick/index.rb +45 -22
- data/lib/searchkick/model.rb +29 -3
- data/lib/searchkick/process_batch_job.rb +23 -0
- data/lib/searchkick/process_queue_job.rb +23 -0
- data/lib/searchkick/reindex_queue.rb +40 -0
- data/lib/searchkick/results.rb +1 -15
- data/lib/searchkick/version.rb +1 -1
- data/test/callbacks_test.rb +32 -0
- data/test/gemfiles/mongoid6.gemfile +1 -0
- data/test/gemfiles/nobrainer.gemfile +1 -0
- data/test/reindex_test.rb +13 -0
- data/test/test_helper.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 275674a7d9ef11e2faba8426a3a85172d84cf16c
|
4
|
+
data.tar.gz: 3134c16ed33df803187e4b8b75dd3f8167745e62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf4ab3f4fb5557051f9e5bbc25fd9aaf26f3c5403e473e86f6108f5418a53cc3ee391ec0cf204cc717ca0df7f96ba5e83deeb42bb725bc6025f6258c067d9c7d
|
7
|
+
data.tar.gz: dba920c1264cf007eb5a4759c83f3bf0269a8b36bf381b4e4f6c8b23089abfb1a90c373a3ae6853f4145bae751b1181f7bcd77fb06cd839cd45c8e350c0f22ea
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
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(
|
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
|
data/lib/searchkick/index.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
data/lib/searchkick/model.rb
CHANGED
@@ -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
|
-
|
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(
|
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
|
data/lib/searchkick/results.rb
CHANGED
@@ -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)
|
data/lib/searchkick/version.rb
CHANGED
data/test/callbacks_test.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|