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