searchkick 4.4.1 → 4.6.3

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: ff49d45a4473f44b07e8f1654d47e9b0590fd71eb3c6567f62b2fbcdc1e837b9
4
- data.tar.gz: fd6ac4c931c7250787bc6f905c6c72b8412355e6b505bb2ef9d8cf5731d2939d
3
+ metadata.gz: 6937cc5456846216a4ea52c2274dfc43220e7f848dde276f0102ce48de3184a6
4
+ data.tar.gz: 0bfcc6a4e5a893629f9cac734498efba357dbd19e6641c21af7ea8e82f94f1ba
5
5
  SHA512:
6
- metadata.gz: 05630a20b302fd406e5545935861b6616db79e2dd12e2eb11fe82bb3733c8b21af1087b7ad8d0fd4bd93eb3a07d2bd67461815a293746a2d2c2d3f2148d7147b
7
- data.tar.gz: 571528f3967fd921e92dda6413313d29bd9048ee832e44bdb1a31a297178c3ee2f0b31dead3c573b297da96b676fdd74467bd7aa514c6a97f2a38b2500901057
6
+ metadata.gz: 0e7e41267464a8d32641edf6a578a6c65a97170f40e2f129effc8ff1da4144c30f00b88e52ddf6c51ffba366ec3cdfb263890e1539165ad47581c1723bf6b992
7
+ data.tar.gz: 49511b973a2fb7dce8d7952c0e5ccd676db357d2d965f46321e1b56bd5213f9f0c44f461f0010344f585e799dcbb13d9c4482772e20ab996462be86430c040cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,56 @@
1
+ ## 4.6.3 (2021-11-19)
2
+
3
+ - Added support for reloadable synonyms for OpenSearch
4
+ - Added experimental support for `opensearch` gem
5
+ - Removed `elasticsearch-xpack` dependency for reloadable synonyms
6
+
7
+ ## 4.6.2 (2021-11-15)
8
+
9
+ - Added support for beginless ranges to `where` option
10
+ - Fixed `like` and `ilike` with `+` character
11
+ - Fixed warning about accessing system indices when no model or index specified
12
+
13
+ ## 4.6.1 (2021-09-25)
14
+
15
+ - Added `ilike` operator for Elasticsearch 7.10+
16
+ - Fixed missing methods with `multi_search`
17
+
18
+ ## 4.6.0 (2021-08-22)
19
+
20
+ - Added support for case-insensitive regular expressions with Elasticsearch 7.10+
21
+ - Added support for `OPENSEARCH_URL`
22
+ - Fixed error with `debug` option
23
+
24
+ ## 4.5.2 (2021-08-05)
25
+
26
+ - Fixed error with reindex queue
27
+ - Fixed error with `model_name` method with multiple models
28
+ - Fixed error with `debug` option with elasticsearch-ruby 7.14
29
+
30
+ ## 4.5.1 (2021-08-03)
31
+
32
+ - Improved performance of reindex queue
33
+
34
+ ## 4.5.0 (2021-06-07)
35
+
36
+ - Added experimental support for OpenSearch
37
+ - Added support for synonyms in Japanese
38
+
39
+ ## 4.4.4 (2021-03-12)
40
+
41
+ - Fixed `too_long_frame_exception` with `scroll` method
42
+ - Fixed multi-word emoji tokenization
43
+
44
+ ## 4.4.3 (2021-02-25)
45
+
46
+ - Added support for Hunspell
47
+ - Fixed warning about accessing system indices
48
+
49
+ ## 4.4.2 (2020-11-23)
50
+
51
+ - Added `missing_records` method to results
52
+ - Fixed issue with `like` and special characters
53
+
1
54
  ## 4.4.1 (2020-06-24)
2
55
 
3
56
  - Added `stem_exclusion` and `stemmer_override` options
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2020 Andrew Kane
1
+ Copyright (c) 2013-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -20,13 +20,13 @@ Plus:
20
20
  - autocomplete
21
21
  - “Did you mean” suggestions
22
22
  - supports many languages
23
- - works with ActiveRecord, Mongoid, and NoBrainer
23
+ - works with Active Record, Mongoid, and NoBrainer
24
24
 
25
- :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
25
+ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Autosuggest](https://github.com/ankane/autosuggest) for query suggestions
26
26
 
27
- :speech_balloon: Get [handcrafted updates](https://chartkick.us7.list-manage.com/subscribe?u=952c861f99eb43084e0a49f98&id=6ea6541e8e&group[0][4]=true) for new features
27
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
28
28
 
29
- [![Build Status](https://travis-ci.org/ankane/searchkick.svg?branch=master)](https://travis-ci.org/ankane/searchkick)
29
+ [![Build Status](https://github.com/ankane/searchkick/workflows/build/badge.svg?branch=master)](https://github.com/ankane/searchkick/actions)
30
30
 
31
31
  ## Contents
32
32
 
@@ -45,11 +45,11 @@ Plus:
45
45
 
46
46
  ## Getting Started
47
47
 
48
- [Install Elasticsearch](https://www.elastic.co/downloads/elasticsearch). For Homebrew, use:
48
+ Install [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) or [OpenSearch](https://opensearch.org/downloads.html). For Homebrew, use:
49
49
 
50
50
  ```sh
51
- brew install elasticsearch
52
- brew services start elasticsearch
51
+ brew install elasticsearch # or opensearch
52
+ brew services start elasticsearch # or opensearch
53
53
  ```
54
54
 
55
55
  Add this line to your application’s Gemfile:
@@ -58,7 +58,7 @@ Add this line to your application’s Gemfile:
58
58
  gem 'searchkick'
59
59
  ```
60
60
 
61
- The latest version works with Elasticsearch 6 and 7. For Elasticsearch 5, use version 3.1.3 and [this readme](https://github.com/ankane/searchkick/blob/v3.1.3/README.md).
61
+ The latest version works with Elasticsearch 6 and 7 and OpenSearch 1. For Elasticsearch 5, use version 3.1.3 and [this readme](https://github.com/ankane/searchkick/blob/v3.1.3/README.md).
62
62
 
63
63
  Add searchkick to models you want to search.
64
64
 
@@ -103,19 +103,20 @@ Where
103
103
 
104
104
  ```ruby
105
105
  where: {
106
- expires_at: {gt: Time.now}, # lt, gte, lte also available
107
- orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
108
- aisle_id: [25, 30], # in
109
- store_id: {not: 2}, # not
110
- aisle_id: {not: [25, 30]}, # not in
111
- user_ids: {all: [1, 3]}, # all elements in array
112
- category: {like: "%frozen%"}, # like
113
- category: /frozen .+/, # regexp
114
- category: {prefix: "frozen"}, # prefix
115
- store_id: {exists: true}, # exists
106
+ expires_at: {gt: Time.now}, # lt, gte, lte also available
107
+ orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
108
+ aisle_id: [25, 30], # in
109
+ store_id: {not: 2}, # not
110
+ aisle_id: {not: [25, 30]}, # not in
111
+ user_ids: {all: [1, 3]}, # all elements in array
112
+ category: {like: "%frozen%"}, # like
113
+ category: {ilike: "%frozen%"}, # ilike
114
+ category: /frozen .+/, # regexp
115
+ category: {prefix: "frozen"}, # prefix
116
+ store_id: {exists: true}, # exists
116
117
  _or: [{in_stock: true}, {backordered: true}],
117
118
  _and: [{in_stock: true}, {backordered: true}],
118
- _not: {store_id: 1} # negate a condition
119
+ _not: {store_id: 1} # negate a condition
119
120
  }
120
121
  ```
121
122
 
@@ -125,7 +126,7 @@ Order
125
126
  order: {_score: :desc} # most relevant first - default
126
127
  ```
127
128
 
128
- [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html)
129
+ [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
129
130
 
130
131
  Limit / offset
131
132
 
@@ -139,7 +140,7 @@ Select
139
140
  select: [:name]
140
141
  ```
141
142
 
142
- [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-source-filtering)
143
+ [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering)
143
144
 
144
145
  ### Results
145
146
 
@@ -176,7 +177,7 @@ Get the full response from Elasticsearch
176
177
  results.response
177
178
  ```
178
179
 
179
- **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.
180
+ **Note:** By default, Elasticsearch [limits paging](#deep-paging) to the first 10,000 results for performance. With Elasticsearch 7, this applies to the total count as well.
180
181
 
181
182
  ### Boosting
182
183
 
@@ -209,7 +210,7 @@ boost_by_recency: {created_at: {scale: "7d", decay: 0.5}}
209
210
 
210
211
  You can also boost by:
211
212
 
212
- - [Conversions](#keep-getting-better)
213
+ - [Conversions](#intelligent-search)
213
214
  - [Distance](#boost-by-distance)
214
215
 
215
216
  ### Get Everything
@@ -311,7 +312,7 @@ class Product < ApplicationRecord
311
312
  end
312
313
  ```
313
314
 
314
- See the [list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html). A few languages require plugins:
315
+ See the [list of languages](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html#analysis-stemmer-tokenfilter-configure-parms). A few languages require plugins:
315
316
 
316
317
  - `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
317
318
  - `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-smartcn.html)
@@ -322,6 +323,14 @@ See the [list of stemmers](https://www.elastic.co/guide/en/elasticsearch/referen
322
323
  - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
323
324
  - `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
324
325
 
326
+ You can also use a Hunspell dictionary for stemming.
327
+
328
+ ```ruby
329
+ class Product < ApplicationRecord
330
+ searchkick stemmer: {type: "hunspell", locale: "en_US"}
331
+ end
332
+ ```
333
+
325
334
  Disable stemming with:
326
335
 
327
336
  ```ruby
@@ -366,9 +375,9 @@ search_synonyms: ["lightbulb => halogenlamp"]
366
375
 
367
376
  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.
368
377
 
369
- #### Elasticsearch 7.3+
378
+ #### Elasticsearch 7.3+ or OpenSearch
370
379
 
371
- 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.
380
+ For Elasticsearch 7.3+ or OpenSearch, we recommend placing synonyms in a file on the Elasticsearch or OpenSearch server (in the `config` directory). This allows you to reload synonyms without reindexing.
372
381
 
373
382
  ```txt
374
383
  pop, soda
@@ -378,16 +387,12 @@ burger, hamburger
378
387
  Then use:
379
388
 
380
389
  ```ruby
381
- search_synonyms: "synonyms.txt"
382
- ```
383
-
384
- Add [elasticsearch-xpack](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-xpack) to your Gemfile:
385
-
386
- ```ruby
387
- gem 'elasticsearch-xpack', '>= 7.8.0'
390
+ class Product < ApplicationRecord
391
+ searchkick search_synonyms: "synonyms.txt"
392
+ end
388
393
  ```
389
394
 
390
- And use:
395
+ And reload with:
391
396
 
392
397
  ```ruby
393
398
  Product.search_index.reload_synonyms
@@ -641,7 +646,7 @@ class Product < ApplicationRecord
641
646
  def search_data
642
647
  {
643
648
  name: name,
644
- conversions: searches.group(:query).uniq.count(:user_id)
649
+ conversions: searches.group(:query).distinct.count(:user_id)
645
650
  # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
646
651
  }
647
652
  end
@@ -891,7 +896,7 @@ Additional options can be specified for each field:
891
896
  Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
892
897
  ```
893
898
 
894
- You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_highlighted_fragments).
899
+ You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
895
900
 
896
901
  ## Similar Items
897
902
 
@@ -942,7 +947,7 @@ Boost results by distance - closer results are boosted more
942
947
  Restaurant.search "noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}}
943
948
  ```
944
949
 
945
- Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#_decay_functions)
950
+ Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
946
951
 
947
952
  ```ruby
948
953
  Restaurant.search "wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}}
@@ -1202,14 +1207,28 @@ end
1202
1207
  FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1203
1208
  ```
1204
1209
 
1210
+ ### GitHub Actions
1211
+
1212
+ Check out [setup-elasticsearch](https://github.com/ankane/setup-elasticsearch) for an easy way to install Elasticsearch:
1213
+
1214
+ ```yml
1215
+ - uses: ankane/setup-elasticsearch@v1
1216
+ ```
1217
+
1218
+ And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy way to install OpenSearch:
1219
+
1220
+ ```yml
1221
+ - uses: ankane/setup-opensearch@v1
1222
+ ```
1223
+
1205
1224
  ## Deployment
1206
1225
 
1207
1226
  Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
1208
1227
 
1209
1228
  - [Elastic Cloud](#elastic-cloud)
1210
1229
  - [Heroku](#heroku)
1211
- - [Amazon Elasticsearch Service](#amazon-elasticsearch-service)
1212
- - [Self-Hosted and Other](#other)
1230
+ - [Amazon OpenSearch Service](#amazon-opensearch-service)
1231
+ - [Self-Hosted and Other](#self-hosted-and-other)
1213
1232
 
1214
1233
  ### Elastic Cloud
1215
1234
 
@@ -1232,7 +1251,7 @@ Choose an add-on: [Bonsai](https://elements.heroku.com/addons/bonsai), [SearchBo
1232
1251
  For Bonsai:
1233
1252
 
1234
1253
  ```sh
1235
- heroku addons:create bonsai
1254
+ heroku addons:create bonsai # use --engine=opensearch for OpenSearch
1236
1255
  heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1237
1256
  ```
1238
1257
 
@@ -1268,7 +1287,7 @@ Then deploy and reindex:
1268
1287
  heroku run rake searchkick:reindex:all
1269
1288
  ```
1270
1289
 
1271
- ### Amazon Elasticsearch Service
1290
+ ### Amazon OpenSearch Service
1272
1291
 
1273
1292
  Create an initializer `config/initializers/elasticsearch.rb` with:
1274
1293
 
@@ -1461,7 +1480,7 @@ Product.search_index.promote(index_name, update_refresh_interval: true)
1461
1480
 
1462
1481
  ### Queuing
1463
1482
 
1464
- Push ids of records needing reindexed to a queue and reindex in bulk for better performance. First, set up Redis in an initializer. We recommend using [connection_pool](https://github.com/mperham/connection_pool).
1483
+ Push ids of records needing reindexing to a queue and reindex in bulk for better performance. First, set up Redis in an initializer. We recommend using [connection_pool](https://github.com/mperham/connection_pool).
1465
1484
 
1466
1485
  ```ruby
1467
1486
  Searchkick.redis = ConnectionPool.new { Redis.new }
@@ -1564,14 +1583,14 @@ class ReindexConversionsJob < ApplicationJob
1564
1583
  # get records that have a recent conversion
1565
1584
  recently_converted_ids =
1566
1585
  Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
1567
- .order(:convertable_id).uniq.pluck(:convertable_id)
1586
+ .order(:convertable_id).distinct.pluck(:convertable_id)
1568
1587
 
1569
1588
  # split into groups
1570
1589
  recently_converted_ids.in_groups_of(1000, false) do |ids|
1571
1590
  # fetch conversions
1572
1591
  conversions =
1573
1592
  Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
1574
- .group(:convertable_id, :query).uniq.count(:user_id)
1593
+ .group(:convertable_id, :query).distinct.count(:user_id)
1575
1594
 
1576
1595
  # group conversions by record
1577
1596
  conversions_by_record = {}
@@ -1695,7 +1714,7 @@ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenan
1695
1714
 
1696
1715
  ## Scroll API
1697
1716
 
1698
- Searchkick also supports the [scroll API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html). Scrolling is not intended for real time user requests, but rather for processing large amounts of data.
1717
+ Searchkick also supports the [scroll API](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results). Scrolling is not intended for real time user requests, but rather for processing large amounts of data.
1699
1718
 
1700
1719
  ```ruby
1701
1720
  Product.search("*", scroll: "1m").scroll do |batch|
@@ -1823,7 +1842,7 @@ class Product < ApplicationRecord
1823
1842
  def search_data
1824
1843
  {
1825
1844
  name: name,
1826
- unique_user_conversions: searches.group(:query).uniq.count(:user_id),
1845
+ unique_user_conversions: searches.group(:query).distinct.count(:user_id),
1827
1846
  # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
1828
1847
  total_conversions: searches.group(:query).count
1829
1848
  # {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
@@ -1948,7 +1967,7 @@ products = Product.search("carrots", execute: false)
1948
1967
  products.each { ... } # search not executed until here
1949
1968
  ```
1950
1969
 
1951
- Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html), like `search_type` and `query_cache`
1970
+ Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
1952
1971
 
1953
1972
  ```ruby
1954
1973
  Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
@@ -84,7 +84,8 @@ module Searchkick
84
84
  old_indices =
85
85
  begin
86
86
  client.indices.get_alias(name: name).keys
87
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
87
+ rescue => e
88
+ raise e unless Searchkick.not_found_error?(e)
88
89
  {}
89
90
  end
90
91
  actions = old_indices.map { |old_name| {remove: {index: old_name, alias: name}} } + [{add: {index: new_name, alias: name}}]
@@ -105,11 +106,12 @@ module Searchkick
105
106
  indices =
106
107
  begin
107
108
  if client.indices.respond_to?(:get_alias)
108
- client.indices.get_alias
109
+ client.indices.get_alias(index: "#{name}*")
109
110
  else
110
111
  client.indices.get_aliases
111
112
  end
112
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
113
+ rescue => e
114
+ raise e unless Searchkick.not_found_error?(e)
113
115
  {}
114
116
  end
115
117
  indices = indices.select { |_k, v| v.empty? || v["aliases"].empty? } if unaliased
@@ -161,6 +163,7 @@ module Searchkick
161
163
  RecordData.new(self, record).document_type
162
164
  end
163
165
 
166
+ # TODO use like: [{_index: ..., _id: ...}] in Searchkick 5
164
167
  def similar_record(record, **options)
165
168
  like_text = retrieve(record).to_hash
166
169
  .keep_if { |k, _| !options[:fields] || options[:fields].map(&:to_s).include?(k) }
@@ -177,13 +180,15 @@ module Searchkick
177
180
  end
178
181
 
179
182
  def reload_synonyms
180
- require "elasticsearch/xpack"
181
- raise Error, "Requires Elasticsearch 7.3+" if Searchkick.server_below?("7.3.0")
182
- raise Error, "Requires elasticsearch-xpack 7.8+" unless client.xpack.respond_to?(:indices)
183
- begin
184
- client.xpack.indices.reload_search_analyzers(index: name)
185
- rescue Elasticsearch::Transport::Transport::Errors::MethodNotAllowed
186
- raise Error, "Requires non-OSS version of Elasticsearch"
183
+ if Searchkick.opensearch?
184
+ client.transport.perform_request "POST", "_plugins/_refresh_search_analyzers/#{CGI.escape(name)}"
185
+ else
186
+ raise Error, "Requires Elasticsearch 7.3+" if Searchkick.server_below?("7.3.0")
187
+ begin
188
+ client.transport.perform_request("GET", "#{CGI.escape(name)}/_reload_search_analyzers")
189
+ rescue Elasticsearch::Transport::Transport::Errors::MethodNotAllowed
190
+ raise Error, "Requires non-OSS version of Elasticsearch"
191
+ end
187
192
  end
188
193
  end
189
194
 
@@ -360,9 +365,9 @@ module Searchkick
360
365
  index.refresh
361
366
  true
362
367
  end
363
- rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
364
- if e.message.include?("No handler for type [text]")
365
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 5 or greater"
368
+ rescue => e
369
+ if Searchkick.transport_error?(e) && e.message.include?("No handler for type [text]")
370
+ raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 6 or greater"
366
371
  end
367
372
 
368
373
  raise e
@@ -153,6 +153,7 @@ module Searchkick
153
153
  }
154
154
  }
155
155
 
156
+ raise ArgumentError, "Can't pass both language and stemmer" if options[:stemmer] && language
156
157
  update_language(settings, language)
157
158
  update_stemming(settings)
158
159
 
@@ -234,6 +235,27 @@ module Searchkick
234
235
  type: "kuromoji"
235
236
  }
236
237
  )
238
+ when "japanese2"
239
+ analyzer = {
240
+ type: "custom",
241
+ tokenizer: "kuromoji_tokenizer",
242
+ filter: [
243
+ "kuromoji_baseform",
244
+ "kuromoji_part_of_speech",
245
+ "cjk_width",
246
+ "ja_stop",
247
+ "searchkick_stemmer",
248
+ "lowercase"
249
+ ]
250
+ }
251
+ settings[:analysis][:analyzer].merge!(
252
+ default_analyzer => analyzer.deep_dup,
253
+ searchkick_search: analyzer.deep_dup,
254
+ searchkick_search2: analyzer.deep_dup
255
+ )
256
+ settings[:analysis][:filter][:searchkick_stemmer] = {
257
+ type: "kuromoji_stemmer"
258
+ }
237
259
  when "korean"
238
260
  settings[:analysis][:analyzer].merge!(
239
261
  default_analyzer => {
@@ -286,6 +308,18 @@ module Searchkick
286
308
  end
287
309
 
288
310
  def update_stemming(settings)
311
+ if options[:stemmer]
312
+ stemmer = options[:stemmer]
313
+ # could also support snowball and stemmer
314
+ case stemmer[:type]
315
+ when "hunspell"
316
+ # supports all token filter options
317
+ settings[:analysis][:filter][:searchkick_stemmer] = stemmer
318
+ else
319
+ raise ArgumentError, "Unknown stemmer: #{stemmer[:type]}"
320
+ end
321
+ end
322
+
289
323
  stem = options[:stem]
290
324
 
291
325
  # language analyzer used
@@ -499,8 +533,18 @@ module Searchkick
499
533
  end
500
534
  settings[:analysis][:filter][:searchkick_synonym_graph] = synonym_graph
501
535
 
502
- [:searchkick_search2, :searchkick_word_search].each do |analyzer|
503
- settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
536
+ if options[:language] == "japanese2"
537
+ [:searchkick_search, :searchkick_search2].each do |analyzer|
538
+ settings[:analysis][:analyzer][analyzer][:filter].insert(4, "searchkick_synonym_graph")
539
+ end
540
+ else
541
+ [:searchkick_search2, :searchkick_word_search].each do |analyzer|
542
+ unless settings[:analysis][:analyzer][analyzer].key?(:filter)
543
+ raise Searchkick::Error, "Search synonyms are not supported yet for language"
544
+ end
545
+
546
+ settings[:analysis][:analyzer][analyzer][:filter].insert(2, "searchkick_synonym_graph")
547
+ end
504
548
  end
505
549
  end
506
550
  end
@@ -6,7 +6,7 @@ module Searchkick
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
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,
9
+ :special_characters, :stem, :stemmer, :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
 
@@ -11,7 +11,7 @@ module Searchkick
11
11
  if record_ids.any?
12
12
  batch_options = {
13
13
  class_name: class_name,
14
- record_ids: record_ids,
14
+ record_ids: record_ids.uniq,
15
15
  index_name: index_name
16
16
  }
17
17
 
@@ -1,5 +1,6 @@
1
1
  module Searchkick
2
2
  class Query
3
+ include Enumerable
3
4
  extend Forwardable
4
5
 
5
6
  @@metric_aggs = [:avg, :cardinality, :max, :min, :sum]
@@ -12,7 +13,8 @@ module Searchkick
12
13
  :took, :error, :model_name, :entry_name, :total_count, :total_entries,
13
14
  :current_page, :per_page, :limit_value, :padding, :total_pages, :num_pages,
14
15
  :offset_value, :offset, :previous_page, :prev_page, :next_page, :first_page?, :last_page?,
15
- :out_of_range?, :hits, :response, :to_a, :first, :scroll
16
+ :out_of_range?, :hits, :response, :to_a, :first, :scroll, :highlights, :with_highlights,
17
+ :with_score, :misspellings?, :scroll_id, :clear_scroll, :missing_records, :with_hit
16
18
 
17
19
  def initialize(klass, term = "*", **options)
18
20
  unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
@@ -25,7 +27,7 @@ module Searchkick
25
27
  term = term.to_s
26
28
 
27
29
  if options[:emoji]
28
- term = EmojiParser.parse_unicode(term) { |e| " #{e.name} " }.strip
30
+ term = EmojiParser.parse_unicode(term) { |e| " #{e.name.tr('_', ' ')} " }.strip
29
31
  end
30
32
 
31
33
  @klass = klass
@@ -73,7 +75,8 @@ module Searchkick
73
75
  elsif searchkick_index
74
76
  searchkick_index.name
75
77
  else
76
- "_all"
78
+ # fixes warning about accessing system indices
79
+ "*,-.*"
77
80
  end
78
81
 
79
82
  params = {
@@ -109,7 +112,12 @@ module Searchkick
109
112
  request_params = query.except(:index, :type, :body)
110
113
 
111
114
  # no easy way to tell which host the client will use
112
- host = Searchkick.client.transport.hosts.first
115
+ host =
116
+ if Searchkick.client.transport.respond_to?(:transport)
117
+ Searchkick.client.transport.transport.hosts.first
118
+ else
119
+ Searchkick.client.transport.hosts.first
120
+ end
113
121
  credentials = host[:user] || host[:password] ? "#{host[:user]}:#{host[:password]}@" : nil
114
122
  params = ["pretty"]
115
123
  request_params.each do |k, v|
@@ -353,8 +361,8 @@ module Searchkick
353
361
  shared_options[:cutoff_frequency] = 0.001 unless operator.to_s == "and" || field_misspellings == false || (!below73? && !track_total_hits?)
354
362
  qs << shared_options.merge(analyzer: "searchkick_search")
355
363
 
356
- # searchkick_search and searchkick_search2 are the same for ukrainian
357
- unless %w(japanese korean polish ukrainian vietnamese).include?(searchkick_options[:language])
364
+ # searchkick_search and searchkick_search2 are the same for some languages
365
+ unless %w(japanese japanese2 korean polish ukrainian vietnamese).include?(searchkick_options[:language])
358
366
  qs << shared_options.merge(analyzer: "searchkick_search2")
359
367
  end
360
368
  exclude_analyzer = "searchkick_search2"
@@ -864,10 +872,11 @@ module Searchkick
864
872
  }
865
873
  end
866
874
 
867
- # TODO id transformation for arrays
868
875
  def set_order(payload)
869
876
  order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
870
877
  id_field = :_id
878
+ # TODO no longer map id to _id in Searchkick 5
879
+ # since sorting on _id is deprecated in Elasticsearch
871
880
  payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
872
881
  end
873
882
 
@@ -896,12 +905,7 @@ module Searchkick
896
905
  else
897
906
  # expand ranges
898
907
  if value.is_a?(Range)
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
908
+ value = expand_range(value)
905
909
  end
906
910
 
907
911
  value = {in: value} if value.is_a?(Array)
@@ -953,7 +957,7 @@ module Searchkick
953
957
  }
954
958
  }
955
959
  }
956
- when :like
960
+ when :like, :ilike
957
961
  # based on Postgres
958
962
  # https://www.postgresql.org/docs/current/functions-matching.html
959
963
  # % matches zero or more characters
@@ -961,13 +965,22 @@ module Searchkick
961
965
  # \ is escape character
962
966
  # escape Lucene reserved characters
963
967
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html#regexp-optional-operators
964
- reserved = %w(. ? + * | { } [ ] ( ) " \\)
968
+ reserved = %w(\\ . ? + * | { } [ ] ( ) ")
965
969
  regex = op_value.dup
966
970
  reserved.each do |v|
967
- regex.gsub!(v, "\\" + v)
971
+ regex.gsub!(v, "\\\\" + v)
968
972
  end
969
973
  regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
970
- filters << {regexp: {field => {value: regex}}}
974
+
975
+ if op == :ilike
976
+ if below710?
977
+ raise ArgumentError, "ilike requires Elasticsearch 7.10+"
978
+ else
979
+ filters << {regexp: {field => {value: regex, flags: "NONE", case_insensitive: true}}}
980
+ end
981
+ else
982
+ filters << {regexp: {field => {value: regex, flags: "NONE"}}}
983
+ end
971
984
  when :prefix
972
985
  filters << {prefix: {field => {value: op_value}}}
973
986
  when :regexp # support for regexp queries without using a regexp ruby object
@@ -1022,10 +1035,6 @@ module Searchkick
1022
1035
  elsif value.nil?
1023
1036
  {bool: {must_not: {exists: {field: field}}}}
1024
1037
  elsif value.is_a?(Regexp)
1025
- if value.casefold?
1026
- Searchkick.warn("Case-insensitive flag does not work with Elasticsearch")
1027
- end
1028
-
1029
1038
  source = value.source
1030
1039
  unless source.start_with?("\\A") && source.end_with?("\\z")
1031
1040
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html
@@ -1047,7 +1056,14 @@ module Searchkick
1047
1056
  # source = "#{source}.*"
1048
1057
  end
1049
1058
 
1050
- {regexp: {field => {value: source, flags: "NONE"}}}
1059
+ if below710?
1060
+ if value.casefold?
1061
+ Searchkick.warn("Case-insensitive flag does not work with Elasticsearch < 7.10")
1062
+ end
1063
+ {regexp: {field => {value: source, flags: "NONE"}}}
1064
+ else
1065
+ {regexp: {field => {value: source, flags: "NONE", case_insensitive: value.casefold?}}}
1066
+ end
1051
1067
  else
1052
1068
  # TODO add this for other values
1053
1069
  if value.as_json.is_a?(Enumerable)
@@ -1118,6 +1134,17 @@ module Searchkick
1118
1134
  end
1119
1135
  end
1120
1136
 
1137
+ def expand_range(range)
1138
+ expanded = {}
1139
+ expanded[:gte] = range.begin if range.begin
1140
+
1141
+ if range.end && !(range.end.respond_to?(:infinite?) && range.end.infinite?)
1142
+ expanded[range.exclude_end? ? :lt : :lte] = range.end
1143
+ end
1144
+
1145
+ expanded
1146
+ end
1147
+
1121
1148
  def base_field(k)
1122
1149
  k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
1123
1150
  end
@@ -1145,5 +1172,9 @@ module Searchkick
1145
1172
  def below75?
1146
1173
  Searchkick.server_below?("7.5.0")
1147
1174
  end
1175
+
1176
+ def below710?
1177
+ Searchkick.server_below?("7.10.0")
1178
+ end
1148
1179
  end
1149
1180
  end
@@ -64,8 +64,9 @@ module Searchkick
64
64
  if record.destroyed? || !record.persisted? || !record.should_index?
65
65
  begin
66
66
  index.remove(record)
67
- rescue Elasticsearch::Transport::Transport::Errors::NotFound
68
- # do nothing
67
+ rescue => e
68
+ raise e unless Searchkick.not_found_error?(e)
69
+ # do nothing if not found
69
70
  end
70
71
  else
71
72
  if method_name
@@ -14,11 +14,17 @@ module Searchkick
14
14
 
15
15
  # TODO use reliable queuing
16
16
  def reserve(limit: 1000)
17
- record_ids = Set.new
18
- while record_ids.size < limit && (record_id = Searchkick.with_redis { |r| r.rpop(redis_key) })
19
- record_ids << record_id
17
+ if supports_rpop_with_count?
18
+ Searchkick.with_redis { |r| r.call("rpop", redis_key, limit) }.to_a
19
+ else
20
+ record_ids = []
21
+ Searchkick.with_redis do |r|
22
+ while record_ids.size < limit && (record_id = r.rpop(redis_key))
23
+ record_ids << record_id
24
+ end
25
+ end
26
+ record_ids
20
27
  end
21
- record_ids.to_a
22
28
  end
23
29
 
24
30
  def clear
@@ -34,5 +40,13 @@ module Searchkick
34
40
  def redis_key
35
41
  "searchkick:reindex_queue:#{name}"
36
42
  end
43
+
44
+ def supports_rpop_with_count?
45
+ redis_version >= Gem::Version.new("6.2")
46
+ end
47
+
48
+ def redis_version
49
+ @redis_version ||= Searchkick.with_redis { |r| Gem::Version.new(r.info["redis_version"]) }
50
+ end
37
51
  end
38
52
  end
@@ -22,76 +22,17 @@ module Searchkick
22
22
  # TODO return enumerator like with_score
23
23
  def with_hit
24
24
  @with_hit ||= begin
25
- if options[:load]
26
- # results can have different types
27
- results = {}
28
-
29
- hits.group_by { |hit, _| hit["_index"] }.each do |index, grouped_hits|
30
- klasses =
31
- if @klass
32
- [@klass]
33
- else
34
- index_alias = index.split("_")[0..-2].join("_")
35
- Array((options[:index_mapping] || {})[index_alias])
36
- end
37
- raise Searchkick::Error, "Unknown model for index: #{index}" unless klasses.any?
38
-
39
- results[index] = {}
40
- klasses.each do |klass|
41
- results[index].merge!(results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s })
42
- end
43
- end
44
-
45
- missing_ids = []
46
-
47
- # sort
48
- results =
49
- hits.map do |hit|
50
- result = results[hit["_index"]][hit["_id"].to_s]
51
- if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
52
- if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
53
- highlights = hit_highlights(hit)
54
- result.define_singleton_method(:search_highlights) do
55
- highlights
56
- end
57
- end
58
- end
59
- [result, hit]
60
- end.select do |result, hit|
61
- missing_ids << hit["_id"] unless result
62
- result
63
- end
64
-
65
- if missing_ids.any?
66
- Searchkick.warn("Records in search index do not exist in database: #{missing_ids.join(", ")}")
67
- end
68
-
69
- results
70
- else
71
- hits.map do |hit|
72
- result =
73
- if hit["_source"]
74
- hit.except("_source").merge(hit["_source"])
75
- elsif hit["fields"]
76
- hit.except("fields").merge(hit["fields"])
77
- else
78
- hit
79
- end
80
-
81
- if hit["highlight"] || options[:highlight]
82
- highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
83
- options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
84
- result["highlighted_#{k}"] ||= (highlight[k] || result[k])
85
- end
86
- end
87
-
88
- result["id"] ||= result["_id"] # needed for legacy reasons
89
- [HashWrapper.new(result), hit]
90
- 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(", ")}")
91
27
  end
28
+ with_hit_and_missing_records[0]
92
29
  end
93
30
  end
94
31
 
32
+ def missing_records
33
+ @missing_records ||= with_hit_and_missing_records[1]
34
+ end
35
+
95
36
  def suggestions
96
37
  if response["suggest"]
97
38
  response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
@@ -130,7 +71,11 @@ module Searchkick
130
71
  end
131
72
 
132
73
  def model_name
133
- klass.model_name
74
+ if klass.nil?
75
+ ActiveModel::Name.new(self.class, nil, 'Result')
76
+ else
77
+ klass.model_name
78
+ end
134
79
  end
135
80
 
136
81
  def entry_name(options = {})
@@ -247,16 +192,11 @@ module Searchkick
247
192
 
248
193
  records.clear_scroll
249
194
  else
250
- params = {
251
- scroll: options[:scroll],
252
- scroll_id: scroll_id
253
- }
254
-
255
195
  begin
256
196
  # TODO Active Support notifications for this scroll call
257
- Searchkick::Results.new(@klass, Searchkick.client.scroll(params), @options)
258
- rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
259
- if e.class.to_s =~ /NotFound/ && e.message =~ /search_context_missing_exception/i
197
+ Searchkick::Results.new(@klass, Searchkick.client.scroll(scroll: options[:scroll], body: {scroll_id: scroll_id}), @options)
198
+ rescue => e
199
+ if Searchkick.not_found_error?(e) && e.message =~ /search_context_missing_exception/i
260
200
  raise Searchkick::Error, "Scroll id has expired"
261
201
  else
262
202
  raise e
@@ -271,13 +211,97 @@ module Searchkick
271
211
  # not required as scroll will expire
272
212
  # but there is a cost to open scrolls
273
213
  Searchkick.client.clear_scroll(scroll_id: scroll_id)
274
- rescue Elasticsearch::Transport::Transport::Error
275
- # do nothing
214
+ rescue => e
215
+ raise e unless Searchkick.transport_error?(e)
276
216
  end
277
217
  end
278
218
 
279
219
  private
280
220
 
221
+ def with_hit_and_missing_records
222
+ @with_hit_and_missing_records ||= begin
223
+ missing_records = []
224
+
225
+ if options[:load]
226
+ grouped_hits = hits.group_by { |hit, _| hit["_index"] }
227
+
228
+ # determine models
229
+ index_models = {}
230
+ grouped_hits.each do |index, _|
231
+ models =
232
+ if @klass
233
+ [@klass]
234
+ else
235
+ index_alias = index.split("_")[0..-2].join("_")
236
+ Array((options[:index_mapping] || {})[index_alias])
237
+ end
238
+ raise Searchkick::Error, "Unknown model for index: #{index}. Pass the `models` option to the search method." unless models.any?
239
+ index_models[index] = models
240
+ end
241
+
242
+ # fetch results
243
+ results = {}
244
+ grouped_hits.each do |index, index_hits|
245
+ results[index] = {}
246
+ index_models[index].each do |model|
247
+ results[index].merge!(results_query(model, index_hits).to_a.index_by { |r| r.id.to_s })
248
+ end
249
+ end
250
+
251
+ # sort
252
+ results =
253
+ hits.map do |hit|
254
+ result = results[hit["_index"]][hit["_id"].to_s]
255
+ if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
256
+ if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
257
+ highlights = hit_highlights(hit)
258
+ result.define_singleton_method(:search_highlights) do
259
+ highlights
260
+ end
261
+ end
262
+ end
263
+ [result, hit]
264
+ end.select do |result, hit|
265
+ unless result
266
+ models = index_models[hit["_index"]]
267
+ missing_records << {
268
+ id: hit["_id"],
269
+ # may be multiple models for inheritance with child models
270
+ # not ideal to return different types
271
+ # but this situation shouldn't be common
272
+ model: models.size == 1 ? models.first : models
273
+ }
274
+ end
275
+ result
276
+ end
277
+ else
278
+ results =
279
+ hits.map do |hit|
280
+ result =
281
+ if hit["_source"]
282
+ hit.except("_source").merge(hit["_source"])
283
+ elsif hit["fields"]
284
+ hit.except("fields").merge(hit["fields"])
285
+ else
286
+ hit
287
+ end
288
+
289
+ if hit["highlight"] || options[:highlight]
290
+ highlight = Hash[hit["highlight"].to_a.map { |k, v| [base_field(k), v.first] }]
291
+ options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
292
+ result["highlighted_#{k}"] ||= (highlight[k] || result[k])
293
+ end
294
+ end
295
+
296
+ result["id"] ||= result["_id"] # needed for legacy reasons
297
+ [HashWrapper.new(result), hit]
298
+ end
299
+ end
300
+
301
+ [results, missing_records]
302
+ end
303
+ end
304
+
281
305
  def results_query(records, hits)
282
306
  ids = hits.map { |hit| hit["_id"] }
283
307
  if options[:includes] || options[:model_includes]
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "4.4.1"
2
+ VERSION = "4.6.3"
3
3
  end
data/lib/searchkick.rb CHANGED
@@ -34,6 +34,7 @@ module Searchkick
34
34
  class Error < StandardError; end
35
35
  class MissingIndexError < Error; end
36
36
  class UnsupportedVersionError < Error; end
37
+ # TODO switch to Error
37
38
  class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end
38
39
  class DangerousOperation < Error; end
39
40
  class ImportError < Error; end
@@ -56,7 +57,7 @@ module Searchkick
56
57
  require "typhoeus/adapters/faraday" if defined?(Typhoeus) && Gem::Version.new(Faraday::VERSION) < Gem::Version.new("0.14.0")
57
58
 
58
59
  Elasticsearch::Client.new({
59
- url: ENV["ELASTICSEARCH_URL"],
60
+ url: ENV["ELASTICSEARCH_URL"] || ENV["OPENSEARCH_URL"],
60
61
  transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}},
61
62
  retry_on_failure: 2
62
63
  }.deep_merge(client_options)) do |f|
@@ -74,11 +75,24 @@ module Searchkick
74
75
  (defined?(@search_timeout) && @search_timeout) || timeout
75
76
  end
76
77
 
78
+ # private
79
+ def self.server_info
80
+ @server_info ||= client.info
81
+ end
82
+
77
83
  def self.server_version
78
- @server_version ||= client.info["version"]["number"]
84
+ @server_version ||= server_info["version"]["number"]
85
+ end
86
+
87
+ def self.opensearch?
88
+ unless defined?(@opensearch)
89
+ @opensearch = server_info["version"]["distribution"] == "opensearch"
90
+ end
91
+ @opensearch
79
92
  end
80
93
 
81
94
  def self.server_below?(version)
95
+ server_version = opensearch? ? "7.10.2" : self.server_version
82
96
  Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
83
97
  end
84
98
 
@@ -265,6 +279,18 @@ module Searchkick
265
279
  !Mongoid::Threaded.current_scope(klass).nil?
266
280
  end
267
281
  end
282
+
283
+ # private
284
+ def self.not_found_error?(e)
285
+ (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Errors::NotFound)) ||
286
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Errors::NotFound))
287
+ end
288
+
289
+ # private
290
+ def self.transport_error?(e)
291
+ (defined?(Elasticsearch) && e.is_a?(Elasticsearch::Transport::Transport::Error)) ||
292
+ (defined?(OpenSearch) && e.is_a?(OpenSearch::Transport::Transport::Error))
293
+ end
268
294
  end
269
295
 
270
296
  require "active_model/callbacks"
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.4.1
4
+ version: 4.6.3
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-06-25 00:00:00.000000000 Z
11
+ date: 2021-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -31,6 +31,9 @@ dependencies:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '6'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '7.14'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,6 +41,9 @@ dependencies:
38
41
  - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '6'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '7.14'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: hashie
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -52,8 +58,8 @@ dependencies:
52
58
  - - ">="
53
59
  - !ruby/object:Gem::Version
54
60
  version: '0'
55
- description:
56
- email: andrew@chartkick.com
61
+ description:
62
+ email: andrew@ankane.org
57
63
  executables: []
58
64
  extensions: []
59
65
  extra_rdoc_files: []
@@ -87,7 +93,7 @@ homepage: https://github.com/ankane/searchkick
87
93
  licenses:
88
94
  - MIT
89
95
  metadata: {}
90
- post_install_message:
96
+ post_install_message:
91
97
  rdoc_options: []
92
98
  require_paths:
93
99
  - lib
@@ -102,8 +108,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
108
  - !ruby/object:Gem::Version
103
109
  version: '0'
104
110
  requirements: []
105
- rubygems_version: 3.1.2
106
- signing_key:
111
+ rubygems_version: 3.2.22
112
+ signing_key:
107
113
  specification_version: 4
108
- summary: Intelligent search made easy with Rails and Elasticsearch
114
+ summary: Intelligent search made easy with Rails and Elasticsearch or OpenSearch
109
115
  test_files: []