searchkick 2.3.1 → 2.3.2
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 +5 -1
- data/CHANGELOG.md +12 -0
- data/README.md +12 -0
- data/lib/searchkick.rb +6 -9
- data/lib/searchkick/index.rb +37 -6
- data/lib/searchkick/index_options.rb +24 -11
- data/lib/searchkick/logging.rb +1 -1
- data/lib/searchkick/model.rb +2 -2
- data/lib/searchkick/multi_search.rb +42 -0
- data/lib/searchkick/query.rb +40 -16
- data/lib/searchkick/results.rb +17 -4
- data/lib/searchkick/version.rb +1 -1
- data/test/boost_test.rb +15 -0
- data/test/geo_shape_test.rb +4 -1
- data/test/highlight_test.rb +1 -1
- data/test/index_test.rb +9 -1
- data/test/multi_search_test.rb +14 -0
- data/test/routing_test.rb +2 -2
- data/test/sql_test.rb +16 -0
- data/test/suggest_test.rb +1 -1
- data/test/test_helper.rb +8 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f081bd9d81b729d9bd3610154f95aeb355684f8
|
4
|
+
data.tar.gz: 88702f0b0417396e58697bd86919b92f716d470b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bbebeb45ba1cff82280c6d0eab7f25ac42dbc6f22b600a23f583b4da36fa6e42a4ebab6d9e149087192ca765dd75aadd5eaa4fb3b5add5e8ce3613ff8f91a85f
|
7
|
+
data.tar.gz: d643fbc24e36968f2820b92875a28fbf215e68d752717d4d2fa8a8fcd7efa32009f03a4c8a983d88666ab7c18e2602e58895f6c36e4934b651c0a838ae4f45ab
|
data/.travis.yml
CHANGED
@@ -20,7 +20,7 @@ gemfile:
|
|
20
20
|
- test/gemfiles/mongoid5.gemfile
|
21
21
|
- test/gemfiles/mongoid6.gemfile
|
22
22
|
env:
|
23
|
-
- ELASTICSEARCH_VERSION=5.
|
23
|
+
- ELASTICSEARCH_VERSION=5.5.0
|
24
24
|
jdk: oraclejdk8
|
25
25
|
matrix:
|
26
26
|
include:
|
@@ -33,3 +33,7 @@ matrix:
|
|
33
33
|
- gemfile: Gemfile
|
34
34
|
env: ELASTICSEARCH_VERSION=5.0.1
|
35
35
|
jdk: oraclejdk8
|
36
|
+
allow_failures:
|
37
|
+
- gemfile: Gemfile
|
38
|
+
env: ELASTICSEARCH_VERSION=6.0.0-beta1
|
39
|
+
jdk: oraclejdk8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 2.3.2
|
2
|
+
|
3
|
+
- Added `_all` and `default_fields` options
|
4
|
+
- Added global `index_prefix` option
|
5
|
+
- Added `wait` option to async reindex
|
6
|
+
- Added `model_includes` option
|
7
|
+
- Added `missing` option for `boost_by`
|
8
|
+
- Raise error for `reindex_status` when Redis not configured
|
9
|
+
- Warn when incompatible options used with `body` option
|
10
|
+
- Fixed bug where `routing` and `type` options were silently ignored with `body` option
|
11
|
+
- Fixed `reindex(async: true)` for non-numeric primary keys in Postgres
|
12
|
+
|
1
13
|
## 2.3.1
|
2
14
|
|
3
15
|
- Added support for `reindex(async: true)` for non-numeric primary keys
|
data/README.md
CHANGED
@@ -1214,6 +1214,12 @@ And use:
|
|
1214
1214
|
Searchkick.reindex_status(index_name)
|
1215
1215
|
```
|
1216
1216
|
|
1217
|
+
You can also have Searchkick wait for reindexing to complete [master]
|
1218
|
+
|
1219
|
+
```ruby
|
1220
|
+
Searchkick.reindex(async: {wait: true})
|
1221
|
+
```
|
1222
|
+
|
1217
1223
|
You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
|
1218
1224
|
|
1219
1225
|
```ruby
|
@@ -1630,6 +1636,12 @@ Eager load associations
|
|
1630
1636
|
Product.search "milk", includes: [:brand, :stores]
|
1631
1637
|
```
|
1632
1638
|
|
1639
|
+
Eager load different associations by model [master]
|
1640
|
+
|
1641
|
+
```ruby
|
1642
|
+
Searchkick.search("*", index_name: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
|
1643
|
+
```
|
1644
|
+
|
1633
1645
|
Turn off special characters
|
1634
1646
|
|
1635
1647
|
```ruby
|
data/lib/searchkick.rb
CHANGED
@@ -8,6 +8,7 @@ require "searchkick/indexer"
|
|
8
8
|
require "searchkick/reindex_queue"
|
9
9
|
require "searchkick/results"
|
10
10
|
require "searchkick/query"
|
11
|
+
require "searchkick/multi_search"
|
11
12
|
require "searchkick/model"
|
12
13
|
require "searchkick/tasks"
|
13
14
|
require "searchkick/middleware"
|
@@ -36,7 +37,7 @@ module Searchkick
|
|
36
37
|
class ImportError < Error; end
|
37
38
|
|
38
39
|
class << self
|
39
|
-
attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_suffix, :queue_name
|
40
|
+
attr_accessor :search_method_name, :wordnet_path, :timeout, :models, :client_options, :redis, :index_prefix, :index_suffix, :queue_name
|
40
41
|
attr_writer :client, :env, :search_timeout
|
41
42
|
attr_reader :aws_credentials
|
42
43
|
end
|
@@ -101,14 +102,8 @@ module Searchkick
|
|
101
102
|
end
|
102
103
|
end
|
103
104
|
|
104
|
-
def self.multi_search(queries)
|
105
|
-
|
106
|
-
responses = client.msearch(body: queries.flat_map { |q| [q.params.except(:body), q.body] })["responses"]
|
107
|
-
queries.each_with_index do |query, i|
|
108
|
-
query.handle_response(responses[i])
|
109
|
-
end
|
110
|
-
end
|
111
|
-
queries
|
105
|
+
def self.multi_search(queries, retry_misspellings: false)
|
106
|
+
Searchkick::MultiSearch.new(queries, retry_misspellings: retry_misspellings).perform
|
112
107
|
end
|
113
108
|
|
114
109
|
# callbacks
|
@@ -153,6 +148,8 @@ module Searchkick
|
|
153
148
|
completed: batches_left == 0,
|
154
149
|
batches_left: batches_left
|
155
150
|
}
|
151
|
+
else
|
152
|
+
raise Searchkick::Error, "Redis not configured"
|
156
153
|
end
|
157
154
|
end
|
158
155
|
|
data/lib/searchkick/index.rb
CHANGED
@@ -15,7 +15,13 @@ module Searchkick
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def delete
|
18
|
-
|
18
|
+
if !Searchkick.server_below?("6.0.0-alpha1") && alias_exists?
|
19
|
+
# can't call delete directly on aliases in ES 6
|
20
|
+
indices = client.indices.get_alias(name: name).keys
|
21
|
+
client.indices.delete index: indices
|
22
|
+
else
|
23
|
+
client.indices.delete index: name
|
24
|
+
end
|
19
25
|
end
|
20
26
|
|
21
27
|
def exists?
|
@@ -228,7 +234,8 @@ module Searchkick
|
|
228
234
|
end
|
229
235
|
|
230
236
|
# check if alias exists
|
231
|
-
|
237
|
+
alias_exists = alias_exists?
|
238
|
+
if alias_exists
|
232
239
|
# import before promotion
|
233
240
|
index.import_scope(scope, resume: resume, async: async, full: true) if import
|
234
241
|
|
@@ -246,6 +253,24 @@ module Searchkick
|
|
246
253
|
end
|
247
254
|
|
248
255
|
if async
|
256
|
+
if async.is_a?(Hash) && async[:wait]
|
257
|
+
puts "Created index: #{index.name}"
|
258
|
+
puts "Jobs queued. Waiting..."
|
259
|
+
loop do
|
260
|
+
sleep 3
|
261
|
+
status = Searchkick.reindex_status(index.name)
|
262
|
+
break if status[:completed]
|
263
|
+
puts "Batches left: #{status[:batches_left]}"
|
264
|
+
end
|
265
|
+
# already promoted if alias didn't exist
|
266
|
+
if alias_exists
|
267
|
+
puts "Jobs complete. Promoting..."
|
268
|
+
promote(index.name, update_refresh_interval: !refresh_interval.nil?)
|
269
|
+
end
|
270
|
+
clean_indices unless retain
|
271
|
+
puts "SUCCESS!"
|
272
|
+
end
|
273
|
+
|
249
274
|
{index_name: index.name}
|
250
275
|
else
|
251
276
|
index.refresh
|
@@ -273,8 +298,8 @@ module Searchkick
|
|
273
298
|
|
274
299
|
scope = scope.select("id").except(:includes, :preload) if async
|
275
300
|
|
276
|
-
scope.find_in_batches batch_size: batch_size do |
|
277
|
-
import_or_update
|
301
|
+
scope.find_in_batches batch_size: batch_size do |items|
|
302
|
+
import_or_update items, method_name, async
|
278
303
|
end
|
279
304
|
else
|
280
305
|
each_batch(scope) do |items|
|
@@ -290,7 +315,7 @@ module Searchkick
|
|
290
315
|
# other
|
291
316
|
|
292
317
|
def tokens(text, options = {})
|
293
|
-
client.indices.analyze({text: text, index: name
|
318
|
+
client.indices.analyze(body: {text: text}.merge(options), index: name)["tokens"].map { |t| t["token"] }
|
294
319
|
end
|
295
320
|
|
296
321
|
def klass_document_type(klass)
|
@@ -421,7 +446,13 @@ module Searchkick
|
|
421
446
|
# TODO expire Redis key
|
422
447
|
primary_key = scope.primary_key
|
423
448
|
|
424
|
-
starting_id =
|
449
|
+
starting_id =
|
450
|
+
begin
|
451
|
+
scope.minimum(primary_key)
|
452
|
+
rescue ActiveRecord::StatementInvalid
|
453
|
+
false
|
454
|
+
end
|
455
|
+
|
425
456
|
if starting_id.nil?
|
426
457
|
# no records, do nothing
|
427
458
|
elsif starting_id.is_a?(Numeric)
|
@@ -11,6 +11,7 @@ module Searchkick
|
|
11
11
|
else
|
12
12
|
below22 = Searchkick.server_below?("2.2.0")
|
13
13
|
below50 = Searchkick.server_below?("5.0.0-alpha1")
|
14
|
+
below60 = Searchkick.server_below?("6.0.0-alpha1")
|
14
15
|
default_type = below50 ? "string" : "text"
|
15
16
|
default_analyzer = :searchkick_index
|
16
17
|
keyword_mapping =
|
@@ -25,6 +26,10 @@ module Searchkick
|
|
25
26
|
}
|
26
27
|
end
|
27
28
|
|
29
|
+
all = options.key?(:_all) ? options[:_all] : below60
|
30
|
+
index_true_value = below50 ? "analyzed" : true
|
31
|
+
index_false_value = below50 ? "no" : false
|
32
|
+
|
28
33
|
keyword_mapping[:ignore_above] = (options[:ignore_above] || 30000) unless below22
|
29
34
|
|
30
35
|
settings = {
|
@@ -178,7 +183,7 @@ module Searchkick
|
|
178
183
|
# - Only apply the synonym expansion at index time
|
179
184
|
# - Don't have the synonym filter applied search
|
180
185
|
# - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
|
181
|
-
settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym")
|
186
|
+
settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym") if below60
|
182
187
|
settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
|
183
188
|
|
184
189
|
%w(word_start word_middle word_end).each do |type|
|
@@ -229,13 +234,13 @@ module Searchkick
|
|
229
234
|
|
230
235
|
mapping_options[:searchable].delete("_all")
|
231
236
|
|
232
|
-
analyzed_field_options = {type: default_type, index:
|
237
|
+
analyzed_field_options = {type: default_type, index: index_true_value, analyzer: default_analyzer}
|
233
238
|
|
234
239
|
mapping_options.values.flatten.uniq.each do |field|
|
235
240
|
fields = {}
|
236
241
|
|
237
242
|
if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
|
238
|
-
fields[field] = {type: default_type, index:
|
243
|
+
fields[field] = {type: default_type, index: index_false_value}
|
239
244
|
else
|
240
245
|
fields[field] = keyword_mapping
|
241
246
|
end
|
@@ -251,7 +256,7 @@ module Searchkick
|
|
251
256
|
|
252
257
|
mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
|
253
258
|
if options[:match] == type || f.include?(field)
|
254
|
-
fields[type] = {type: default_type, index:
|
259
|
+
fields[type] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{type}_index"}
|
255
260
|
end
|
256
261
|
end
|
257
262
|
end
|
@@ -283,16 +288,20 @@ module Searchkick
|
|
283
288
|
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
284
289
|
# however, we can include the not_analyzed field in _all
|
285
290
|
# and the _all index analyzer will take care of it
|
286
|
-
"{name}" => keyword_mapping
|
291
|
+
"{name}" => keyword_mapping
|
287
292
|
}
|
288
293
|
|
294
|
+
if below60 && all
|
295
|
+
dynamic_fields["{name}"][:include_in_all] = !options[:searchable]
|
296
|
+
end
|
297
|
+
|
289
298
|
if options.key?(:filterable)
|
290
|
-
dynamic_fields["{name}"] = {type: default_type, index:
|
299
|
+
dynamic_fields["{name}"] = {type: default_type, index: index_false_value}
|
291
300
|
end
|
292
301
|
|
293
302
|
unless options[:searchable]
|
294
303
|
if options[:match] && options[:match] != :word
|
295
|
-
dynamic_fields[options[:match]] = {type: default_type, index:
|
304
|
+
dynamic_fields[options[:match]] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{options[:match]}_index"}
|
296
305
|
end
|
297
306
|
|
298
307
|
if word
|
@@ -303,11 +312,8 @@ module Searchkick
|
|
303
312
|
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
304
313
|
multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
|
305
314
|
|
306
|
-
all_enabled = !options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all")
|
307
|
-
|
308
315
|
mappings = {
|
309
316
|
_default_: {
|
310
|
-
_all: all_enabled ? analyzed_field_options : {enabled: false},
|
311
317
|
properties: mapping,
|
312
318
|
_routing: routing,
|
313
319
|
# https://gist.github.com/kimchy/2898285
|
@@ -321,7 +327,14 @@ module Searchkick
|
|
321
327
|
}
|
322
328
|
]
|
323
329
|
}
|
324
|
-
}
|
330
|
+
}
|
331
|
+
|
332
|
+
if below60
|
333
|
+
all_enabled = all && (!options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all"))
|
334
|
+
mappings[:_default_][:_all] = all_enabled ? analyzed_field_options : {enabled: false}
|
335
|
+
end
|
336
|
+
|
337
|
+
mappings = mappings.deep_merge(options[:mappings] || {})
|
325
338
|
end
|
326
339
|
|
327
340
|
{
|
data/lib/searchkick/logging.rb
CHANGED
@@ -129,7 +129,7 @@ module Searchkick
|
|
129
129
|
end
|
130
130
|
|
131
131
|
module SearchkickWithInstrumentation
|
132
|
-
def multi_search(searches)
|
132
|
+
def multi_search(searches, **options)
|
133
133
|
event = {
|
134
134
|
name: "Multi Search",
|
135
135
|
body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
|
data/lib/searchkick/model.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Searchkick
|
2
2
|
module Model
|
3
3
|
def searchkick(**options)
|
4
|
-
unknown_keywords = options.keys - [:batch_size, :callbacks, :conversions,
|
4
|
+
unknown_keywords = options.keys - [:_all, :batch_size, :callbacks, :conversions, :default_fields,
|
5
5
|
:filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :language,
|
6
6
|
:locations, :mappings, :match, :merge_mappings, :routing, :searchable, :settings, :similarity,
|
7
7
|
:special_characters, :stem_conversions, :suggest, :synonyms, :text_end,
|
@@ -22,7 +22,7 @@ module Searchkick
|
|
22
22
|
class_variable_set :@@searchkick_callbacks, callbacks
|
23
23
|
class_variable_set :@@searchkick_index, options[:index_name] ||
|
24
24
|
(options[:index_prefix].respond_to?(:call) && proc { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }) ||
|
25
|
-
[options[:index_prefix], model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
25
|
+
[options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
26
26
|
|
27
27
|
class << self
|
28
28
|
def searchkick_search(term = "*", **options, &block)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class MultiSearch
|
3
|
+
attr_reader :queries
|
4
|
+
|
5
|
+
def initialize(queries, retry_misspellings: false)
|
6
|
+
@queries = queries
|
7
|
+
@retry_misspellings = retry_misspellings
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
if queries.any?
|
12
|
+
perform_search(queries, retry_misspellings: @retry_misspellings)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def perform_search(queries, retry_misspellings: true)
|
19
|
+
responses = client.msearch(body: queries.flat_map { |q| [q.params.except(:body), q.body] })["responses"]
|
20
|
+
|
21
|
+
retry_queries = []
|
22
|
+
queries.each_with_index do |query, i|
|
23
|
+
if retry_misspellings && query.retry_misspellings?(responses[i])
|
24
|
+
query.send(:prepare) # okay, since we don't want to expose this method outside Searchkick
|
25
|
+
retry_queries << query
|
26
|
+
else
|
27
|
+
query.handle_response(responses[i])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if retry_misspellings && retry_queries.any?
|
32
|
+
perform_search(retry_queries, retry_misspellings: false)
|
33
|
+
end
|
34
|
+
|
35
|
+
queries
|
36
|
+
end
|
37
|
+
|
38
|
+
def client
|
39
|
+
Searchkick.client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/searchkick/query.rb
CHANGED
@@ -18,7 +18,7 @@ module Searchkick
|
|
18
18
|
unknown_keywords = options.keys - [:aggs, :body, :body_options, :boost,
|
19
19
|
:boost_by, :boost_by_distance, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
|
20
20
|
:fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
|
21
|
-
:match, :misspellings, :offset, :operator, :order, :padding, :page, :per_page, :profile,
|
21
|
+
:match, :misspellings, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
|
22
22
|
:request_params, :routing, :select, :similar, :smart_aggs, :suggest, :track, :type, :where]
|
23
23
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
24
24
|
|
@@ -79,7 +79,7 @@ module Searchkick
|
|
79
79
|
@execute ||= begin
|
80
80
|
begin
|
81
81
|
response = execute_search
|
82
|
-
if
|
82
|
+
if retry_misspellings?(response)
|
83
83
|
prepare
|
84
84
|
response = execute_search
|
85
85
|
end
|
@@ -108,6 +108,7 @@ module Searchkick
|
|
108
108
|
padding: @padding,
|
109
109
|
load: @load,
|
110
110
|
includes: options[:includes],
|
111
|
+
model_includes: options[:model_includes],
|
111
112
|
json: !@json.nil?,
|
112
113
|
match_suffix: @match_suffix,
|
113
114
|
highlighted_fields: @highlighted_fields || [],
|
@@ -159,6 +160,10 @@ module Searchkick
|
|
159
160
|
@execute = Searchkick::Results.new(searchkick_klass, response, opts)
|
160
161
|
end
|
161
162
|
|
163
|
+
def retry_misspellings?(response)
|
164
|
+
@misspellings_below && response["hits"]["total"] < @misspellings_below
|
165
|
+
end
|
166
|
+
|
162
167
|
private
|
163
168
|
|
164
169
|
def handle_error(e)
|
@@ -218,6 +223,11 @@ module Searchkick
|
|
218
223
|
|
219
224
|
@json = options[:body]
|
220
225
|
if @json
|
226
|
+
ignored_options = options.keys & [:aggs, :boost,
|
227
|
+
:boost_by, :boost_by_distance, :boost_where, :conversions, :conversions_term, :exclude, :explain,
|
228
|
+
:fields, :highlight, :indices_boost, :limit, :match, :misspellings, :offset, :operator, :order,
|
229
|
+
:padding, :page, :per_page, :select, :smart_aggs, :suggest, :where]
|
230
|
+
warn "The body option replaces the entire body, so the following options are ignored: #{ignored_options.join(", ")}" if ignored_options.any?
|
221
231
|
payload = @json
|
222
232
|
else
|
223
233
|
if options[:similar]
|
@@ -468,15 +478,16 @@ module Searchkick
|
|
468
478
|
elsif load
|
469
479
|
payload[:_source] = false
|
470
480
|
end
|
481
|
+
end
|
471
482
|
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
# routing
|
477
|
-
@routing = options[:routing] if options[:routing]
|
483
|
+
# type
|
484
|
+
if options[:type] || (klass != searchkick_klass && searchkick_index)
|
485
|
+
@type = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v) }
|
478
486
|
end
|
479
487
|
|
488
|
+
# routing
|
489
|
+
@routing = options[:routing] if options[:routing]
|
490
|
+
|
480
491
|
# merge more body options
|
481
492
|
payload = payload.deep_merge(options[:body_options]) if options[:body_options]
|
482
493
|
|
@@ -489,7 +500,8 @@ module Searchkick
|
|
489
500
|
|
490
501
|
def set_fields
|
491
502
|
boost_fields = {}
|
492
|
-
fields = options[:fields] || searchkick_options[:searchable]
|
503
|
+
fields = options[:fields] || searchkick_options[:default_fields] || searchkick_options[:searchable]
|
504
|
+
all = searchkick_options.key?(:_all) ? searchkick_options[:_all] : below60?
|
493
505
|
default_match = options[:match] || searchkick_options[:match] || :word
|
494
506
|
fields =
|
495
507
|
if fields
|
@@ -500,12 +512,12 @@ module Searchkick
|
|
500
512
|
boost_fields[field] = boost.to_f if boost
|
501
513
|
field
|
502
514
|
end
|
503
|
-
elsif default_match == :word
|
515
|
+
elsif all && default_match == :word
|
504
516
|
["_all"]
|
505
|
-
elsif default_match == :phrase
|
517
|
+
elsif all && default_match == :phrase
|
506
518
|
["_all.phrase"]
|
507
519
|
else
|
508
|
-
raise ArgumentError, "Must specify fields"
|
520
|
+
raise ArgumentError, "Must specify fields to search"
|
509
521
|
end
|
510
522
|
[boost_fields, fields]
|
511
523
|
end
|
@@ -830,7 +842,7 @@ module Searchkick
|
|
830
842
|
if value.any?(&:nil?)
|
831
843
|
{bool: {should: [term_filters(field, nil), term_filters(field, value.compact)]}}
|
832
844
|
else
|
833
|
-
{
|
845
|
+
{terms: {field => value}}
|
834
846
|
end
|
835
847
|
elsif value.nil?
|
836
848
|
{bool: {must_not: {exists: {field: field}}}}
|
@@ -871,13 +883,21 @@ module Searchkick
|
|
871
883
|
}
|
872
884
|
}
|
873
885
|
|
874
|
-
|
875
|
-
|
886
|
+
if value[:missing]
|
887
|
+
if below50?
|
888
|
+
raise ArgumentError, "The missing option for boost_by is not supported in Elasticsearch < 5"
|
889
|
+
else
|
890
|
+
script_score[:field_value_factor][:missing] = value[:missing].to_f
|
891
|
+
end
|
892
|
+
else
|
893
|
+
script_score[:filter] = {
|
876
894
|
exists: {
|
877
895
|
field: field
|
878
896
|
}
|
879
897
|
}
|
880
|
-
|
898
|
+
end
|
899
|
+
|
900
|
+
script_score
|
881
901
|
end
|
882
902
|
end
|
883
903
|
|
@@ -905,5 +925,9 @@ module Searchkick
|
|
905
925
|
def below50?
|
906
926
|
Searchkick.server_below?("5.0.0-alpha1")
|
907
927
|
end
|
928
|
+
|
929
|
+
def below60?
|
930
|
+
Searchkick.server_below?("6.0.0-alpha1")
|
931
|
+
end
|
908
932
|
end
|
909
933
|
end
|
data/lib/searchkick/results.rb
CHANGED
@@ -198,23 +198,36 @@ module Searchkick
|
|
198
198
|
|
199
199
|
def results_query(records, hits)
|
200
200
|
ids = hits.map { |hit| hit["_id"] }
|
201
|
+
if options[:includes] || options[:model_includes]
|
202
|
+
included_relations = []
|
203
|
+
combine_includes(included_relations, options[:includes])
|
204
|
+
combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]
|
201
205
|
|
202
|
-
if options[:includes]
|
203
206
|
records =
|
204
207
|
if defined?(NoBrainer::Document) && records < NoBrainer::Document
|
205
208
|
if Gem.loaded_specs["nobrainer"].version >= Gem::Version.new("0.21")
|
206
|
-
records.eager_load(
|
209
|
+
records.eager_load(included_relations)
|
207
210
|
else
|
208
|
-
records.preload(
|
211
|
+
records.preload(included_relations)
|
209
212
|
end
|
210
213
|
else
|
211
|
-
records.includes(
|
214
|
+
records.includes(included_relations)
|
212
215
|
end
|
213
216
|
end
|
214
217
|
|
215
218
|
Searchkick.load_records(records, ids)
|
216
219
|
end
|
217
220
|
|
221
|
+
def combine_includes(result, inc)
|
222
|
+
if inc
|
223
|
+
if inc.is_a?(Array)
|
224
|
+
result.concat(inc)
|
225
|
+
else
|
226
|
+
result << inc
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
218
231
|
def base_field(k)
|
219
232
|
k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
|
220
233
|
end
|
data/lib/searchkick/version.rb
CHANGED
data/test/boost_test.rb
CHANGED
@@ -116,6 +116,21 @@ class BoostTest < Minitest::Test
|
|
116
116
|
assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"], boost_by: {orders_count: {factor: 10}}
|
117
117
|
end
|
118
118
|
|
119
|
+
def test_boost_by_missing
|
120
|
+
store [
|
121
|
+
{name: "Tomato A"},
|
122
|
+
{name: "Tomato B", orders_count: 10},
|
123
|
+
]
|
124
|
+
|
125
|
+
if elasticsearch_below50?
|
126
|
+
assert_raises(ArgumentError) do
|
127
|
+
assert_order "tomato", ["Tomato A", "Tomato B"], boost_by: {orders_count: {missing: 100}}
|
128
|
+
end
|
129
|
+
else
|
130
|
+
assert_order "tomato", ["Tomato A", "Tomato B"], boost_by: {orders_count: {missing: 100}}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
119
134
|
def test_boost_by_boost_mode_multiply
|
120
135
|
store [
|
121
136
|
{name: "Tomato A", found_rate: 0.9},
|
data/test/geo_shape_test.rb
CHANGED
@@ -108,7 +108,7 @@ class GeoShapeTest < Minitest::Test
|
|
108
108
|
geo_shape: {
|
109
109
|
type: "envelope",
|
110
110
|
relation: "within",
|
111
|
-
coordinates: [[20,50], [50,20]]
|
111
|
+
coordinates: [[20, 50], [50, 20]]
|
112
112
|
}
|
113
113
|
}
|
114
114
|
}
|
@@ -116,6 +116,9 @@ class GeoShapeTest < Minitest::Test
|
|
116
116
|
end
|
117
117
|
|
118
118
|
def test_search_math
|
119
|
+
# TODO find out why this is failing
|
120
|
+
skip unless elasticsearch_below60?
|
121
|
+
|
119
122
|
assert_search "witch", ["Region A"], {
|
120
123
|
where: {
|
121
124
|
territory: {
|
data/test/highlight_test.rb
CHANGED
@@ -32,7 +32,7 @@ class HighlightTest < Minitest::Test
|
|
32
32
|
|
33
33
|
def test_field_options
|
34
34
|
store_names ["Two Door Cinema Club are a Northern Irish indie rock band"]
|
35
|
-
fragment_size = ENV["MATCH"] == "word_start" ? 26 :
|
35
|
+
fragment_size = ENV["MATCH"] == "word_start" ? 26 : 21
|
36
36
|
assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: fragment_size}}}).first.search_highlights[:name]
|
37
37
|
end
|
38
38
|
|
data/test/index_test.rb
CHANGED
@@ -59,6 +59,10 @@ class IndexTest < Minitest::Test
|
|
59
59
|
assert_equal ["Dollar Tree"], Store.search(body: {query: {match: {name: "Dollar Tree"}}}, load: false).map(&:name)
|
60
60
|
end
|
61
61
|
|
62
|
+
def test_body_warning
|
63
|
+
assert_output(nil, "The body option replaces the entire body, so the following options are ignored: where\n") { Store.search(body: {query: {match: {name: "dollar"}}}, where: {id: 1}) }
|
64
|
+
end
|
65
|
+
|
62
66
|
def test_block
|
63
67
|
store_names ["Dollar Tree"]
|
64
68
|
products =
|
@@ -143,7 +147,11 @@ class IndexTest < Minitest::Test
|
|
143
147
|
store [{name: "Product A", text: large_value}], Region
|
144
148
|
assert_search "product", ["Product A"], {}, Region
|
145
149
|
assert_search "hello", ["Product A"], {fields: [:name, :text]}, Region
|
146
|
-
|
150
|
+
|
151
|
+
# needs fields for ES 6
|
152
|
+
if elasticsearch_below60?
|
153
|
+
assert_search "hello", ["Product A"], {}, Region
|
154
|
+
end
|
147
155
|
end
|
148
156
|
|
149
157
|
def test_very_large_value
|
data/test/multi_search_test.rb
CHANGED
@@ -19,4 +19,18 @@ class MultiSearchTest < Minitest::Test
|
|
19
19
|
assert !products.error
|
20
20
|
assert stores.error
|
21
21
|
end
|
22
|
+
|
23
|
+
def test_misspellings_below_unmet
|
24
|
+
store_names ["abc", "abd", "aee"]
|
25
|
+
products = Product.search("abc", misspellings: {below: 2}, execute: false)
|
26
|
+
Searchkick.multi_search([products])
|
27
|
+
assert_equal ["abc"], products.map(&:name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_misspellings_below_unmet_retry
|
31
|
+
store_names ["abc", "abd", "aee"]
|
32
|
+
products = Product.search("abc", misspellings: {below: 2}, execute: false)
|
33
|
+
Searchkick.multi_search([products], retry_misspellings: true)
|
34
|
+
assert_equal ["abc", "abd"], products.map(&:name)
|
35
|
+
end
|
22
36
|
end
|
data/test/routing_test.rb
CHANGED
@@ -13,11 +13,11 @@ class RoutingTest < Minitest::Test
|
|
13
13
|
|
14
14
|
def test_routing_correct_node
|
15
15
|
store_names ["Dollar Tree"], Store
|
16
|
-
assert_search "
|
16
|
+
assert_search "*", ["Dollar Tree"], {routing: "Dollar Tree"}, Store
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_routing_incorrect_node
|
20
20
|
store_names ["Dollar Tree"], Store
|
21
|
-
assert_search "
|
21
|
+
assert_search "*", ["Dollar Tree"], {routing: "Boom"}, Store
|
22
22
|
end
|
23
23
|
end
|
data/test/sql_test.rb
CHANGED
@@ -195,4 +195,20 @@ class SqlTest < Minitest::Test
|
|
195
195
|
store_names ["Product A"]
|
196
196
|
assert Product.search("product", includes: [:store]).first.association(:store).loaded?
|
197
197
|
end
|
198
|
+
|
199
|
+
def test_model_includes
|
200
|
+
skip unless defined?(ActiveRecord)
|
201
|
+
|
202
|
+
store_names ["Product A"]
|
203
|
+
store_names ["Store A"], Store
|
204
|
+
|
205
|
+
associations = {Product => [:store], Store => [:products]}
|
206
|
+
result = Searchkick.search("*", index_name: [Product, Store], model_includes: associations)
|
207
|
+
|
208
|
+
assert_equal 2, result.length
|
209
|
+
|
210
|
+
result.group_by(&:class).each_pair do |klass, records|
|
211
|
+
assert records.first.association(associations[klass].first).loaded?
|
212
|
+
end
|
213
|
+
end
|
198
214
|
end
|
data/test/suggest_test.rb
CHANGED
@@ -69,7 +69,7 @@ class SuggestTest < Minitest::Test
|
|
69
69
|
|
70
70
|
def test_multiple_models
|
71
71
|
store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
|
72
|
-
assert_equal "how big is a tiger shark", Searchkick.search("How Big is a Tigre Shar", suggest: [:name]).suggestions.first
|
72
|
+
assert_equal "how big is a tiger shark", Searchkick.search("How Big is a Tigre Shar", suggest: [:name], fields: [:name]).suggestions.first
|
73
73
|
end
|
74
74
|
|
75
75
|
def test_multiple_models_no_fields
|
data/test/test_helper.rb
CHANGED
@@ -42,6 +42,10 @@ def elasticsearch_below50?
|
|
42
42
|
Searchkick.server_below?("5.0.0-alpha1")
|
43
43
|
end
|
44
44
|
|
45
|
+
def elasticsearch_below60?
|
46
|
+
Searchkick.server_below?("6.0.0-alpha1")
|
47
|
+
end
|
48
|
+
|
45
49
|
def elasticsearch_below22?
|
46
50
|
Searchkick.server_below?("2.2.0")
|
47
51
|
end
|
@@ -444,6 +448,7 @@ end
|
|
444
448
|
|
445
449
|
class Store
|
446
450
|
searchkick \
|
451
|
+
default_fields: elasticsearch_below60? ? nil : [:name],
|
447
452
|
routing: true,
|
448
453
|
merge_mappings: true,
|
449
454
|
mappings: {
|
@@ -465,6 +470,7 @@ end
|
|
465
470
|
|
466
471
|
class Region
|
467
472
|
searchkick \
|
473
|
+
default_fields: elasticsearch_below60? ? nil : [:name],
|
468
474
|
geo_shape: {
|
469
475
|
territory: {tree: "quadtree", precision: "10km"}
|
470
476
|
}
|
@@ -482,6 +488,7 @@ end
|
|
482
488
|
|
483
489
|
class Speaker
|
484
490
|
searchkick \
|
491
|
+
default_fields: elasticsearch_below60? ? nil : [:name],
|
485
492
|
conversions: ["conversions_a", "conversions_b"]
|
486
493
|
|
487
494
|
attr_accessor :conversions_a, :conversions_b, :aisle
|
@@ -497,6 +504,7 @@ end
|
|
497
504
|
|
498
505
|
class Animal
|
499
506
|
searchkick \
|
507
|
+
default_fields: elasticsearch_below60? ? nil : [:name],
|
500
508
|
text_start: [:name],
|
501
509
|
suggest: [:name],
|
502
510
|
index_name: -> { "#{name.tableize}-#{Date.today.year}#{Searchkick.index_suffix}" },
|
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.3.
|
4
|
+
version: 2.3.2
|
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-
|
11
|
+
date: 2017-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -118,6 +118,7 @@ files:
|
|
118
118
|
- lib/searchkick/logging.rb
|
119
119
|
- lib/searchkick/middleware.rb
|
120
120
|
- lib/searchkick/model.rb
|
121
|
+
- lib/searchkick/multi_search.rb
|
121
122
|
- lib/searchkick/process_batch_job.rb
|
122
123
|
- lib/searchkick/process_queue_job.rb
|
123
124
|
- lib/searchkick/query.rb
|
@@ -194,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
194
195
|
version: '0'
|
195
196
|
requirements: []
|
196
197
|
rubyforge_project:
|
197
|
-
rubygems_version: 2.6.
|
198
|
+
rubygems_version: 2.6.13
|
198
199
|
signing_key:
|
199
200
|
specification_version: 4
|
200
201
|
summary: Searchkick learns what your users are looking for. As more people search,
|