searchkick 4.6.3 → 5.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -1
- data/README.md +321 -203
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +149 -67
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +19 -69
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +9 -2
- data/lib/searchkick/model.rb +49 -50
- data/lib/searchkick/process_batch_job.rb +9 -25
- data/lib/searchkick/process_queue_job.rb +3 -2
- data/lib/searchkick/query.rb +51 -57
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -52
- data/lib/searchkick/reindex_queue.rb +36 -8
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +247 -0
- data/lib/searchkick/relation_indexer.rb +155 -0
- data/lib/searchkick/results.rb +29 -28
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +175 -98
- data/lib/tasks/searchkick.rake +6 -3
- metadata +12 -28
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
data/lib/searchkick/indexer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# thread-local (technically fiber-local) indexer
|
2
|
+
# used to aggregate bulk callbacks across models
|
1
3
|
module Searchkick
|
2
4
|
class Indexer
|
3
5
|
attr_reader :queued_items
|
@@ -14,15 +16,20 @@ module Searchkick
|
|
14
16
|
def perform
|
15
17
|
items = @queued_items
|
16
18
|
@queued_items = []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
19
|
+
|
20
|
+
return if items.empty?
|
21
|
+
|
22
|
+
response = Searchkick.client.bulk(body: items)
|
23
|
+
if response["errors"]
|
24
|
+
# note: delete does not set error when item not found
|
25
|
+
first_with_error = response["items"].map do |item|
|
26
|
+
(item["index"] || item["delete"] || item["update"])
|
27
|
+
end.find { |item| item["error"] }
|
28
|
+
raise ImportError, "#{first_with_error["error"]} on item with id '#{first_with_error["_id"]}'"
|
25
29
|
end
|
30
|
+
|
31
|
+
# maybe return response in future
|
32
|
+
nil
|
26
33
|
end
|
27
34
|
end
|
28
35
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# based on https://gist.github.com/mnutt/566725
|
2
|
+
module Searchkick
|
3
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
4
|
+
def self.runtime=(value)
|
5
|
+
Thread.current[:searchkick_runtime] = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.runtime
|
9
|
+
Thread.current[:searchkick_runtime] ||= 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.reset_runtime
|
13
|
+
rt = runtime
|
14
|
+
self.runtime = 0
|
15
|
+
rt
|
16
|
+
end
|
17
|
+
|
18
|
+
def search(event)
|
19
|
+
self.class.runtime += event.duration
|
20
|
+
return unless logger.debug?
|
21
|
+
|
22
|
+
payload = event.payload
|
23
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
24
|
+
|
25
|
+
index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
|
26
|
+
type = payload[:query][:type]
|
27
|
+
request_params = payload[:query].except(:index, :type, :body)
|
28
|
+
|
29
|
+
params = []
|
30
|
+
request_params.each do |k, v|
|
31
|
+
params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
32
|
+
end
|
33
|
+
|
34
|
+
debug " #{color(name, YELLOW, bold: true)} #{index}#{type ? "/#{type.join(',')}" : ''}/_search#{params.any? ? '?' + params.join('&') : nil} #{payload[:query][:body].to_json}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def request(event)
|
38
|
+
self.class.runtime += event.duration
|
39
|
+
return unless logger.debug?
|
40
|
+
|
41
|
+
payload = event.payload
|
42
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
43
|
+
|
44
|
+
debug " #{color(name, YELLOW, bold: true)} #{payload.except(:name).to_json}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def multi_search(event)
|
48
|
+
self.class.runtime += event.duration
|
49
|
+
return unless logger.debug?
|
50
|
+
|
51
|
+
payload = event.payload
|
52
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
53
|
+
|
54
|
+
debug " #{color(name, YELLOW, bold: true)} _msearch #{payload[:body]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,10 +1,17 @@
|
|
1
|
-
require "faraday
|
1
|
+
require "faraday"
|
2
2
|
|
3
3
|
module Searchkick
|
4
4
|
class Middleware < Faraday::Middleware
|
5
5
|
def call(env)
|
6
|
-
|
6
|
+
path = env[:url].path.to_s
|
7
|
+
if path.end_with?("/_search")
|
7
8
|
env[:request][:timeout] = Searchkick.search_timeout
|
9
|
+
elsif path.end_with?("/_msearch")
|
10
|
+
# assume no concurrent searches for timeout for now
|
11
|
+
searches = env[:request_body].count("\n") / 2
|
12
|
+
# do not allow timeout to exceed Searchkick.timeout
|
13
|
+
timeout = [Searchkick.search_timeout * searches, Searchkick.timeout].min
|
14
|
+
env[:request][:timeout] = timeout
|
8
15
|
end
|
9
16
|
@app.call(env)
|
10
17
|
end
|
data/lib/searchkick/model.rb
CHANGED
@@ -5,9 +5,9 @@ module Searchkick
|
|
5
5
|
|
6
6
|
unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
|
7
7
|
:filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
|
8
|
-
:locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
|
8
|
+
:locations, :mappings, :match, :max_result_window, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
|
9
9
|
:special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
|
10
|
-
:text_middle, :text_start, :
|
10
|
+
:text_middle, :text_start, :unscope, :word, :word_end, :word_middle, :word_start]
|
11
11
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
12
12
|
|
13
13
|
raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
|
@@ -22,52 +22,78 @@ module Searchkick
|
|
22
22
|
raise ArgumentError, "Invalid value for callbacks"
|
23
23
|
end
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
end
|
25
|
+
base = self
|
26
|
+
|
27
|
+
mod = Module.new
|
28
|
+
include(mod)
|
29
|
+
mod.module_eval do
|
30
|
+
def reindex(method_name = nil, mode: nil, refresh: false)
|
31
|
+
self.class.searchkick_index.reindex([self], method_name: method_name, mode: mode, refresh: refresh, single: true)
|
32
|
+
end unless base.method_defined?(:reindex)
|
33
|
+
|
34
|
+
def similar(**options)
|
35
|
+
self.class.searchkick_index.similar_record(self, **options)
|
36
|
+
end unless base.method_defined?(:similar)
|
37
|
+
|
38
|
+
def search_data
|
39
|
+
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
40
|
+
data.delete("id")
|
41
|
+
data.delete("_id")
|
42
|
+
data.delete("_type")
|
43
|
+
data
|
44
|
+
end unless base.method_defined?(:search_data)
|
45
|
+
|
46
|
+
def should_index?
|
47
|
+
true
|
48
|
+
end unless base.method_defined?(:should_index?)
|
49
|
+
end
|
33
50
|
|
34
51
|
class_eval do
|
35
|
-
cattr_reader :searchkick_options, :searchkick_klass
|
52
|
+
cattr_reader :searchkick_options, :searchkick_klass, instance_reader: false
|
36
53
|
|
37
54
|
class_variable_set :@@searchkick_options, options.dup
|
38
55
|
class_variable_set :@@searchkick_klass, self
|
39
|
-
class_variable_set :@@
|
40
|
-
class_variable_set :@@searchkick_index_cache, {}
|
56
|
+
class_variable_set :@@searchkick_index_cache, Searchkick::IndexCache.new
|
41
57
|
|
42
58
|
class << self
|
43
59
|
def searchkick_search(term = "*", **options, &block)
|
44
|
-
|
45
|
-
|
60
|
+
if Searchkick.relation?(self)
|
61
|
+
raise Searchkick::Error, "search must be called on model, not relation"
|
62
|
+
end
|
46
63
|
|
47
64
|
Searchkick.search(term, model: self, **options, &block)
|
48
65
|
end
|
49
66
|
alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
|
50
67
|
|
51
68
|
def searchkick_index(name: nil)
|
52
|
-
|
53
|
-
|
69
|
+
index_name = name || searchkick_klass.searchkick_index_name
|
70
|
+
index_name = index_name.call if index_name.respond_to?(:call)
|
54
71
|
index_cache = class_variable_get(:@@searchkick_index_cache)
|
55
|
-
index_cache
|
72
|
+
index_cache.fetch(index_name) { Searchkick::Index.new(index_name, searchkick_options) }
|
56
73
|
end
|
57
74
|
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
|
58
75
|
|
59
76
|
def searchkick_reindex(method_name = nil, **options)
|
60
|
-
|
61
|
-
relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
|
62
|
-
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
63
|
-
|
64
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
|
77
|
+
searchkick_index.reindex(self, method_name: method_name, **options)
|
65
78
|
end
|
66
79
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
67
80
|
|
68
81
|
def searchkick_index_options
|
69
82
|
searchkick_index.index_options
|
70
83
|
end
|
84
|
+
|
85
|
+
def searchkick_index_name
|
86
|
+
@searchkick_index_name ||= begin
|
87
|
+
options = class_variable_get(:@@searchkick_options)
|
88
|
+
if options[:index_name]
|
89
|
+
options[:index_name]
|
90
|
+
elsif options[:index_prefix].respond_to?(:call)
|
91
|
+
-> { [options[:index_prefix].call, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_") }
|
92
|
+
else
|
93
|
+
[options.key?(:index_prefix) ? options[:index_prefix] : Searchkick.index_prefix, model_name.plural, Searchkick.env, Searchkick.index_suffix].compact.join("_")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
71
97
|
end
|
72
98
|
|
73
99
|
# always add callbacks, even when callbacks is false
|
@@ -78,33 +104,6 @@ module Searchkick
|
|
78
104
|
after_save :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
79
105
|
after_destroy :reindex, if: -> { Searchkick.callbacks?(default: callbacks) }
|
80
106
|
end
|
81
|
-
|
82
|
-
def reindex(method_name = nil, **options)
|
83
|
-
RecordIndexer.new(self).reindex(method_name, **options)
|
84
|
-
end unless method_defined?(:reindex)
|
85
|
-
|
86
|
-
# TODO switch to keyword arguments
|
87
|
-
def similar(options = {})
|
88
|
-
self.class.searchkick_index.similar_record(self, **options)
|
89
|
-
end unless method_defined?(:similar)
|
90
|
-
|
91
|
-
def search_data
|
92
|
-
data = respond_to?(:to_hash) ? to_hash : serializable_hash
|
93
|
-
data.delete("id")
|
94
|
-
data.delete("_id")
|
95
|
-
data.delete("_type")
|
96
|
-
data
|
97
|
-
end unless method_defined?(:search_data)
|
98
|
-
|
99
|
-
def should_index?
|
100
|
-
true
|
101
|
-
end unless method_defined?(:should_index?)
|
102
|
-
|
103
|
-
if defined?(Cequel) && self < Cequel::Record && !method_defined?(:destroyed?)
|
104
|
-
def destroyed?
|
105
|
-
transient?
|
106
|
-
end
|
107
|
-
end
|
108
107
|
end
|
109
108
|
end
|
110
109
|
end
|
@@ -3,34 +3,18 @@ module Searchkick
|
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
5
|
def perform(class_name:, record_ids:, index_name: nil)
|
6
|
-
|
7
|
-
|
8
|
-
record_ids = routing.keys
|
6
|
+
model = Searchkick.load_model(class_name)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# determine which records to delete
|
16
|
-
delete_ids = record_ids - records.map { |r| r.id.to_s }
|
17
|
-
delete_records = delete_ids.map do |id|
|
18
|
-
m = klass.new
|
19
|
-
m.id = id
|
20
|
-
if routing[id]
|
21
|
-
m.define_singleton_method(:search_routing) do
|
22
|
-
routing[id]
|
23
|
-
end
|
9
|
+
items =
|
10
|
+
record_ids.map do |r|
|
11
|
+
parts = r.split(/(?<!\|)\|(?!\|)/, 2)
|
12
|
+
.map { |v| v.gsub("||", "|") }
|
13
|
+
{id: parts[0], routing: parts[1]}
|
24
14
|
end
|
25
|
-
m
|
26
|
-
end
|
27
15
|
|
28
|
-
|
29
|
-
index
|
30
|
-
Searchkick.callbacks(:bulk) do
|
31
|
-
index.bulk_index(records) if records.any?
|
32
|
-
index.bulk_delete(delete_records) if delete_records.any?
|
33
|
-
end
|
16
|
+
relation = Searchkick.scope(model)
|
17
|
+
RecordIndexer.new(index).reindex_items(relation, items, method_name: nil)
|
34
18
|
end
|
35
19
|
end
|
36
20
|
end
|
@@ -3,11 +3,12 @@ module Searchkick
|
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
5
|
def perform(class_name:, index_name: nil, inline: false)
|
6
|
-
model = class_name
|
6
|
+
model = Searchkick.load_model(class_name)
|
7
|
+
index = model.searchkick_index(name: index_name)
|
7
8
|
limit = model.searchkick_options[:batch_size] || 1000
|
8
9
|
|
9
10
|
loop do
|
10
|
-
record_ids =
|
11
|
+
record_ids = index.reindex_queue.reserve(limit: limit)
|
11
12
|
if record_ids.any?
|
12
13
|
batch_options = {
|
13
14
|
class_name: class_name,
|
data/lib/searchkick/query.rb
CHANGED
@@ -18,7 +18,7 @@ module Searchkick
|
|
18
18
|
|
19
19
|
def initialize(klass, term = "*", **options)
|
20
20
|
unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
|
21
|
-
:boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :
|
21
|
+
:boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :explain,
|
22
22
|
:fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
|
23
23
|
:match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
|
24
24
|
:request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
|
@@ -148,9 +148,6 @@ module Searchkick
|
|
148
148
|
}
|
149
149
|
|
150
150
|
if options[:debug]
|
151
|
-
# can remove when minimum Ruby version is 2.5
|
152
|
-
require "pp"
|
153
|
-
|
154
151
|
puts "Searchkick Version: #{Searchkick::VERSION}"
|
155
152
|
puts "Elasticsearch Version: #{Searchkick.server_version}"
|
156
153
|
puts
|
@@ -190,11 +187,11 @@ module Searchkick
|
|
190
187
|
end
|
191
188
|
|
192
189
|
# set execute for multi search
|
193
|
-
@execute =
|
190
|
+
@execute = Results.new(searchkick_klass, response, opts)
|
194
191
|
end
|
195
192
|
|
196
193
|
def retry_misspellings?(response)
|
197
|
-
@misspellings_below &&
|
194
|
+
@misspellings_below && Results.new(searchkick_klass, response).total_count < @misspellings_below
|
198
195
|
end
|
199
196
|
|
200
197
|
private
|
@@ -202,7 +199,11 @@ module Searchkick
|
|
202
199
|
def handle_error(e)
|
203
200
|
status_code = e.message[1..3].to_i
|
204
201
|
if status_code == 404
|
205
|
-
|
202
|
+
if e.message.include?("No search context found for id")
|
203
|
+
raise MissingIndexError, "No search context found for id"
|
204
|
+
else
|
205
|
+
raise MissingIndexError, "Index missing - run #{reindex_command}"
|
206
|
+
end
|
206
207
|
elsif status_code == 500 && (
|
207
208
|
e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") ||
|
208
209
|
e.message.include?("No query registered for [multi_match]") ||
|
@@ -210,14 +211,14 @@ module Searchkick
|
|
210
211
|
e.message.include?("No query registered for [function_score]")
|
211
212
|
)
|
212
213
|
|
213
|
-
raise UnsupportedVersionError
|
214
|
+
raise UnsupportedVersionError
|
214
215
|
elsif status_code == 400
|
215
216
|
if (
|
216
217
|
e.message.include?("bool query does not support [filter]") ||
|
217
218
|
e.message.include?("[bool] filter does not support [filter]")
|
218
219
|
)
|
219
220
|
|
220
|
-
raise UnsupportedVersionError
|
221
|
+
raise UnsupportedVersionError
|
221
222
|
elsif e.message =~ /analyzer \[searchkick_.+\] not found/
|
222
223
|
raise InvalidQueryError, "Bad mapping - run #{reindex_command}"
|
223
224
|
else
|
@@ -233,7 +234,14 @@ module Searchkick
|
|
233
234
|
end
|
234
235
|
|
235
236
|
def execute_search
|
236
|
-
|
237
|
+
name = searchkick_klass ? "#{searchkick_klass.name} Search" : "Search"
|
238
|
+
event = {
|
239
|
+
name: name,
|
240
|
+
query: params
|
241
|
+
}
|
242
|
+
ActiveSupport::Notifications.instrument("search.searchkick", event) do
|
243
|
+
Searchkick.client.search(params)
|
244
|
+
end
|
237
245
|
end
|
238
246
|
|
239
247
|
def prepare
|
@@ -247,9 +255,15 @@ module Searchkick
|
|
247
255
|
default_limit = searchkick_options[:deep_paging] ? 1_000_000_000 : 10_000
|
248
256
|
per_page = (options[:limit] || options[:per_page] || default_limit).to_i
|
249
257
|
padding = [options[:padding].to_i, 0].max
|
250
|
-
offset = options[:offset] || (page - 1) * per_page + padding
|
258
|
+
offset = (options[:offset] || (page - 1) * per_page + padding).to_i
|
251
259
|
scroll = options[:scroll]
|
252
260
|
|
261
|
+
max_result_window = searchkick_options[:max_result_window]
|
262
|
+
if max_result_window
|
263
|
+
offset = max_result_window if offset > max_result_window
|
264
|
+
per_page = max_result_window - offset if offset + per_page > max_result_window
|
265
|
+
end
|
266
|
+
|
253
267
|
# model and eager loading
|
254
268
|
load = options[:load].nil? ? true : options[:load]
|
255
269
|
|
@@ -268,9 +282,10 @@ module Searchkick
|
|
268
282
|
should = []
|
269
283
|
|
270
284
|
if options[:similar]
|
285
|
+
like = options[:similar] == true ? term : options[:similar]
|
271
286
|
query = {
|
272
287
|
more_like_this: {
|
273
|
-
like:
|
288
|
+
like: like,
|
274
289
|
min_doc_freq: 1,
|
275
290
|
min_term_freq: 1,
|
276
291
|
analyzer: "searchkick_search2"
|
@@ -383,11 +398,6 @@ module Searchkick
|
|
383
398
|
|
384
399
|
if field.start_with?("*.")
|
385
400
|
q2 = qs.map { |q| {multi_match: q.merge(fields: [field], type: match_type == :match_phrase ? "phrase" : "best_fields")} }
|
386
|
-
if below61?
|
387
|
-
q2.each do |q|
|
388
|
-
q[:multi_match].delete(:fuzzy_transpositions)
|
389
|
-
end
|
390
|
-
end
|
391
401
|
else
|
392
402
|
q2 = qs.map { |q| {match_type => {field => q}} }
|
393
403
|
end
|
@@ -439,7 +449,7 @@ module Searchkick
|
|
439
449
|
payload = {}
|
440
450
|
|
441
451
|
# type when inheritance
|
442
|
-
where = (options[:where] || {}).dup
|
452
|
+
where = ensure_permitted(options[:where] || {}).dup
|
443
453
|
if searchkick_options[:inheritance] && (options[:type] || (klass != searchkick_klass && searchkick_index))
|
444
454
|
where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
|
445
455
|
end
|
@@ -499,7 +509,7 @@ module Searchkick
|
|
499
509
|
set_highlights(payload, fields) if options[:highlight]
|
500
510
|
|
501
511
|
# timeout shortly after client times out
|
502
|
-
payload[:timeout] ||= "#{Searchkick.search_timeout + 1}
|
512
|
+
payload[:timeout] ||= "#{((Searchkick.search_timeout + 1) * 1000).round}ms"
|
503
513
|
|
504
514
|
# An empty array will cause only the _id and _type for each hit to be returned
|
505
515
|
# https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
|
@@ -696,9 +706,9 @@ module Searchkick
|
|
696
706
|
def set_boost_by(multiply_filters, custom_filters)
|
697
707
|
boost_by = options[:boost_by] || {}
|
698
708
|
if boost_by.is_a?(Array)
|
699
|
-
boost_by =
|
709
|
+
boost_by = boost_by.to_h { |f| [f, {factor: 1}] }
|
700
710
|
elsif boost_by.is_a?(Hash)
|
701
|
-
multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map
|
711
|
+
multiply_by, boost_by = boost_by.partition { |_, v| v.delete(:boost_mode) == "multiply" }.map(&:to_h)
|
702
712
|
end
|
703
713
|
boost_by[options[:boost]] = {factor: 1} if options[:boost]
|
704
714
|
|
@@ -763,7 +773,7 @@ module Searchkick
|
|
763
773
|
|
764
774
|
def set_highlights(payload, fields)
|
765
775
|
payload[:highlight] = {
|
766
|
-
fields:
|
776
|
+
fields: fields.to_h { |f| [f, {}] },
|
767
777
|
fragment_size: 0
|
768
778
|
}
|
769
779
|
|
@@ -797,7 +807,7 @@ module Searchkick
|
|
797
807
|
aggs = options[:aggs]
|
798
808
|
payload[:aggs] = {}
|
799
809
|
|
800
|
-
aggs =
|
810
|
+
aggs = aggs.to_h { |f| [f, {}] } if aggs.is_a?(Array) # convert to more advanced syntax
|
801
811
|
aggs.each do |field, agg_options|
|
802
812
|
size = agg_options[:limit] ? agg_options[:limit] : 1_000
|
803
813
|
shared_agg_options = agg_options.except(:limit, :field, :ranges, :date_ranges, :where)
|
@@ -836,8 +846,9 @@ module Searchkick
|
|
836
846
|
end
|
837
847
|
|
838
848
|
where = {}
|
839
|
-
where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
|
840
|
-
|
849
|
+
where = ensure_permitted(options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
|
850
|
+
agg_where = ensure_permitted(agg_options[:where] || {})
|
851
|
+
agg_filters = where_filters(where.merge(agg_where))
|
841
852
|
|
842
853
|
# only do one level comparison for simplicity
|
843
854
|
filters.select! do |filter|
|
@@ -873,19 +884,16 @@ module Searchkick
|
|
873
884
|
end
|
874
885
|
|
875
886
|
def set_order(payload)
|
876
|
-
|
877
|
-
id_field = :_id
|
878
|
-
# TODO no longer map id to _id in Searchkick 5
|
879
|
-
# since sorting on _id is deprecated in Elasticsearch
|
880
|
-
payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
|
887
|
+
payload[:sort] = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
|
881
888
|
end
|
882
889
|
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
890
|
+
# provides *very* basic protection from unfiltered parameters
|
891
|
+
# this is not meant to be comprehensive and may be expanded in the future
|
892
|
+
def ensure_permitted(obj)
|
893
|
+
obj.to_h
|
894
|
+
end
|
888
895
|
|
896
|
+
def where_filters(where)
|
889
897
|
filters = []
|
890
898
|
(where || {}).each do |field, value|
|
891
899
|
field = :_id if field.to_s == "id"
|
@@ -1007,7 +1015,7 @@ module Searchkick
|
|
1007
1015
|
when :lte
|
1008
1016
|
{to: op_value, include_upper: true}
|
1009
1017
|
else
|
1010
|
-
raise "Unknown where operator: #{op.inspect}"
|
1018
|
+
raise ArgumentError, "Unknown where operator: #{op.inspect}"
|
1011
1019
|
end
|
1012
1020
|
# issue 132
|
1013
1021
|
if (existing = filters.find { |f| f[:range] && f[:range][field] })
|
@@ -1036,29 +1044,25 @@ module Searchkick
|
|
1036
1044
|
{bool: {must_not: {exists: {field: field}}}}
|
1037
1045
|
elsif value.is_a?(Regexp)
|
1038
1046
|
source = value.source
|
1039
|
-
|
1040
|
-
|
1041
|
-
Searchkick.warn("Regular expressions are always anchored in Elasticsearch")
|
1042
|
-
end
|
1047
|
+
|
1048
|
+
# TODO handle other regexp options
|
1043
1049
|
|
1044
1050
|
# TODO handle other anchor characters, like ^, $, \Z
|
1045
1051
|
if source.start_with?("\\A")
|
1046
1052
|
source = source[2..-1]
|
1047
1053
|
else
|
1048
|
-
|
1049
|
-
# source = ".*#{source}"
|
1054
|
+
source = ".*#{source}"
|
1050
1055
|
end
|
1051
1056
|
|
1052
1057
|
if source.end_with?("\\z")
|
1053
1058
|
source = source[0..-3]
|
1054
1059
|
else
|
1055
|
-
|
1056
|
-
# source = "#{source}.*"
|
1060
|
+
source = "#{source}.*"
|
1057
1061
|
end
|
1058
1062
|
|
1059
1063
|
if below710?
|
1060
1064
|
if value.casefold?
|
1061
|
-
|
1065
|
+
raise ArgumentError, "Case-insensitive flag does not work with Elasticsearch < 7.10"
|
1062
1066
|
end
|
1063
1067
|
{regexp: {field => {value: source, flags: "NONE"}}}
|
1064
1068
|
else
|
@@ -1069,9 +1073,7 @@ module Searchkick
|
|
1069
1073
|
if value.as_json.is_a?(Enumerable)
|
1070
1074
|
# query will fail, but this is better
|
1071
1075
|
# same message as Active Record
|
1072
|
-
|
1073
|
-
# raise InvalidQueryError for backward compatibility
|
1074
|
-
raise Searchkick::InvalidQueryError, "can't cast #{value.class.name}"
|
1076
|
+
raise TypeError, "can't cast #{value.class.name}"
|
1075
1077
|
end
|
1076
1078
|
|
1077
1079
|
{term: {field => {value: value}}}
|
@@ -1150,21 +1152,13 @@ module Searchkick
|
|
1150
1152
|
end
|
1151
1153
|
|
1152
1154
|
def track_total_hits?
|
1153
|
-
|
1155
|
+
searchkick_options[:deep_paging] || body_options[:track_total_hits]
|
1154
1156
|
end
|
1155
1157
|
|
1156
1158
|
def body_options
|
1157
1159
|
options[:body_options] || {}
|
1158
1160
|
end
|
1159
1161
|
|
1160
|
-
def below61?
|
1161
|
-
Searchkick.server_below?("6.1.0")
|
1162
|
-
end
|
1163
|
-
|
1164
|
-
def below70?
|
1165
|
-
Searchkick.server_below?("7.0.0")
|
1166
|
-
end
|
1167
|
-
|
1168
1162
|
def below73?
|
1169
1163
|
Searchkick.server_below?("7.3.0")
|
1170
1164
|
end
|
@@ -25,6 +25,7 @@ module Searchkick
|
|
25
25
|
{delete: record_data}
|
26
26
|
end
|
27
27
|
|
28
|
+
# custom id can be useful for load: false
|
28
29
|
def search_id
|
29
30
|
id = record.respond_to?(:search_document_id) ? record.search_document_id : record.id
|
30
31
|
id.is_a?(Numeric) ? id : id.to_s
|
@@ -39,7 +40,6 @@ module Searchkick
|
|
39
40
|
_index: index.name,
|
40
41
|
_id: search_id
|
41
42
|
}
|
42
|
-
data[:_type] = document_type if Searchkick.server_below7?
|
43
43
|
data[:routing] = record.search_routing if record.respond_to?(:search_routing)
|
44
44
|
data
|
45
45
|
end
|