searchkick 4.1.1 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79cd2e4afe02598dbdc951ed222c98ab51c8f6b0580c7beb70d7f6f5e9223c8f
4
- data.tar.gz: 804ae10724df4b1f72fc93f157e185cd6acbc734952c64e1c596d20f26360ce9
3
+ metadata.gz: '0218ffe2be1e6fcebea56dfa7c8d112e2f43580d69b513d064f48c4905c51934'
4
+ data.tar.gz: 79b21178f3db14d84179aa3db7cbf5dae4d7adfa98a9a72e7f92991c9bff2ce0
5
5
  SHA512:
6
- metadata.gz: 732852787481b5aaf69edda3850c50846d5d07a55ba73d41dbee864988f04c5480c8c1535af6b82374982da4f95de5735f36433e6a37b46a41053ecc975fc9ef
7
- data.tar.gz: 6d4408714af66540fc2868d2d9d4bd7b72d468150f5a29c9200d4eb349ee89f3155c98536742eb0015f905de644345e832af3a752613007d424cbb2c353051c2
6
+ metadata.gz: ce5ec190b6d754210ce88f48197cc0c08a32b6d33cc9a904385613ae3ec5bd7490a1d6c711bcf6cd3645636d64fe0c7e91bc23e009a32416194c8fbbe0119956
7
+ data.tar.gz: 3b306480292cfbcdc5854cc491f3700ecfd88715d37f879aa5715f0782ac86f380aa4828bec2866d2ea74186e445b9d4380d4696c8c2abcf1404207afa8ede18
@@ -1,3 +1,29 @@
1
+ ## 4.4.0 (2020-06-17)
2
+
3
+ - Added support for reloadable, multi-word, search time synonyms
4
+ - Fixed another deprecation warning in Ruby 2.7
5
+
6
+ ## 4.3.1 (2020-05-13)
7
+
8
+ - Fixed error with `exclude` in certain cases for Elasticsearch 7.7
9
+
10
+ ## 4.3.0 (2020-02-19)
11
+
12
+ - Fixed `like` queries with `"` character
13
+ - Better error when invalid parameters passed to `where`
14
+
15
+ ## 4.2.1 (2020-01-27)
16
+
17
+ - Fixed deprecation warnings with Elasticsearch
18
+ - Fixed deprecation warnings in Ruby 2.7
19
+
20
+ ## 4.2.0 (2019-12-18)
21
+
22
+ - Added safety check for multiple `Model.reindex`
23
+ - Added `deep_paging` option
24
+ - Added request parameters to search notifications and curl representation
25
+ - Removed curl from search notifications to prevent confusion
26
+
1
27
  ## 4.1.1 (2019-11-19)
2
28
 
3
29
  - Added `chinese2` and `korean2` languages
@@ -4,7 +4,7 @@ First, thanks for wanting to contribute. You’re awesome! :heart:
4
4
 
5
5
  ## Help
6
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/).
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/questions/tagged/searchkick).
8
8
 
9
9
  All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
10
10
 
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2019 Andrew Kane
1
+ Copyright (c) 2013-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -4,12 +4,6 @@
4
4
 
5
5
  **Searchkick learns what your users are looking for.** As more people search, it gets smarter and the results get better. It’s friendly for developers - and magical for your users.
6
6
 
7
- ---
8
-
9
- [Searchkick Pro](https://searchkick.org/pro?utm_source=readme) is now available!
10
-
11
- ---
12
-
13
7
  Searchkick handles:
14
8
 
15
9
  - stemming - `tomatoes` matches `tomato`
@@ -49,7 +43,7 @@ Plus:
49
43
 
50
44
  ## Getting Started
51
45
 
52
- [Install Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html). For Homebrew, use:
46
+ [Install Elasticsearch](https://www.elastic.co/downloads/elasticsearch). For Homebrew, use:
53
47
 
54
48
  ```sh
55
49
  brew install elasticsearch
@@ -137,15 +131,13 @@ Limit / offset
137
131
  limit: 20, offset: 40
138
132
  ```
139
133
 
140
- **Note:** By default, Elasticsearch [limits pagination](#deep-pagination) to the first 10,000 results for performance
141
-
142
134
  Select
143
135
 
144
136
  ```ruby
145
137
  select: [:name]
146
138
  ```
147
139
 
148
- [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html)
140
+ [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-source-filtering)
149
141
 
150
142
  ### Results
151
143
 
@@ -182,6 +174,8 @@ Get the full response from Elasticsearch
182
174
  results.response
183
175
  ```
184
176
 
177
+ **Note:** By default, Elasticsearch [limits paging](#deep-paging-master) to the first 10,000 results for performance. With Elasticsearch 7, this applies to the total count as well.
178
+
185
179
  ### Boosting
186
180
 
187
181
  Boost important fields
@@ -330,29 +324,52 @@ A few languages require plugins:
330
324
 
331
325
  ```ruby
332
326
  class Product < ApplicationRecord
333
- searchkick synonyms: [["pop", "soda"], ["burger", "hamburger"]]
327
+ searchkick search_synonyms: [["pop", "soda"], ["burger", "hamburger"]]
334
328
  end
335
329
  ```
336
330
 
337
- Call `Product.reindex` after changing synonyms.
331
+ Call `Product.reindex` after changing synonyms. Synonyms are applied at search time before stemming, and can be a single word or multiple words.
338
332
 
339
- Synonyms cannot be multiple words at the moment.
333
+ For directional synonyms, use:
340
334
 
341
- To read synonyms from a file, use:
335
+ ```ruby
336
+ synonyms: ["lightbulb => halogenlamp"]
337
+ ```
338
+
339
+ ### Dynamic Synonyms
340
+
341
+ The above approach works well when your synonym list is static, but in practice, this is often not the case. When you analyze search conversions, you often want to add new synonyms without a full reindex.
342
+
343
+ #### Elasticsearch 7.3+
344
+
345
+ For Elasticsearch 7.3+, we recommend placing synonyms in a file on the Elasticsearch server (in the `config` directory). This allows you to reload synonyms without reindexing.
346
+
347
+ ```txt
348
+ pop, soda
349
+ burger, hamburger
350
+ ```
351
+
352
+ Then use:
342
353
 
343
354
  ```ruby
344
- synonyms: -> { CSV.read("/some/path/synonyms.csv") }
355
+ search_synonyms: "synonyms.txt"
345
356
  ```
346
357
 
347
- For directional synonyms, use:
358
+ Add [elasticsearch-xpack](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-xpack) to your Gemfile:
348
359
 
349
360
  ```ruby
350
- synonyms: ["lightbulb => halogenlamp"]
361
+ gem 'elasticsearch-xpack', '>= 7.8.0.pre'
351
362
  ```
352
363
 
353
- ### Tags and Dynamic Synonyms
364
+ And use:
365
+
366
+ ```ruby
367
+ Product.search_index.reload_synonyms
368
+ ```
354
369
 
355
- The above approach works well when your synonym list is static, but in practice, this is often not the case. When you analyze search conversions, you often want to add new synonyms or tags without a full reindex. You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
370
+ #### Elasticsearch < 7.3
371
+
372
+ You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
356
373
 
357
374
  ```ruby
358
375
  class Product < ApplicationRecord
@@ -554,7 +571,7 @@ Searchkick.callbacks(false) do
554
571
  end
555
572
  ```
556
573
 
557
- #### Associations
574
+ ### Associations
558
575
 
559
576
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
560
577
 
@@ -770,8 +787,6 @@ Order
770
787
  Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
771
788
  ```
772
789
 
773
- **Note:** Use `_term` instead of `_key` in Elasticsearch 5
774
-
775
790
  [All of these options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-order)
776
791
 
777
792
  Ranges
@@ -1040,7 +1055,7 @@ Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This de
1040
1055
 
1041
1056
  ### Heroku
1042
1057
 
1043
- Choose an add-on: [Bonsai](https://elements.heroku.com/addons/bonsai) or [Elastic Cloud](https://elements.heroku.com/addons/foundelasticsearch). [SearchBox](https://elements.heroku.com/addons/searchbox) does not work at the moment.
1058
+ Choose an add-on: [Bonsai](https://elements.heroku.com/addons/bonsai), [SearchBox](https://elements.heroku.com/addons/searchbox), or [Elastic Cloud](https://elements.heroku.com/addons/foundelasticsearch).
1044
1059
 
1045
1060
  For Bonsai:
1046
1061
 
@@ -1049,6 +1064,13 @@ heroku addons:create bonsai
1049
1064
  heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1050
1065
  ```
1051
1066
 
1067
+ For SearchBox:
1068
+
1069
+ ```sh
1070
+ heroku addons:create searchbox:starter
1071
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
1072
+ ```
1073
+
1052
1074
  For Elastic Cloud (previously Found):
1053
1075
 
1054
1076
  ```sh
@@ -1518,20 +1540,20 @@ end
1518
1540
  products.clear_scroll
1519
1541
  ```
1520
1542
 
1521
- ## Deep Pagination
1543
+ ## Deep Paging
1522
1544
 
1523
- By default, Elasticsearch limits pagination to the first 10,000 results [for performance](https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html). We don’t recommend changing this, but if you need to, you can use:
1545
+ By default, Elasticsearch limits paging to the first 10,000 results. [Here’s why](https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html). We don’t recommend changing this, but if you really need all results, you can use:
1524
1546
 
1525
1547
  ```ruby
1526
1548
  class Product < ApplicationRecord
1527
- searchkick settings: {index: {max_result_window: 1000000000}}
1549
+ searchkick deep_paging: true
1528
1550
  end
1529
1551
  ```
1530
1552
 
1531
- And search with:
1553
+ If you just need an accurate total count with Elasticsearch 7, you can instead use:
1532
1554
 
1533
1555
  ```ruby
1534
- Product.search("pears", limit: 1000000000, body_options: {track_total_hits: true})
1556
+ Product.search("pears", body_options: {track_total_hits: true})
1535
1557
  ```
1536
1558
 
1537
1559
  ## Nested Data
@@ -1813,6 +1835,44 @@ Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1813
1835
 
1814
1836
  For performance, only enable Searchkick callbacks for the tests that need it.
1815
1837
 
1838
+ ### Parallel Tests
1839
+
1840
+ Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
1841
+
1842
+ ```ruby
1843
+ class ActiveSupport::TestCase
1844
+ parallelize_setup do |worker|
1845
+ Searchkick.index_suffix = worker
1846
+
1847
+ # reindex models
1848
+ Product.reindex
1849
+
1850
+ # and disable callbacks
1851
+ Searchkick.disable_callbacks
1852
+ end
1853
+ end
1854
+ ```
1855
+
1856
+ And use:
1857
+
1858
+ ```ruby
1859
+ class ProductTest < ActiveSupport::TestCase
1860
+ def setup
1861
+ Searchkick.enable_callbacks
1862
+ end
1863
+
1864
+ def teardown
1865
+ Searchkick.disable_callbacks
1866
+ end
1867
+
1868
+ def test_search
1869
+ Product.create!(name: "Apple")
1870
+ Product.search_index.refresh
1871
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1872
+ end
1873
+ end
1874
+ ```
1875
+
1816
1876
  ### Minitest
1817
1877
 
1818
1878
  Add to your `test/test_helper.rb`:
@@ -1860,7 +1920,7 @@ RSpec.configure do |config|
1860
1920
  end
1861
1921
 
1862
1922
  config.around(:each, search: true) do |example|
1863
- Searchkick.callbacks(true) do
1923
+ Searchkick.callbacks(nil) do
1864
1924
  example.run
1865
1925
  end
1866
1926
  end
@@ -1902,14 +1962,6 @@ end
1902
1962
  FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1903
1963
  ```
1904
1964
 
1905
- ### Parallel Tests
1906
-
1907
- Set:
1908
-
1909
- ```ruby
1910
- Searchkick.index_suffix = ENV["TEST_ENV_NUMBER"]
1911
- ```
1912
-
1913
1965
  ## Multi-Tenancy
1914
1966
 
1915
1967
  Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenancy-with-searchkick/) on the [Apartment](https://github.com/influitive/apartment) gem. Follow a similar pattern if you use another gem.
@@ -1985,13 +2037,11 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
1985
2037
  - Write, clarify, or fix documentation
1986
2038
  - Suggest or add new features
1987
2039
 
1988
- If you’re looking for ideas, [try here](https://github.com/ankane/searchkick/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
1989
-
1990
- To get started with development and testing:
2040
+ To get started with development:
1991
2041
 
1992
2042
  ```sh
1993
2043
  git clone https://github.com/ankane/searchkick.git
1994
2044
  cd searchkick
1995
2045
  bundle install
1996
- rake test
2046
+ bundle exec rake test
1997
2047
  ```
@@ -1,8 +1,10 @@
1
+ # dependencies
1
2
  require "active_support"
2
3
  require "active_support/core_ext/hash/deep_merge"
3
4
  require "elasticsearch"
4
5
  require "hashie"
5
6
 
7
+ # modules
6
8
  require "searchkick/bulk_indexer"
7
9
  require "searchkick/index"
8
10
  require "searchkick/indexer"
@@ -17,6 +19,7 @@ require "searchkick/record_indexer"
17
19
  require "searchkick/results"
18
20
  require "searchkick/version"
19
21
 
22
+ # integrations
20
23
  require "searchkick/railtie" if defined?(Rails)
21
24
  require "searchkick/logging" if defined?(ActiveSupport::Notifications)
22
25
 
@@ -27,6 +30,7 @@ module Searchkick
27
30
  autoload :ProcessQueueJob, "searchkick/process_queue_job"
28
31
  autoload :ReindexV2Job, "searchkick/reindex_v2_job"
29
32
 
33
+ # errors
30
34
  class Error < StandardError; end
31
35
  class MissingIndexError < Error; end
32
36
  class UnsupportedVersionError < Error; end
@@ -112,7 +116,7 @@ module Searchkick
112
116
  end
113
117
 
114
118
  options = options.merge(block: block) if block
115
- query = Searchkick::Query.new(klass, term, options)
119
+ query = Searchkick::Query.new(klass, term, **options)
116
120
  if options[:execute] == false
117
121
  query
118
122
  else
@@ -142,7 +146,7 @@ module Searchkick
142
146
  end
143
147
  end
144
148
 
145
- def self.callbacks(value)
149
+ def self.callbacks(value = nil)
146
150
  if block_given?
147
151
  previous_value = callbacks_value
148
152
  begin
@@ -249,6 +253,18 @@ module Searchkick
249
253
  }
250
254
  end
251
255
  end
256
+
257
+ # private
258
+ # methods are forwarded to base class
259
+ # this check to see if scope exists on that class
260
+ # it's a bit tricky, but this seems to work
261
+ def self.relation?(klass)
262
+ if klass.respond_to?(:current_scope)
263
+ !klass.current_scope.nil?
264
+ elsif defined?(Mongoid::Threaded)
265
+ !Mongoid::Threaded.current_scope(klass).nil?
266
+ end
267
+ end
252
268
  end
253
269
 
254
270
  # TODO find better ActiveModel hook
@@ -141,7 +141,7 @@ module Searchkick
141
141
 
142
142
  def bulk_reindex_job(scope, batch_id, options)
143
143
  Searchkick.with_redis { |r| r.sadd(batches_key, batch_id) }
144
- Searchkick::BulkReindexJob.perform_later({
144
+ Searchkick::BulkReindexJob.perform_later(**{
145
145
  class_name: scope.searchkick_options[:class_name],
146
146
  index_name: index.name,
147
147
  batch_id: batch_id
@@ -47,7 +47,7 @@ module Searchkick
47
47
  end
48
48
 
49
49
  def refresh_interval
50
- settings.values.first["settings"]["index"]["refresh_interval"]
50
+ index_settings["refresh_interval"]
51
51
  end
52
52
 
53
53
  def update_settings(settings)
@@ -174,6 +174,13 @@ module Searchkick
174
174
  Searchkick.search(like_text, model: record.class, **options)
175
175
  end
176
176
 
177
+ def reload_synonyms
178
+ require "elasticsearch/xpack"
179
+ raise Error, "Requires Elasticsearch 7.3+" if Searchkick.server_below?("7.3.0")
180
+ raise Error, "Requires elasticsearch-xpack 7.8+" unless client.xpack.respond_to?(:indices)
181
+ client.xpack.indices.reload_search_analyzers(index: name)
182
+ end
183
+
177
184
  # queue
178
185
 
179
186
  def reindex_queue
@@ -184,13 +191,20 @@ module Searchkick
184
191
 
185
192
  def reindex(relation, method_name, scoped:, full: false, scope: nil, **options)
186
193
  refresh = options.fetch(:refresh, !scoped)
194
+ options.delete(:refresh)
187
195
 
188
196
  if method_name
197
+ # TODO throw ArgumentError
198
+ Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
199
+
189
200
  # update
190
201
  import_scope(relation, method_name: method_name, scope: scope)
191
202
  self.refresh if refresh
192
203
  true
193
204
  elsif scoped && !full
205
+ # TODO throw ArgumentError
206
+ Searchkick.warn("unsupported keywords: #{options.keys.map(&:inspect).join(", ")}") if options.any?
207
+
194
208
  # reindex association
195
209
  import_scope(relation, scope: scope)
196
210
  self.refresh if refresh
@@ -249,6 +263,11 @@ module Searchkick
249
263
  end
250
264
  end
251
265
 
266
+ # private
267
+ def uuid
268
+ index_settings["uuid"]
269
+ end
270
+
252
271
  protected
253
272
 
254
273
  def client
@@ -259,6 +278,10 @@ module Searchkick
259
278
  @bulk_indexer ||= BulkIndexer.new(self)
260
279
  end
261
280
 
281
+ def index_settings
282
+ settings.values.first["settings"]["index"]
283
+ end
284
+
262
285
  def import_before_promotion(index, relation, **import_options)
263
286
  index.import_scope(relation, **import_options)
264
287
  end
@@ -285,6 +308,8 @@ module Searchkick
285
308
  scope: scope
286
309
  }
287
310
 
311
+ uuid = index.uuid
312
+
288
313
  # check if alias exists
289
314
  alias_exists = alias_exists?
290
315
  if alias_exists
@@ -292,6 +317,7 @@ module Searchkick
292
317
 
293
318
  # get existing indices to remove
294
319
  unless async
320
+ check_uuid(uuid, index.uuid)
295
321
  promote(index.name, update_refresh_interval: !refresh_interval.nil?)
296
322
  clean_indices unless retain
297
323
  end
@@ -316,6 +342,7 @@ module Searchkick
316
342
  # already promoted if alias didn't exist
317
343
  if alias_exists
318
344
  puts "Jobs complete. Promoting..."
345
+ check_uuid(uuid, index.uuid)
319
346
  promote(index.name, update_refresh_interval: !refresh_interval.nil?)
320
347
  end
321
348
  clean_indices unless retain
@@ -334,5 +361,15 @@ module Searchkick
334
361
 
335
362
  raise e
336
363
  end
364
+
365
+ # safety check
366
+ # still a chance for race condition since its called before promotion
367
+ # ideal is for user to disable automatic index creation
368
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
369
+ def check_uuid(old_uuid, new_uuid)
370
+ if old_uuid != new_uuid
371
+ raise Searchkick::Error, "Safety check failed - only run one Model.reindex per model at a time"
372
+ end
373
+ end
337
374
  end
338
375
  end
@@ -7,6 +7,7 @@ module Searchkick
7
7
 
8
8
  below62 = Searchkick.server_below?("6.2.0")
9
9
  below70 = Searchkick.server_below?("7.0.0")
10
+ below73 = Searchkick.server_below?("7.3.0")
10
11
 
11
12
  if below70
12
13
  index_type = options[:_type]
@@ -27,9 +28,6 @@ module Searchkick
27
28
  default_analyzer = :searchkick_index
28
29
  keyword_mapping = {type: "keyword"}
29
30
 
30
- index_true_value = true
31
- index_false_value = false
32
-
33
31
  keyword_mapping[:ignore_above] = options[:ignore_above] || 30000
34
32
 
35
33
  settings = {
@@ -126,12 +124,12 @@ module Searchkick
126
124
  max_shingle_size: 5
127
125
  },
128
126
  searchkick_edge_ngram: {
129
- type: "edgeNGram",
127
+ type: "edge_ngram",
130
128
  min_gram: 1,
131
129
  max_gram: 50
132
130
  },
133
131
  searchkick_ngram: {
134
- type: "nGram",
132
+ type: "ngram",
135
133
  min_gram: 1,
136
134
  max_gram: 50
137
135
  },
@@ -288,9 +286,7 @@ module Searchkick
288
286
 
289
287
  # synonyms
290
288
  synonyms = options[:synonyms] || []
291
-
292
289
  synonyms = synonyms.call if synonyms.respond_to?(:call)
293
-
294
290
  if synonyms.any?
295
291
  settings[:analysis][:filter][:searchkick_synonym] = {
296
292
  type: "synonym",
@@ -313,6 +309,29 @@ module Searchkick
313
309
  end
314
310
  end
315
311
 
312
+ search_synonyms = options[:search_synonyms] || []
313
+ search_synonyms = search_synonyms.call if search_synonyms.respond_to?(:call)
314
+ if search_synonyms.is_a?(String) || search_synonyms.any?
315
+ if search_synonyms.is_a?(String)
316
+ synonym_graph = {
317
+ type: "synonym_graph",
318
+ synonyms_path: search_synonyms
319
+ }
320
+ synonym_graph[:updateable] = true unless below73
321
+ else
322
+ synonym_graph = {
323
+ type: "synonym_graph",
324
+ # TODO confirm this is correct
325
+ synonyms: search_synonyms.select { |s| s.size > 1 }.map { |s| s.is_a?(Array) ? s.join(",") : s }.map(&:downcase)
326
+ }
327
+ end
328
+ settings[:analysis][:filter][:searchkick_synonym_graph] = synonym_graph
329
+
330
+ [:searchkick_search2, :searchkick_word_search].each do |analyzer|
331
+ settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
332
+ end
333
+ end
334
+
316
335
  if options[:wordnet]
317
336
  settings[:analysis][:filter][:searchkick_wordnet] = {
318
337
  type: "synonym",
@@ -356,13 +375,13 @@ module Searchkick
356
375
 
357
376
  mapping_options[:searchable].delete("_all")
358
377
 
359
- analyzed_field_options = {type: default_type, index: index_true_value, analyzer: default_analyzer}
378
+ analyzed_field_options = {type: default_type, index: true, analyzer: default_analyzer}
360
379
 
361
380
  mapping_options.values.flatten.uniq.each do |field|
362
381
  fields = {}
363
382
 
364
383
  if options.key?(:filterable) && !mapping_options[:filterable].include?(field)
365
- fields[field] = {type: default_type, index: index_false_value}
384
+ fields[field] = {type: default_type, index: false}
366
385
  else
367
386
  fields[field] = keyword_mapping
368
387
  end
@@ -378,7 +397,7 @@ module Searchkick
378
397
 
379
398
  mapping_options.except(:highlight, :searchable, :filterable, :word).each do |type, f|
380
399
  if options[:match] == type || f.include?(field)
381
- fields[type] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{type}_index"}
400
+ fields[type] = {type: default_type, index: true, analyzer: "searchkick_#{type}_index"}
382
401
  end
383
402
  end
384
403
  end
@@ -418,12 +437,12 @@ module Searchkick
418
437
  }
419
438
 
420
439
  if options.key?(:filterable)
421
- dynamic_fields["{name}"] = {type: default_type, index: index_false_value}
440
+ dynamic_fields["{name}"] = {type: default_type, index: false}
422
441
  end
423
442
 
424
443
  unless options[:searchable]
425
444
  if options[:match] && options[:match] != :word
426
- dynamic_fields[options[:match]] = {type: default_type, index: index_true_value, analyzer: "searchkick_#{options[:match]}_index"}
445
+ dynamic_fields[options[:match]] = {type: default_type, index: true, analyzer: "searchkick_#{options[:match]}_index"}
427
446
  end
428
447
 
429
448
  if word
@@ -456,6 +475,13 @@ module Searchkick
456
475
  mappings = mappings.symbolize_keys.deep_merge(custom_mapping.symbolize_keys)
457
476
  end
458
477
 
478
+ if options[:deep_paging]
479
+ if !settings.dig(:index, :max_result_window) && !settings[:"index.max_result_window"]
480
+ settings[:index] ||= {}
481
+ settings[:index][:max_result_window] = 1_000_000_000
482
+ end
483
+ end
484
+
459
485
  {
460
486
  settings: settings,
461
487
  mappings: mappings
@@ -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,14 +162,17 @@ module Searchkick
162
162
 
163
163
  payload = event.payload
164
164
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
165
- type = payload[:query][:type]
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
- # no easy way to tell which host the client will use
169
- host = Searchkick.client.transport.hosts.first
170
- params = ["pretty"]
171
- params << "scroll=#{payload[:query][:scroll]}" if payload[:query][:scroll]
172
- debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?#{params.join('&')} -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}"
173
176
  end
174
177
 
175
178
  def request(event)
@@ -189,9 +192,7 @@ module Searchkick
189
192
  payload = event.payload
190
193
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
191
194
 
192
- # no easy way to tell which host the client will use
193
- host = Searchkick.client.transport.hosts.first
194
- 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]}"
195
196
  end
196
197
  end
197
198
 
@@ -3,9 +3,9 @@ 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,
8
+ :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
9
9
  :special_characters, :stem, :stem_conversions, :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?
@@ -41,7 +41,10 @@ module Searchkick
41
41
 
42
42
  class << self
43
43
  def searchkick_search(term = "*", **options, &block)
44
- Searchkick.search(term, {model: self}.merge(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
+
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
47
50
 
@@ -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
 
@@ -79,8 +83,9 @@ module Searchkick
79
83
  RecordIndexer.new(self).reindex(method_name, **options)
80
84
  end unless method_defined?(:reindex)
81
85
 
86
+ # TODO switch to keyword arguments
82
87
  def similar(options = {})
83
- self.class.searchkick_index.similar_record(self, options)
88
+ self.class.searchkick_index.similar_record(self, **options)
84
89
  end unless method_defined?(:similar)
85
90
 
86
91
  def search_data
@@ -106,12 +106,15 @@ module Searchkick
106
106
  query = params
107
107
  type = query[:type]
108
108
  index = query[:index].is_a?(Array) ? query[:index].join(",") : query[:index]
109
+ request_params = query.except(:index, :type, :body)
109
110
 
110
111
  # no easy way to tell which host the client will use
111
112
  host = Searchkick.client.transport.hosts.first
112
113
  credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
113
114
  params = ["pretty"]
114
- params << "scroll=#{options[:scroll]}" if options[:scroll]
115
+ request_params.each do |k, v|
116
+ params << "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
117
+ end
115
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}'"
116
119
  end
117
120
 
@@ -232,7 +235,9 @@ module Searchkick
232
235
 
233
236
  # pagination
234
237
  page = [options[:page].to_i, 1].max
235
- per_page = (options[:limit] || options[:per_page] || 10_000).to_i
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
236
241
  padding = [options[:padding].to_i, 0].max
237
242
  offset = options[:offset] || (page - 1) * per_page + padding
238
243
  scroll = options[:scroll]
@@ -345,7 +350,7 @@ module Searchkick
345
350
  field_misspellings = misspellings && (!misspellings_fields || misspellings_fields.include?(base_field(field)))
346
351
 
347
352
  if field == "_all" || field.end_with?(".analyzed")
348
- 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?)
349
354
  qs << shared_options.merge(analyzer: "searchkick_search")
350
355
 
351
356
  # searchkick_search and searchkick_search2 are the same for ukrainian
@@ -433,19 +438,20 @@ module Searchkick
433
438
 
434
439
  models = Array(options[:models])
435
440
  if models.any? { |m| m != m.searchkick_klass }
436
- Searchkick.warn("Passing child models to models option throws off hits and pagination - use type option instead")
437
-
438
- # TODO uncomment once aliases are supported with _index
439
- # should be ES 7.5
441
+ # aliases are not supported with _index in ES below 7.5
440
442
  # see https://github.com/elastic/elasticsearch/pull/46640
441
- # index_type_or =
442
- # models.map do |m|
443
- # v = {_index: m.searchkick_index.name}
444
- # v[:type] = m.searchkick_index.klass_document_type(m, true) if m != m.searchkick_klass
445
- # v
446
- # end
447
-
448
- # where[:or] = Array(where[:or]) + [index_type_or]
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
449
455
  end
450
456
 
451
457
  # start everything as efficient filters
@@ -516,6 +522,10 @@ module Searchkick
516
522
  # routing
517
523
  @routing = options[:routing] if options[:routing]
518
524
 
525
+ if track_total_hits?
526
+ payload[:track_total_hits] = true
527
+ end
528
+
519
529
  # merge more body options
520
530
  payload = payload.deep_merge(options[:body_options]) if options[:body_options]
521
531
 
@@ -564,7 +574,8 @@ module Searchkick
564
574
 
565
575
  def build_query(query, filters, should, must_not, custom_filters, multiply_filters)
566
576
  if filters.any? || must_not.any? || should.any?
567
- bool = {must: query}
577
+ bool = {}
578
+ bool[:must] = query if query
568
579
  bool[:filter] = filters if filters.any? # where
569
580
  bool[:must_not] = must_not if must_not.any? # exclude
570
581
  bool[:should] = should if should.any? # conversions
@@ -861,6 +872,11 @@ module Searchkick
861
872
  end
862
873
 
863
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
+
864
880
  filters = []
865
881
  (where || {}).each do |field, value|
866
882
  field = :_id if field.to_s == "id"
@@ -943,10 +959,17 @@ module Searchkick
943
959
  # % matches zero or more characters
944
960
  # _ matches one character
945
961
  # \ is escape character
946
- regex = Regexp.escape(op_value).gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
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("\\_", "_")
947
970
  filters << {regexp: {field => {value: regex}}}
948
971
  when :prefix
949
- filters << {prefix: {field => op_value}}
972
+ filters << {prefix: {field => {value: op_value}}}
950
973
  when :regexp # support for regexp queries without using a regexp ruby object
951
974
  filters << {regexp: {field => {value: op_value}}}
952
975
  when :not, :_not # not equal
@@ -1026,7 +1049,16 @@ module Searchkick
1026
1049
 
1027
1050
  {regexp: {field => {value: source, flags: "NONE"}}}
1028
1051
  else
1029
- {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}}}
1030
1062
  end
1031
1063
  end
1032
1064
 
@@ -1090,6 +1122,14 @@ module Searchkick
1090
1122
  k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
1091
1123
  end
1092
1124
 
1125
+ def track_total_hits?
1126
+ (searchkick_options[:deep_paging] && !below70?) || body_options[:track_total_hits]
1127
+ end
1128
+
1129
+ def body_options
1130
+ options[:body_options] || {}
1131
+ end
1132
+
1093
1133
  def below61?
1094
1134
  Searchkick.server_below?("6.1.0")
1095
1135
  end
@@ -1097,5 +1137,13 @@ module Searchkick
1097
1137
  def below70?
1098
1138
  Searchkick.server_below?("7.0.0")
1099
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
1100
1148
  end
1101
1149
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.1.1"
2
+ VERSION = "4.4.0"
3
3
  end
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.1.1
4
+ version: 4.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-20 00:00:00.000000000 Z
11
+ date: 2020-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: elasticsearch-xpack
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 7.8.0.pre
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 7.8.0.pre
97
111
  description:
98
112
  email: andrew@chartkick.com
99
113
  executables: []
@@ -145,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
159
  - !ruby/object:Gem::Version
146
160
  version: '0'
147
161
  requirements: []
148
- rubygems_version: 3.0.6
162
+ rubygems_version: 3.1.2
149
163
  signing_key:
150
164
  specification_version: 4
151
165
  summary: Intelligent search made easy with Rails and Elasticsearch