searchkick 3.1.2 → 4.4.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 +178 -94
- data/LICENSE.txt +1 -1
- data/README.md +339 -272
- data/lib/searchkick.rb +72 -41
- data/lib/searchkick/bulk_indexer.rb +5 -3
- data/lib/searchkick/index.rb +64 -13
- data/lib/searchkick/index_options.rb +480 -348
- data/lib/searchkick/logging.rb +11 -8
- data/lib/searchkick/model.rb +15 -9
- data/lib/searchkick/process_batch_job.rb +2 -2
- data/lib/searchkick/process_queue_job.rb +19 -11
- data/lib/searchkick/query.rb +161 -38
- data/lib/searchkick/railtie.rb +7 -0
- data/lib/searchkick/record_data.rb +5 -10
- data/lib/searchkick/results.rb +154 -57
- data/lib/searchkick/version.rb +1 -1
- data/lib/tasks/searchkick.rake +34 -0
- metadata +15 -58
- data/CONTRIBUTING.md +0 -53
- data/lib/searchkick/tasks.rb +0 -29
data/lib/searchkick/logging.rb
CHANGED
@@ -132,7 +132,7 @@ module Searchkick
|
|
132
132
|
def multi_search(searches)
|
133
133
|
event = {
|
134
134
|
name: "Multi Search",
|
135
|
-
body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join
|
135
|
+
body: searches.flat_map { |q| [q.params.except(:body).to_json, q.body.to_json] }.map { |v| "#{v}\n" }.join,
|
136
136
|
}
|
137
137
|
ActiveSupport::Notifications.instrument("multi_search.searchkick", event) do
|
138
138
|
super
|
@@ -162,12 +162,17 @@ module Searchkick
|
|
162
162
|
|
163
163
|
payload = event.payload
|
164
164
|
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
165
|
-
|
165
|
+
|
166
166
|
index = payload[:query][:index].is_a?(Array) ? payload[:query][:index].join(",") : payload[:query][:index]
|
167
|
+
type = payload[:query][:type]
|
168
|
+
request_params = payload[:query].except(:index, :type, :body)
|
169
|
+
|
170
|
+
params = []
|
171
|
+
request_params.each do |k, v|
|
172
|
+
params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
173
|
+
end
|
167
174
|
|
168
|
-
#
|
169
|
-
host = Searchkick.client.transport.hosts.first
|
170
|
-
debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -H 'Content-Type: application/json' -d '#{payload[:query][:body].to_json}'"
|
175
|
+
debug " #{color(name, YELLOW, true)} #{index}#{type ? "/#{type.join(',')}" : ''}/_search#{params.any? ? '?' + params.join('&') : nil} #{payload[:query][:body].to_json}"
|
171
176
|
end
|
172
177
|
|
173
178
|
def request(event)
|
@@ -187,9 +192,7 @@ module Searchkick
|
|
187
192
|
payload = event.payload
|
188
193
|
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
189
194
|
|
190
|
-
#
|
191
|
-
host = Searchkick.client.transport.hosts.first
|
192
|
-
debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/_msearch?pretty -H 'Content-Type: application/json' -d '#{payload[:body]}'"
|
195
|
+
debug " #{color(name, YELLOW, true)} _msearch #{payload[:body]}"
|
193
196
|
end
|
194
197
|
end
|
195
198
|
|
data/lib/searchkick/model.rb
CHANGED
@@ -3,10 +3,10 @@ module Searchkick
|
|
3
3
|
def searchkick(**options)
|
4
4
|
options = Searchkick.model_options.merge(options)
|
5
5
|
|
6
|
-
unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :default_fields,
|
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, :settings, :similarity,
|
9
|
-
:special_characters, :stem, :stem_conversions, :suggest, :synonyms, :text_end,
|
8
|
+
:locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
|
9
|
+
:special_characters, :stem, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
|
10
10
|
:text_middle, :text_start, :word, :wordnet, :word_end, :word_middle, :word_start]
|
11
11
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
12
12
|
|
@@ -15,6 +15,7 @@ module Searchkick
|
|
15
15
|
Searchkick.models << self
|
16
16
|
|
17
17
|
options[:_type] ||= -> { searchkick_index.klass_document_type(self, true) }
|
18
|
+
options[:class_name] = model_name.name
|
18
19
|
|
19
20
|
callbacks = options.key?(:callbacks) ? options[:callbacks] : :inline
|
20
21
|
unless [:inline, true, false, :async, :queue].include?(callbacks)
|
@@ -40,12 +41,15 @@ module Searchkick
|
|
40
41
|
|
41
42
|
class << self
|
42
43
|
def searchkick_search(term = "*", **options, &block)
|
43
|
-
|
44
|
+
# TODO throw error in next major version
|
45
|
+
Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
|
46
|
+
|
47
|
+
Searchkick.search(term, model: self, **options, &block)
|
44
48
|
end
|
45
49
|
alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
|
46
50
|
|
47
|
-
def searchkick_index
|
48
|
-
index = class_variable_get(:@@searchkick_index)
|
51
|
+
def searchkick_index(name: nil)
|
52
|
+
index = name || class_variable_get(:@@searchkick_index)
|
49
53
|
index = index.call if index.respond_to?(:call)
|
50
54
|
index_cache = class_variable_get(:@@searchkick_index_cache)
|
51
55
|
index_cache[index] ||= Searchkick::Index.new(index, searchkick_options)
|
@@ -53,10 +57,11 @@ module Searchkick
|
|
53
57
|
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
|
54
58
|
|
55
59
|
def searchkick_reindex(method_name = nil, **options)
|
56
|
-
|
60
|
+
# TODO relation = Searchkick.relation?(self)
|
61
|
+
relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
|
57
62
|
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
58
63
|
|
59
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped:
|
64
|
+
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
|
60
65
|
end
|
61
66
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
62
67
|
|
@@ -78,8 +83,9 @@ module Searchkick
|
|
78
83
|
RecordIndexer.new(self).reindex(method_name, **options)
|
79
84
|
end unless method_defined?(:reindex)
|
80
85
|
|
86
|
+
# TODO switch to keyword arguments
|
81
87
|
def similar(options = {})
|
82
|
-
self.class.searchkick_index.similar_record(self, options)
|
88
|
+
self.class.searchkick_index.similar_record(self, **options)
|
83
89
|
end unless method_defined?(:similar)
|
84
90
|
|
85
91
|
def search_data
|
@@ -2,7 +2,7 @@ module Searchkick
|
|
2
2
|
class ProcessBatchJob < ActiveJob::Base
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
|
-
def perform(class_name:, record_ids:)
|
5
|
+
def perform(class_name:, record_ids:, index_name: nil)
|
6
6
|
# separate routing from id
|
7
7
|
routing = Hash[record_ids.map { |r| r.split(/(?<!\|)\|(?!\|)/, 2).map { |v| v.gsub("||", "|") } }]
|
8
8
|
record_ids = routing.keys
|
@@ -26,7 +26,7 @@ module Searchkick
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# bulk reindex
|
29
|
-
index = klass.searchkick_index
|
29
|
+
index = klass.searchkick_index(name: index_name)
|
30
30
|
Searchkick.callbacks(:bulk) do
|
31
31
|
index.bulk_index(records) if records.any?
|
32
32
|
index.bulk_delete(delete_records) if delete_records.any?
|
@@ -2,21 +2,29 @@ module Searchkick
|
|
2
2
|
class ProcessQueueJob < ActiveJob::Base
|
3
3
|
queue_as { Searchkick.queue_name }
|
4
4
|
|
5
|
-
def perform(class_name:)
|
5
|
+
def perform(class_name:, index_name: nil, inline: false)
|
6
6
|
model = class_name.constantize
|
7
|
+
limit = model.searchkick_options[:batch_size] || 1000
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
9
|
+
loop do
|
10
|
+
record_ids = model.searchkick_index(name: index_name).reindex_queue.reserve(limit: limit)
|
11
|
+
if record_ids.any?
|
12
|
+
batch_options = {
|
13
|
+
class_name: class_name,
|
14
|
+
record_ids: record_ids,
|
15
|
+
index_name: index_name
|
16
|
+
}
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
if inline
|
19
|
+
# use new.perform to avoid excessive logging
|
20
|
+
Searchkick::ProcessBatchJob.new.perform(**batch_options)
|
21
|
+
else
|
22
|
+
Searchkick::ProcessBatchJob.perform_later(**batch_options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# TODO when moving to reliable queuing, mark as complete
|
19
26
|
end
|
27
|
+
break unless record_ids.size == limit
|
20
28
|
end
|
21
29
|
end
|
22
30
|
end
|
data/lib/searchkick/query.rb
CHANGED
@@ -12,14 +12,14 @@ module Searchkick
|
|
12
12
|
:took, :error, :model_name, :entry_name, :total_count, :total_entries,
|
13
13
|
:current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
|
14
14
|
:offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
|
15
|
-
:out_of_range?, :hits, :response, :to_a, :first
|
15
|
+
:out_of_range?, :hits, :response, :to_a, :first, :scroll
|
16
16
|
|
17
17
|
def initialize(klass, term = "*", **options)
|
18
|
-
unknown_keywords = options.keys - [:aggs, :body, :body_options, :boost,
|
18
|
+
unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
|
19
19
|
:boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
|
20
20
|
:fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
|
21
|
-
:match, :misspellings, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
|
22
|
-
:request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
|
21
|
+
:match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
|
22
|
+
:request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
|
23
23
|
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
24
24
|
|
25
25
|
term = term.to_s
|
@@ -39,6 +39,7 @@ module Searchkick
|
|
39
39
|
@misspellings = false
|
40
40
|
@misspellings_below = nil
|
41
41
|
@highlighted_fields = nil
|
42
|
+
@index_mapping = nil
|
42
43
|
|
43
44
|
prepare
|
44
45
|
end
|
@@ -56,9 +57,19 @@ module Searchkick
|
|
56
57
|
end
|
57
58
|
|
58
59
|
def params
|
60
|
+
if options[:models]
|
61
|
+
@index_mapping = {}
|
62
|
+
Array(options[:models]).each do |model|
|
63
|
+
# there can be multiple models per index name due to inheritance - see #1259
|
64
|
+
(@index_mapping[model.searchkick_index.name] ||= []) << model
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
59
68
|
index =
|
60
69
|
if options[:index_name]
|
61
70
|
Array(options[:index_name]).map { |v| v.respond_to?(:searchkick_index) ? v.searchkick_index.name : v }.join(",")
|
71
|
+
elsif options[:models]
|
72
|
+
@index_mapping.keys.join(",")
|
62
73
|
elsif searchkick_index
|
63
74
|
searchkick_index.name
|
64
75
|
else
|
@@ -71,6 +82,7 @@ module Searchkick
|
|
71
82
|
}
|
72
83
|
params[:type] = @type if @type
|
73
84
|
params[:routing] = @routing if @routing
|
85
|
+
params[:scroll] = @scroll if @scroll
|
74
86
|
params.merge!(options[:request_params]) if options[:request_params]
|
75
87
|
params
|
76
88
|
end
|
@@ -94,11 +106,16 @@ module Searchkick
|
|
94
106
|
query = params
|
95
107
|
type = query[:type]
|
96
108
|
index = query[:index].is_a?(Array) ? query[:index].join(",") : query[:index]
|
109
|
+
request_params = query.except(:index, :type, :body)
|
97
110
|
|
98
111
|
# no easy way to tell which host the client will use
|
99
112
|
host = Searchkick.client.transport.hosts.first
|
100
113
|
credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
|
101
|
-
|
114
|
+
params = ["pretty"]
|
115
|
+
request_params.each do |k, v|
|
116
|
+
params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
|
117
|
+
end
|
118
|
+
"curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?#{params.join('&')} -H 'Content-Type: application/json' -d '#{query[:body].to_json}'"
|
102
119
|
end
|
103
120
|
|
104
121
|
def handle_response(response)
|
@@ -116,11 +133,14 @@ module Searchkick
|
|
116
133
|
misspellings: @misspellings,
|
117
134
|
term: term,
|
118
135
|
scope_results: options[:scope_results],
|
119
|
-
|
120
|
-
|
136
|
+
total_entries: options[:total_entries],
|
137
|
+
index_mapping: @index_mapping,
|
138
|
+
suggest: options[:suggest],
|
139
|
+
scroll: options[:scroll]
|
121
140
|
}
|
122
141
|
|
123
142
|
if options[:debug]
|
143
|
+
# can remove when minimum Ruby version is 2.5
|
124
144
|
require "pp"
|
125
145
|
|
126
146
|
puts "Searchkick Version: #{Searchkick::VERSION}"
|
@@ -166,7 +186,7 @@ module Searchkick
|
|
166
186
|
end
|
167
187
|
|
168
188
|
def retry_misspellings?(response)
|
169
|
-
@misspellings_below && response
|
189
|
+
@misspellings_below && Searchkick::Results.new(searchkick_klass, response).total_count < @misspellings_below
|
170
190
|
end
|
171
191
|
|
172
192
|
private
|
@@ -215,9 +235,12 @@ module Searchkick
|
|
215
235
|
|
216
236
|
# pagination
|
217
237
|
page = [options[:page].to_i, 1].max
|
218
|
-
|
238
|
+
# maybe use index.max_result_window in the future
|
239
|
+
default_limit = searchkick_options[:deep_paging] ? 1_000_000_000 : 10_000
|
240
|
+
per_page = (options[:limit] || options[:per_page] || default_limit).to_i
|
219
241
|
padding = [options[:padding].to_i, 0].max
|
220
242
|
offset = options[:offset] || (page - 1) * per_page + padding
|
243
|
+
scroll = options[:scroll]
|
221
244
|
|
222
245
|
# model and eager loading
|
223
246
|
load = options[:load].nil? ? true : options[:load]
|
@@ -327,7 +350,7 @@ module Searchkick
|
|
327
350
|
field_misspellings = misspellings && (!misspellings_fields || misspellings_fields.include?(base_field(field)))
|
328
351
|
|
329
352
|
if field == "_all" || field.end_with?(".analyzed")
|
330
|
-
shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false
|
353
|
+
shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false || (!below73? && !track_total_hits?)
|
331
354
|
qs << shared_options.merge(analyzer: "searchkick_search")
|
332
355
|
|
333
356
|
# searchkick_search and searchkick_search2 are the same for ukrainian
|
@@ -377,7 +400,7 @@ module Searchkick
|
|
377
400
|
queries_to_add.concat(q2)
|
378
401
|
end
|
379
402
|
|
380
|
-
queries
|
403
|
+
queries << queries_to_add
|
381
404
|
|
382
405
|
if options[:exclude]
|
383
406
|
must_not.concat(set_exclude(exclude_field, exclude_analyzer))
|
@@ -392,9 +415,10 @@ module Searchkick
|
|
392
415
|
|
393
416
|
should = []
|
394
417
|
else
|
418
|
+
# higher score for matching more fields
|
395
419
|
payload = {
|
396
|
-
|
397
|
-
queries: queries
|
420
|
+
bool: {
|
421
|
+
should: queries.map { |qs| {dis_max: {queries: qs}} }
|
398
422
|
}
|
399
423
|
}
|
400
424
|
|
@@ -412,6 +436,24 @@ module Searchkick
|
|
412
436
|
where[:type] = [options[:type] || klass].flatten.map { |v| searchkick_index.klass_document_type(v, true) }
|
413
437
|
end
|
414
438
|
|
439
|
+
models = Array(options[:models])
|
440
|
+
if models.any? { |m| m != m.searchkick_klass }
|
441
|
+
# aliases are not supported with _index in ES below 7.5
|
442
|
+
# see https://github.com/elastic/elasticsearch/pull/46640
|
443
|
+
if below75?
|
444
|
+
Searchkick.warn("Passing child models to models option throws off hits and pagination - use type option instead")
|
445
|
+
else
|
446
|
+
index_type_or =
|
447
|
+
models.map do |m|
|
448
|
+
v = {_index: m.searchkick_index.name}
|
449
|
+
v[:type] = m.searchkick_index.klass_document_type(m, true) if m != m.searchkick_klass
|
450
|
+
v
|
451
|
+
end
|
452
|
+
|
453
|
+
where[:or] = Array(where[:or]) + [index_type_or]
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
415
457
|
# start everything as efficient filters
|
416
458
|
# move to post_filters as aggs demand
|
417
459
|
filters = where_filters(where)
|
@@ -469,7 +511,7 @@ module Searchkick
|
|
469
511
|
pagination_options = options[:page] || options[:limit] || options[:per_page] || options[:offset] || options[:padding]
|
470
512
|
if !options[:body] || pagination_options
|
471
513
|
payload[:size] = per_page
|
472
|
-
payload[:from] = offset
|
514
|
+
payload[:from] = offset if offset > 0
|
473
515
|
end
|
474
516
|
|
475
517
|
# type
|
@@ -480,14 +522,28 @@ module Searchkick
|
|
480
522
|
# routing
|
481
523
|
@routing = options[:routing] if options[:routing]
|
482
524
|
|
525
|
+
if track_total_hits?
|
526
|
+
payload[:track_total_hits] = true
|
527
|
+
end
|
528
|
+
|
483
529
|
# merge more body options
|
484
530
|
payload = payload.deep_merge(options[:body_options]) if options[:body_options]
|
485
531
|
|
532
|
+
# run block
|
533
|
+
options[:block].call(payload) if options[:block]
|
534
|
+
|
535
|
+
# scroll optimization when interating over all docs
|
536
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
|
537
|
+
if options[:scroll] && payload[:query] == {match_all: {}}
|
538
|
+
payload[:sort] ||= ["_doc"]
|
539
|
+
end
|
540
|
+
|
486
541
|
@body = payload
|
487
542
|
@page = page
|
488
543
|
@per_page = per_page
|
489
544
|
@padding = padding
|
490
545
|
@load = load
|
546
|
+
@scroll = scroll
|
491
547
|
end
|
492
548
|
|
493
549
|
def set_fields
|
@@ -518,7 +574,8 @@ module Searchkick
|
|
518
574
|
|
519
575
|
def build_query(query, filters, should, must_not, custom_filters, multiply_filters)
|
520
576
|
if filters.any? || must_not.any? || should.any?
|
521
|
-
bool = {
|
577
|
+
bool = {}
|
578
|
+
bool[:must] = query if query
|
522
579
|
bool[:filter] = filters if filters.any? # where
|
523
580
|
bool[:must_not] = must_not if must_not.any? # exclude
|
524
581
|
bool[:should] = should if should.any? # conversions
|
@@ -660,20 +717,9 @@ module Searchkick
|
|
660
717
|
def set_boost_by_indices(payload)
|
661
718
|
return unless options[:indices_boost]
|
662
719
|
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
# try to use index explicitly instead of alias: https://github.com/elasticsearch/elasticsearch/issues/4756
|
667
|
-
index_by_alias = Searchkick.client.indices.get_alias(index: index).keys.first
|
668
|
-
memo[index_by_alias || index] = boost
|
669
|
-
end
|
670
|
-
else
|
671
|
-
# array format supports alias resolution
|
672
|
-
# https://github.com/elastic/elasticsearch/pull/21393
|
673
|
-
indices_boost = options[:indices_boost].map do |key, boost|
|
674
|
-
index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
|
675
|
-
{index => boost}
|
676
|
-
end
|
720
|
+
indices_boost = options[:indices_boost].map do |key, boost|
|
721
|
+
index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
|
722
|
+
{index => boost}
|
677
723
|
end
|
678
724
|
|
679
725
|
payload[:indices_boost] = indices_boost
|
@@ -710,7 +756,7 @@ module Searchkick
|
|
710
756
|
def set_highlights(payload, fields)
|
711
757
|
payload[:highlight] = {
|
712
758
|
fields: Hash[fields.map { |f| [f, {}] }],
|
713
|
-
fragment_size:
|
759
|
+
fragment_size: 0
|
714
760
|
}
|
715
761
|
|
716
762
|
if options[:highlight].is_a?(Hash)
|
@@ -821,11 +867,16 @@ module Searchkick
|
|
821
867
|
# TODO id transformation for arrays
|
822
868
|
def set_order(payload)
|
823
869
|
order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
|
824
|
-
id_field =
|
870
|
+
id_field = :_id
|
825
871
|
payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
|
826
872
|
end
|
827
873
|
|
828
874
|
def where_filters(where)
|
875
|
+
# if where.respond_to?(:permitted?) && !where.permitted?
|
876
|
+
# # TODO check in more places
|
877
|
+
# Searchkick.warn("Passing unpermitted parameters will raise an exception in Searchkick 5")
|
878
|
+
# end
|
879
|
+
|
829
880
|
filters = []
|
830
881
|
(where || {}).each do |field, value|
|
831
882
|
field = :_id if field.to_s == "id"
|
@@ -840,10 +891,17 @@ module Searchkick
|
|
840
891
|
filters << {bool: {must_not: where_filters(value)}}
|
841
892
|
elsif field == :_and
|
842
893
|
filters << {bool: {must: value.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
|
894
|
+
# elsif field == :_script
|
895
|
+
# filters << {script: {script: {source: value, lang: "painless"}}}
|
843
896
|
else
|
844
897
|
# expand ranges
|
845
898
|
if value.is_a?(Range)
|
846
|
-
|
899
|
+
# infinite? added in Ruby 2.4
|
900
|
+
if value.end.nil? || (value.end.respond_to?(:infinite?) && value.end.infinite?)
|
901
|
+
value = {gte: value.first}
|
902
|
+
else
|
903
|
+
value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
|
904
|
+
end
|
847
905
|
end
|
848
906
|
|
849
907
|
value = {in: value} if value.is_a?(Array)
|
@@ -895,6 +953,23 @@ module Searchkick
|
|
895
953
|
}
|
896
954
|
}
|
897
955
|
}
|
956
|
+
when :like
|
957
|
+
# based on Postgres
|
958
|
+
# https://www.postgresql.org/docs/current/functions-matching.html
|
959
|
+
# % matches zero or more characters
|
960
|
+
# _ matches one character
|
961
|
+
# \ is escape character
|
962
|
+
# escape Lucene reserved characters
|
963
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html#regexp-optional-operators
|
964
|
+
reserved = %w(. ? + * | { } [ ] ( ) " \\)
|
965
|
+
regex = op_value.dup
|
966
|
+
reserved.each do |v|
|
967
|
+
regex.gsub!(v, "\\" + v)
|
968
|
+
end
|
969
|
+
regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
|
970
|
+
filters << {regexp: {field => {value: regex, flags: "NONE"}}}
|
971
|
+
when :prefix
|
972
|
+
filters << {prefix: {field => {value: op_value}}}
|
898
973
|
when :regexp # support for regexp queries without using a regexp ruby object
|
899
974
|
filters << {regexp: {field => {value: op_value}}}
|
900
975
|
when :not, :_not # not equal
|
@@ -905,6 +980,8 @@ module Searchkick
|
|
905
980
|
end
|
906
981
|
when :in
|
907
982
|
filters << term_filters(field, op_value)
|
983
|
+
when :exists
|
984
|
+
filters << {exists: {field: field}}
|
908
985
|
else
|
909
986
|
range_query =
|
910
987
|
case op
|
@@ -945,9 +1022,43 @@ module Searchkick
|
|
945
1022
|
elsif value.nil?
|
946
1023
|
{bool: {must_not: {exists: {field: field}}}}
|
947
1024
|
elsif value.is_a?(Regexp)
|
948
|
-
|
1025
|
+
if value.casefold?
|
1026
|
+
Searchkick.warn("Case-insensitive flag does not work with Elasticsearch")
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
source = value.source
|
1030
|
+
unless source.start_with?("\\A") && source.end_with?("\\z")
|
1031
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html
|
1032
|
+
Searchkick.warn("Regular expressions are always anchored in Elasticsearch")
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
# TODO handle other anchor characters, like ^, $, \Z
|
1036
|
+
if source.start_with?("\\A")
|
1037
|
+
source = source[2..-1]
|
1038
|
+
else
|
1039
|
+
# TODO uncomment in Searchkick 5
|
1040
|
+
# source = ".*#{source}"
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
if source.end_with?("\\z")
|
1044
|
+
source = source[0..-3]
|
1045
|
+
else
|
1046
|
+
# TODO uncomment in Searchkick 5
|
1047
|
+
# source = "#{source}.*"
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
{regexp: {field => {value: source, flags: "NONE"}}}
|
949
1051
|
else
|
950
|
-
|
1052
|
+
# TODO add this for other values
|
1053
|
+
if value.as_json.is_a?(Enumerable)
|
1054
|
+
# query will fail, but this is better
|
1055
|
+
# same message as Active Record
|
1056
|
+
# TODO make TypeError
|
1057
|
+
# raise InvalidQueryError for backward compatibility
|
1058
|
+
raise Searchkick::InvalidQueryError, "can't cast #{value.class.name}"
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
{term: {field => {value: value}}}
|
951
1062
|
end
|
952
1063
|
end
|
953
1064
|
|
@@ -1011,16 +1122,28 @@ module Searchkick
|
|
1011
1122
|
k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
|
1012
1123
|
end
|
1013
1124
|
|
1014
|
-
def
|
1015
|
-
|
1125
|
+
def track_total_hits?
|
1126
|
+
(searchkick_options[:deep_paging] && !below70?) || body_options[:track_total_hits]
|
1016
1127
|
end
|
1017
1128
|
|
1018
|
-
def
|
1019
|
-
|
1129
|
+
def body_options
|
1130
|
+
options[:body_options] || {}
|
1020
1131
|
end
|
1021
1132
|
|
1022
1133
|
def below61?
|
1023
1134
|
Searchkick.server_below?("6.1.0")
|
1024
1135
|
end
|
1136
|
+
|
1137
|
+
def below70?
|
1138
|
+
Searchkick.server_below?("7.0.0")
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
def below73?
|
1142
|
+
Searchkick.server_below?("7.3.0")
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def below75?
|
1146
|
+
Searchkick.server_below?("7.5.0")
|
1147
|
+
end
|
1025
1148
|
end
|
1026
1149
|
end
|