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 +4 -4
- data/CHANGELOG.md +53 -0
- data/LICENSE.txt +1 -1
- data/README.md +66 -47
- data/lib/searchkick/index.rb +18 -13
- data/lib/searchkick/index_options.rb +46 -2
- data/lib/searchkick/model.rb +1 -1
- data/lib/searchkick/process_queue_job.rb +1 -1
- data/lib/searchkick/query.rb +53 -22
- data/lib/searchkick/record_indexer.rb +3 -2
- data/lib/searchkick/reindex_queue.rb +18 -4
- data/lib/searchkick/results.rb +101 -77
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick.rb +28 -2
- metadata +15 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6937cc5456846216a4ea52c2274dfc43220e7f848dde276f0102ce48de3184a6
|
4
|
+
data.tar.gz: 0bfcc6a4e5a893629f9cac734498efba357dbd19e6641c21af7ea8e82f94f1ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
23
|
+
- works with Active Record, Mongoid, and NoBrainer
|
24
24
|
|
25
|
-
|
25
|
+
Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Autosuggest](https://github.com/ankane/autosuggest) for query suggestions
|
26
26
|
|
27
|
-
:
|
27
|
+
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
28
28
|
|
29
|
-
[![Build Status](https://
|
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
|
-
|
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},
|
107
|
-
orders_count: 1..10,
|
108
|
-
aisle_id: [25, 30],
|
109
|
-
store_id: {not: 2},
|
110
|
-
aisle_id: {not: [25, 30]},
|
111
|
-
user_ids: {all: [1, 3]},
|
112
|
-
category: {like: "%frozen%"},
|
113
|
-
category:
|
114
|
-
category:
|
115
|
-
|
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}
|
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-
|
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-
|
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
|
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](#
|
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
|
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
|
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
|
-
|
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
|
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).
|
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/
|
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#
|
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
|
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
|
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
|
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).
|
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).
|
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-
|
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).
|
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-
|
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"})
|
data/lib/searchkick/index.rb
CHANGED
@@ -84,7 +84,8 @@ module Searchkick
|
|
84
84
|
old_indices =
|
85
85
|
begin
|
86
86
|
client.indices.get_alias(name: name).keys
|
87
|
-
rescue
|
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
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
364
|
-
if e.message.include?("No handler for type [text]")
|
365
|
-
raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch
|
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
|
-
[:
|
503
|
-
|
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
|
data/lib/searchkick/model.rb
CHANGED
@@ -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
|
|
data/lib/searchkick/query.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
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
|
-
|
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, "
|
971
|
+
regex.gsub!(v, "\\\\" + v)
|
968
972
|
end
|
969
973
|
regex = regex.gsub(/(?<!\\)%/, ".*").gsub(/(?<!\\)_/, ".").gsub("\\%", "%").gsub("\\_", "_")
|
970
|
-
|
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
|
-
|
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
|
68
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/searchkick/results.rb
CHANGED
@@ -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
|
26
|
-
#
|
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.
|
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(
|
258
|
-
rescue
|
259
|
-
if e
|
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
|
275
|
-
|
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]
|
data/lib/searchkick/version.rb
CHANGED
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 ||=
|
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
|
+
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:
|
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@
|
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.
|
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: []
|