searchkick 4.2.1 → 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 +30 -3
- data/README.md +264 -199
- data/lib/searchkick.rb +30 -11
- data/lib/searchkick/bulk_indexer.rb +1 -1
- data/lib/searchkick/index.rb +22 -2
- data/lib/searchkick/index_options.rb +468 -382
- data/lib/searchkick/model.rb +8 -4
- data/lib/searchkick/query.rb +27 -5
- data/lib/searchkick/results.rb +101 -66
- data/lib/searchkick/version.rb +1 -1
- data/lib/tasks/searchkick.rake +12 -11
- metadata +7 -50
- data/CONTRIBUTING.md +0 -53
data/lib/searchkick/model.rb
CHANGED
@@ -5,8 +5,8 @@ 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, :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
|
|
@@ -41,6 +41,9 @@ module Searchkick
|
|
41
41
|
|
42
42
|
class << self
|
43
43
|
def searchkick_search(term = "*", **options, &block)
|
44
|
+
# TODO throw error in next major version
|
45
|
+
Searchkick.warn("calling search on a relation is deprecated") if Searchkick.relation?(self)
|
46
|
+
|
44
47
|
Searchkick.search(term, model: self, **options, &block)
|
45
48
|
end
|
46
49
|
alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name
|
@@ -54,10 +57,11 @@ module Searchkick
|
|
54
57
|
alias_method :search_index, :searchkick_index unless method_defined?(:search_index)
|
55
58
|
|
56
59
|
def searchkick_reindex(method_name = nil, **options)
|
57
|
-
|
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) ||
|
58
62
|
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
59
63
|
|
60
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped:
|
64
|
+
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
|
61
65
|
end
|
62
66
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
63
67
|
|
data/lib/searchkick/query.rb
CHANGED
@@ -574,7 +574,8 @@ module Searchkick
|
|
574
574
|
|
575
575
|
def build_query(query, filters, should, must_not, custom_filters, multiply_filters)
|
576
576
|
if filters.any? || must_not.any? || should.any?
|
577
|
-
bool = {
|
577
|
+
bool = {}
|
578
|
+
bool[:must] = query if query
|
578
579
|
bool[:filter] = filters if filters.any? # where
|
579
580
|
bool[:must_not] = must_not if must_not.any? # exclude
|
580
581
|
bool[:should] = should if should.any? # conversions
|
@@ -871,6 +872,11 @@ module Searchkick
|
|
871
872
|
end
|
872
873
|
|
873
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
|
+
|
874
880
|
filters = []
|
875
881
|
(where || {}).each do |field, value|
|
876
882
|
field = :_id if field.to_s == "id"
|
@@ -953,10 +959,17 @@ module Searchkick
|
|
953
959
|
# % matches zero or more characters
|
954
960
|
# _ matches one character
|
955
961
|
# \ is escape character
|
956
|
-
|
957
|
-
|
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"}}}
|
958
971
|
when :prefix
|
959
|
-
filters << {prefix: {field => op_value}}
|
972
|
+
filters << {prefix: {field => {value: op_value}}}
|
960
973
|
when :regexp # support for regexp queries without using a regexp ruby object
|
961
974
|
filters << {regexp: {field => {value: op_value}}}
|
962
975
|
when :not, :_not # not equal
|
@@ -1036,7 +1049,16 @@ module Searchkick
|
|
1036
1049
|
|
1037
1050
|
{regexp: {field => {value: source, flags: "NONE"}}}
|
1038
1051
|
else
|
1039
|
-
|
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}}}
|
1040
1062
|
end
|
1041
1063
|
end
|
1042
1064
|
|
data/lib/searchkick/results.rb
CHANGED
@@ -19,78 +19,20 @@ module Searchkick
|
|
19
19
|
@results ||= with_hit.map(&:first)
|
20
20
|
end
|
21
21
|
|
22
|
+
# TODO return enumerator like with_score
|
22
23
|
def with_hit
|
23
24
|
@with_hit ||= begin
|
24
|
-
if
|
25
|
-
#
|
26
|
-
results = {}
|
27
|
-
|
28
|
-
hits.group_by { |hit, _| hit["_index"] }.each do |index, grouped_hits|
|
29
|
-
klasses =
|
30
|
-
if @klass
|
31
|
-
[@klass]
|
32
|
-
else
|
33
|
-
index_alias = index.split("_")[0..-2].join("_")
|
34
|
-
Array((options[:index_mapping] || {})[index_alias])
|
35
|
-
end
|
36
|
-
raise Searchkick::Error, "Unknown model for index: #{index}" unless klasses.any?
|
37
|
-
|
38
|
-
results[index] = {}
|
39
|
-
klasses.each do |klass|
|
40
|
-
results[index].merge!(results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s })
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
missing_ids = []
|
45
|
-
|
46
|
-
# sort
|
47
|
-
results =
|
48
|
-
hits.map do |hit|
|
49
|
-
result = results[hit["_index"]][hit["_id"].to_s]
|
50
|
-
if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
|
51
|
-
if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
|
52
|
-
highlights = hit_highlights(hit)
|
53
|
-
result.define_singleton_method(:search_highlights) do
|
54
|
-
highlights
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
[result, hit]
|
59
|
-
end.select do |result, hit|
|
60
|
-
missing_ids << hit["_id"] unless result
|
61
|
-
result
|
62
|
-
end
|
63
|
-
|
64
|
-
if missing_ids.any?
|
65
|
-
Searchkick.warn("Records in search index do not exist in database: #{missing_ids.join(", ")}")
|
66
|
-
end
|
67
|
-
|
68
|
-
results
|
69
|
-
else
|
70
|
-
hits.map do |hit|
|
71
|
-
result =
|
72
|
-
if hit["_source"]
|
73
|
-
hit.except("_source").merge(hit["_source"])
|
74
|
-
elsif hit["fields"]
|
75
|
-
hit.except("fields").merge(hit["fields"])
|
76
|
-
else
|
77
|
-
hit
|
78
|
-
end
|
79
|
-
|
80
|
-
if hit["highlight"] || options[:highlight]
|
81
|
-
highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
|
82
|
-
options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
|
83
|
-
result["highlighted_#{k}"] ||= (highlight[k] || result[k])
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
result["id"] ||= result["_id"] # needed for legacy reasons
|
88
|
-
[HashWrapper.new(result), hit]
|
89
|
-
end
|
25
|
+
if missing_records.any?
|
26
|
+
Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| v[:id] }.join(", ")}")
|
90
27
|
end
|
28
|
+
with_hit_and_missing_records[0]
|
91
29
|
end
|
92
30
|
end
|
93
31
|
|
32
|
+
def missing_records
|
33
|
+
@missing_records ||= with_hit_and_missing_records[1]
|
34
|
+
end
|
35
|
+
|
94
36
|
def suggestions
|
95
37
|
if response["suggest"]
|
96
38
|
response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
|
@@ -211,12 +153,21 @@ module Searchkick
|
|
211
153
|
end
|
212
154
|
end
|
213
155
|
|
156
|
+
# TODO return enumerator like with_score
|
214
157
|
def with_highlights(multiple: false)
|
215
158
|
with_hit.map do |result, hit|
|
216
159
|
[result, hit_highlights(hit, multiple: multiple)]
|
217
160
|
end
|
218
161
|
end
|
219
162
|
|
163
|
+
def with_score
|
164
|
+
return enum_for(:with_score) unless block_given?
|
165
|
+
|
166
|
+
with_hit.each do |result, hit|
|
167
|
+
yield result, hit["_score"]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
220
171
|
def misspellings?
|
221
172
|
@options[:misspellings]
|
222
173
|
end
|
@@ -268,6 +219,90 @@ module Searchkick
|
|
268
219
|
|
269
220
|
private
|
270
221
|
|
222
|
+
def with_hit_and_missing_records
|
223
|
+
@with_hit_and_missing_records ||= begin
|
224
|
+
missing_records = []
|
225
|
+
|
226
|
+
if options[:load]
|
227
|
+
grouped_hits = hits.group_by { |hit, _| hit["_index"] }
|
228
|
+
|
229
|
+
# determine models
|
230
|
+
index_models = {}
|
231
|
+
grouped_hits.each do |index, _|
|
232
|
+
models =
|
233
|
+
if @klass
|
234
|
+
[@klass]
|
235
|
+
else
|
236
|
+
index_alias = index.split("_")[0..-2].join("_")
|
237
|
+
Array((options[:index_mapping] || {})[index_alias])
|
238
|
+
end
|
239
|
+
raise Searchkick::Error, "Unknown model for index: #{index}" unless models.any?
|
240
|
+
index_models[index] = models
|
241
|
+
end
|
242
|
+
|
243
|
+
# fetch results
|
244
|
+
results = {}
|
245
|
+
grouped_hits.each do |index, index_hits|
|
246
|
+
results[index] = {}
|
247
|
+
index_models[index].each do |model|
|
248
|
+
results[index].merge!(results_query(model, index_hits).to_a.index_by { |r| r.id.to_s })
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# sort
|
253
|
+
results =
|
254
|
+
hits.map do |hit|
|
255
|
+
result = results[hit["_index"]][hit["_id"].to_s]
|
256
|
+
if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
|
257
|
+
if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
|
258
|
+
highlights = hit_highlights(hit)
|
259
|
+
result.define_singleton_method(:search_highlights) do
|
260
|
+
highlights
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
[result, hit]
|
265
|
+
end.select do |result, hit|
|
266
|
+
unless result
|
267
|
+
models = index_models[hit["_index"]]
|
268
|
+
missing_records << {
|
269
|
+
id: hit["_id"],
|
270
|
+
# may be multiple models for inheritance with child models
|
271
|
+
# not ideal to return different types
|
272
|
+
# but this situation shouldn't be common
|
273
|
+
model: models.size == 1 ? models.first : models
|
274
|
+
}
|
275
|
+
end
|
276
|
+
result
|
277
|
+
end
|
278
|
+
else
|
279
|
+
results =
|
280
|
+
hits.map do |hit|
|
281
|
+
result =
|
282
|
+
if hit["_source"]
|
283
|
+
hit.except("_source").merge(hit["_source"])
|
284
|
+
elsif hit["fields"]
|
285
|
+
hit.except("fields").merge(hit["fields"])
|
286
|
+
else
|
287
|
+
hit
|
288
|
+
end
|
289
|
+
|
290
|
+
if hit["highlight"] || options[:highlight]
|
291
|
+
highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
|
292
|
+
options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
|
293
|
+
result["highlighted_#{k}"] ||= (highlight[k] || result[k])
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
result["id"] ||= result["_id"] # needed for legacy reasons
|
298
|
+
[HashWrapper.new(result), hit]
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
[results, missing_records]
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
271
306
|
def results_query(records, hits)
|
272
307
|
ids = hits.map { |hit| hit["_id"] }
|
273
308
|
if options[:includes] || options[:model_includes]
|
data/lib/searchkick/version.rb
CHANGED
data/lib/tasks/searchkick.rake
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
namespace :searchkick do
|
2
|
-
desc "reindex model"
|
2
|
+
desc "reindex a model (specify CLASS)"
|
3
3
|
task reindex: :environment do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
4
|
+
class_name = ENV["CLASS"]
|
5
|
+
abort "USAGE: rake searchkick:reindex CLASS=Product" unless class_name
|
6
|
+
|
7
|
+
model = class_name.safe_constantize
|
8
|
+
abort "Could not find class: #{class_name}" unless model
|
9
|
+
abort "#{class_name} is not a searchkick model" unless Searchkick.models.include?(model)
|
10
|
+
|
11
|
+
puts "Reindexing #{model.name}..."
|
12
|
+
model.reindex
|
13
|
+
puts "Reindex successful"
|
14
14
|
end
|
15
15
|
|
16
16
|
namespace :reindex do
|
17
17
|
desc "reindex all models"
|
18
18
|
task all: :environment do
|
19
|
+
# eager load models to populate Searchkick.models
|
19
20
|
if Rails.respond_to?(:autoloaders) && Rails.autoloaders.zeitwerk_enabled?
|
20
21
|
# fix for https://github.com/rails/rails/issues/37006
|
21
22
|
Zeitwerk::Loader.eager_load_all
|
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: 4.2
|
4
|
+
version: 4.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -52,56 +52,13 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
|
56
|
-
name: bundler
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: minitest
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rake
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
description:
|
55
|
+
description:
|
98
56
|
email: andrew@chartkick.com
|
99
57
|
executables: []
|
100
58
|
extensions: []
|
101
59
|
extra_rdoc_files: []
|
102
60
|
files:
|
103
61
|
- CHANGELOG.md
|
104
|
-
- CONTRIBUTING.md
|
105
62
|
- LICENSE.txt
|
106
63
|
- README.md
|
107
64
|
- lib/searchkick.rb
|
@@ -130,7 +87,7 @@ homepage: https://github.com/ankane/searchkick
|
|
130
87
|
licenses:
|
131
88
|
- MIT
|
132
89
|
metadata: {}
|
133
|
-
post_install_message:
|
90
|
+
post_install_message:
|
134
91
|
rdoc_options: []
|
135
92
|
require_paths:
|
136
93
|
- lib
|
@@ -145,8 +102,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
102
|
- !ruby/object:Gem::Version
|
146
103
|
version: '0'
|
147
104
|
requirements: []
|
148
|
-
rubygems_version: 3.1.
|
149
|
-
signing_key:
|
105
|
+
rubygems_version: 3.1.4
|
106
|
+
signing_key:
|
150
107
|
specification_version: 4
|
151
108
|
summary: Intelligent search made easy with Rails and Elasticsearch
|
152
109
|
test_files: []
|
data/CONTRIBUTING.md
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
# Contributing
|
2
|
-
|
3
|
-
First, thanks for wanting to contribute. You’re awesome! :heart:
|
4
|
-
|
5
|
-
## Help
|
6
|
-
|
7
|
-
We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
|
8
|
-
|
9
|
-
All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
|
10
|
-
|
11
|
-
## Bugs
|
12
|
-
|
13
|
-
Think you’ve discovered a bug?
|
14
|
-
|
15
|
-
1. Search existing issues to see if it’s been reported.
|
16
|
-
2. Try the `master` branch to make sure it hasn’t been fixed.
|
17
|
-
|
18
|
-
```rb
|
19
|
-
gem "searchkick", github: "ankane/searchkick"
|
20
|
-
```
|
21
|
-
|
22
|
-
3. Try the `debug` option when searching. This can reveal useful info.
|
23
|
-
|
24
|
-
```ruby
|
25
|
-
Product.search("something", debug: true)
|
26
|
-
```
|
27
|
-
|
28
|
-
If the above steps don’t help, create an issue.
|
29
|
-
|
30
|
-
- Recreate the problem by forking [this gist](https://gist.github.com/ankane/f80b0923d9ae2c077f41997f7b704e5c). Include a link to your gist and the output in the issue.
|
31
|
-
- For exceptions, include the complete backtrace.
|
32
|
-
|
33
|
-
## New Features
|
34
|
-
|
35
|
-
If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
|
36
|
-
|
37
|
-
## Pull Requests
|
38
|
-
|
39
|
-
Fork the project and create a pull request. A few tips:
|
40
|
-
|
41
|
-
- Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
|
42
|
-
- Follow the existing style. The code should read like it’s written by a single person.
|
43
|
-
- Add one or more tests if possible. Make sure existing tests pass with:
|
44
|
-
|
45
|
-
```sh
|
46
|
-
bundle exec rake test
|
47
|
-
```
|
48
|
-
|
49
|
-
Feel free to open an issue to get feedback on your idea before spending too much time on it.
|
50
|
-
|
51
|
-
---
|
52
|
-
|
53
|
-
This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.
|