searchkick 4.2.1 → 4.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- scoped = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
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: scoped, **options)
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
 
@@ -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 = {must: query}
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
- regex = Regexp.escape(op_value).gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
957
- filters << {regexp: {field => {value: regex}}}
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
- {term: {field => value}}
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
 
@@ -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 options[:load]
25
- # results can have different types
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]
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.2.1"
2
+ VERSION = "4.4.2"
3
3
  end
@@ -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
- if ENV["CLASS"]
5
- klass = ENV["CLASS"].constantize rescue nil
6
- if klass
7
- klass.reindex
8
- else
9
- abort "Could not find class: #{ENV['CLASS']}"
10
- end
11
- else
12
- abort "USAGE: rake searchkick:reindex CLASS=Product"
13
- end
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.1
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-01-28 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
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.2
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: []
@@ -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.