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 +4 -4
- data/CHANGELOG.md +26 -0
- data/CONTRIBUTING.md +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +91 -41
- data/lib/searchkick.rb +18 -2
- data/lib/searchkick/bulk_indexer.rb +1 -1
- data/lib/searchkick/index.rb +38 -1
- data/lib/searchkick/index_options.rb +38 -12
- data/lib/searchkick/logging.rb +11 -10
- data/lib/searchkick/model.rb +11 -6
- data/lib/searchkick/query.rb +67 -19
- data/lib/searchkick/version.rb +1 -1
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0218ffe2be1e6fcebea56dfa7c8d112e2f43580d69b513d064f48c4905c51934'
|
4
|
+
data.tar.gz: 79b21178f3db14d84179aa3db7cbf5dae4d7adfa98a9a72e7f92991c9bff2ce0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce5ec190b6d754210ce88f48197cc0c08a32b6d33cc9a904385613ae3ec5bd7490a1d6c711bcf6cd3645636d64fe0c7e91bc23e009a32416194c8fbbe0119956
|
7
|
+
data.tar.gz: 3b306480292cfbcdc5854cc491f3700ecfd88715d37f879aa5715f0782ac86f380aa4828bec2866d2ea74186e445b9d4380d4696c8c2abcf1404207afa8ede18
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/CONTRIBUTING.md
CHANGED
@@ -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
|
|
data/LICENSE.txt
CHANGED
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/
|
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
|
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
|
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
|
-
|
333
|
+
For directional synonyms, use:
|
340
334
|
|
341
|
-
|
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
|
-
|
355
|
+
search_synonyms: "synonyms.txt"
|
345
356
|
```
|
346
357
|
|
347
|
-
|
358
|
+
Add [elasticsearch-xpack](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-xpack) to your Gemfile:
|
348
359
|
|
349
360
|
```ruby
|
350
|
-
|
361
|
+
gem 'elasticsearch-xpack', '>= 7.8.0.pre'
|
351
362
|
```
|
352
363
|
|
353
|
-
|
364
|
+
And use:
|
365
|
+
|
366
|
+
```ruby
|
367
|
+
Product.search_index.reload_synonyms
|
368
|
+
```
|
354
369
|
|
355
|
-
|
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
|
-
|
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)
|
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
|
1543
|
+
## Deep Paging
|
1522
1544
|
|
1523
|
-
By default, Elasticsearch limits
|
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
|
1549
|
+
searchkick deep_paging: true
|
1528
1550
|
end
|
1529
1551
|
```
|
1530
1552
|
|
1531
|
-
|
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",
|
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(
|
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
|
-
|
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
|
```
|
data/lib/searchkick.rb
CHANGED
@@ -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
|
data/lib/searchkick/index.rb
CHANGED
@@ -47,7 +47,7 @@ module Searchkick
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def refresh_interval
|
50
|
-
|
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: "
|
127
|
+
type: "edge_ngram",
|
130
128
|
min_gram: 1,
|
131
129
|
max_gram: 50
|
132
130
|
},
|
133
131
|
searchkick_ngram: {
|
134
|
-
type: "
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
data/lib/searchkick/logging.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
|
data/lib/searchkick/model.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
60
|
+
# TODO relation = Searchkick.relation?(self)
|
61
|
+
relation = (respond_to?(:current_scope) && respond_to?(:default_scoped) && current_scope && current_scope.to_sql != default_scoped.to_sql) ||
|
58
62
|
(respond_to?(:queryable) && queryable != unscoped.with_default_scope)
|
59
63
|
|
60
|
-
searchkick_index.reindex(searchkick_klass, method_name, scoped:
|
64
|
+
searchkick_index.reindex(searchkick_klass, method_name, scoped: relation, **options)
|
61
65
|
end
|
62
66
|
alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)
|
63
67
|
|
@@ -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
|
data/lib/searchkick/query.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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 = {
|
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
|
-
|
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
|
-
|
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
|
data/lib/searchkick/version.rb
CHANGED
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.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:
|
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.
|
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
|