searchkick 4.0.0 → 5.0.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 +234 -96
- data/LICENSE.txt +1 -1
- data/README.md +446 -268
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +174 -56
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +472 -349
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +1 -1
- data/lib/searchkick/model.rb +51 -48
- data/lib/searchkick/process_batch_job.rb +10 -26
- data/lib/searchkick/process_queue_job.rb +21 -12
- data/lib/searchkick/query.rb +183 -51
- data/lib/searchkick/record_data.rb +0 -1
- data/lib/searchkick/record_indexer.rb +135 -50
- data/lib/searchkick/reindex_queue.rb +43 -6
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +36 -0
- data/lib/searchkick/relation_indexer.rb +150 -0
- data/lib/searchkick/results.rb +162 -80
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick.rb +203 -79
- data/lib/tasks/searchkick.rake +21 -11
- metadata +17 -71
- data/CONTRIBUTING.md +0 -53
- data/lib/searchkick/bulk_indexer.rb +0 -171
- data/lib/searchkick/logging.rb +0 -243
data/README.md
CHANGED
@@ -10,7 +10,7 @@ Searchkick handles:
|
|
10
10
|
- special characters - `jalapeno` matches `jalapeño`
|
11
11
|
- extra whitespace - `dishwasher` matches `dish washer`
|
12
12
|
- misspellings - `zuchini` matches `zucchini`
|
13
|
-
- custom synonyms - `
|
13
|
+
- custom synonyms - `pop` matches `soda`
|
14
14
|
|
15
15
|
Plus:
|
16
16
|
|
@@ -20,40 +20,53 @@ Plus:
|
|
20
20
|
- autocomplete
|
21
21
|
- “Did you mean” suggestions
|
22
22
|
- supports many languages
|
23
|
-
- works with
|
23
|
+
- works with Active Record and Mongoid
|
24
|
+
|
25
|
+
Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Autosuggest](https://github.com/ankane/autosuggest) for query suggestions
|
24
26
|
|
25
27
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
26
28
|
|
27
|
-
[![Build Status](https://
|
29
|
+
[![Build Status](https://github.com/ankane/searchkick/workflows/build/badge.svg?branch=master)](https://github.com/ankane/searchkick/actions)
|
28
30
|
|
29
31
|
## Contents
|
30
32
|
|
31
33
|
- [Getting Started](#getting-started)
|
32
34
|
- [Querying](#querying)
|
33
35
|
- [Indexing](#indexing)
|
36
|
+
- [Intelligent Search](#intelligent-search)
|
34
37
|
- [Instant Search / Autocomplete](#instant-search--autocomplete)
|
35
38
|
- [Aggregations](#aggregations)
|
39
|
+
- [Testing](#testing)
|
36
40
|
- [Deployment](#deployment)
|
37
41
|
- [Performance](#performance)
|
38
|
-
- [
|
42
|
+
- [Advanced Search](#advanced)
|
39
43
|
- [Reference](#reference)
|
44
|
+
- [Contributing](#contributing)
|
45
|
+
|
46
|
+
Searchkick 5.0 was recently released! See [how to upgrade](#upgrading)
|
40
47
|
|
41
48
|
## Getting Started
|
42
49
|
|
43
|
-
|
50
|
+
Install [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) or [OpenSearch](https://opensearch.org/downloads.html). For Homebrew, use:
|
44
51
|
|
45
52
|
```sh
|
46
53
|
brew install elasticsearch
|
47
54
|
brew services start elasticsearch
|
55
|
+
# or
|
56
|
+
brew install opensearch
|
57
|
+
brew services start opensearch
|
48
58
|
```
|
49
59
|
|
50
|
-
Add
|
60
|
+
Add these lines to your application’s Gemfile:
|
51
61
|
|
52
62
|
```ruby
|
53
|
-
gem
|
63
|
+
gem "searchkick"
|
64
|
+
|
65
|
+
gem "elasticsearch" # select one
|
66
|
+
gem "opensearch-ruby" # select one
|
54
67
|
```
|
55
68
|
|
56
|
-
The latest version works with Elasticsearch
|
69
|
+
The latest version works with Elasticsearch 7 and 8 and OpenSearch 1. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
|
57
70
|
|
58
71
|
Add searchkick to models you want to search.
|
59
72
|
|
@@ -78,7 +91,7 @@ products.each do |product|
|
|
78
91
|
end
|
79
92
|
```
|
80
93
|
|
81
|
-
Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html). As your search becomes more advanced, we recommend you use the [
|
94
|
+
Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html) and [OpenSearch Search API](https://opensearch.org/docs/latest/opensearch/rest-api/search/). As your search becomes more advanced, we recommend you use the [search server DSL](#advanced) for maximum flexibility.
|
82
95
|
|
83
96
|
## Querying
|
84
97
|
|
@@ -98,14 +111,20 @@ Where
|
|
98
111
|
|
99
112
|
```ruby
|
100
113
|
where: {
|
101
|
-
expires_at: {gt: Time.now},
|
102
|
-
orders_count: 1..10,
|
103
|
-
aisle_id: [25, 30],
|
104
|
-
store_id: {not: 2},
|
105
|
-
aisle_id: {not: [25, 30]},
|
106
|
-
user_ids: {all: [1, 3]},
|
107
|
-
category:
|
108
|
-
|
114
|
+
expires_at: {gt: Time.now}, # lt, gte, lte also available
|
115
|
+
orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
|
116
|
+
aisle_id: [25, 30], # in
|
117
|
+
store_id: {not: 2}, # not
|
118
|
+
aisle_id: {not: [25, 30]}, # not in
|
119
|
+
user_ids: {all: [1, 3]}, # all elements in array
|
120
|
+
category: {like: "%frozen%"}, # like
|
121
|
+
category: {ilike: "%frozen%"}, # ilike
|
122
|
+
category: /frozen .+/, # regexp
|
123
|
+
category: {prefix: "frozen"}, # prefix
|
124
|
+
store_id: {exists: true}, # exists
|
125
|
+
_or: [{in_stock: true}, {backordered: true}],
|
126
|
+
_and: [{in_stock: true}, {backordered: true}],
|
127
|
+
_not: {store_id: 1} # negate a condition
|
109
128
|
}
|
110
129
|
```
|
111
130
|
|
@@ -115,7 +134,7 @@ Order
|
|
115
134
|
order: {_score: :desc} # most relevant first - default
|
116
135
|
```
|
117
136
|
|
118
|
-
[All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-
|
137
|
+
[All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
|
119
138
|
|
120
139
|
Limit / offset
|
121
140
|
|
@@ -129,11 +148,11 @@ Select
|
|
129
148
|
select: [:name]
|
130
149
|
```
|
131
150
|
|
132
|
-
[These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-
|
151
|
+
[These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering)
|
133
152
|
|
134
153
|
### Results
|
135
154
|
|
136
|
-
Searches return a `Searchkick::
|
155
|
+
Searches return a `Searchkick::Relation` object. This responds like an array to most methods.
|
137
156
|
|
138
157
|
```ruby
|
139
158
|
results = Product.search("milk")
|
@@ -142,7 +161,7 @@ results.any?
|
|
142
161
|
results.each { |result| ... }
|
143
162
|
```
|
144
163
|
|
145
|
-
By default, ids are fetched from
|
164
|
+
By default, ids are fetched from the search server and records are fetched from your database. To fetch everything from the search server, use:
|
146
165
|
|
147
166
|
```ruby
|
148
167
|
Product.search("apples", load: false)
|
@@ -160,12 +179,14 @@ Get the time the search took (in milliseconds)
|
|
160
179
|
results.took
|
161
180
|
```
|
162
181
|
|
163
|
-
Get the full response from
|
182
|
+
Get the full response from the search server
|
164
183
|
|
165
184
|
```ruby
|
166
185
|
results.response
|
167
186
|
```
|
168
187
|
|
188
|
+
**Note:** By default, Elasticsearch and OpenSearch [limit paging](#deep-paging) to the first 10,000 results for performance. This applies to the total count as well.
|
189
|
+
|
169
190
|
### Boosting
|
170
191
|
|
171
192
|
Boost important fields
|
@@ -197,7 +218,7 @@ boost_by_recency: {created_at: {scale: "7d", decay: 0.5}}
|
|
197
218
|
|
198
219
|
You can also boost by:
|
199
220
|
|
200
|
-
- [Conversions](#
|
221
|
+
- [Conversions](#intelligent-search)
|
201
222
|
- [Distance](#boost-by-distance)
|
202
223
|
|
203
224
|
### Get Everything
|
@@ -287,7 +308,9 @@ To only match the exact order, use:
|
|
287
308
|
User.search "fresh honey", match: :phrase
|
288
309
|
```
|
289
310
|
|
290
|
-
### Language
|
311
|
+
### Stemming and Language
|
312
|
+
|
313
|
+
Searchkick stems words by default for better matching. `apple` and `apples` both stem to `appl`, so searches for either term will have the same matches.
|
291
314
|
|
292
315
|
Searchkick defaults to English for stemming. To change this, use:
|
293
316
|
|
@@ -297,44 +320,95 @@ class Product < ApplicationRecord
|
|
297
320
|
end
|
298
321
|
```
|
299
322
|
|
300
|
-
|
301
|
-
|
302
|
-
A few languages require plugins:
|
323
|
+
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:
|
303
324
|
|
304
325
|
- `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
|
305
|
-
- `
|
326
|
+
- `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-smartcn.html)
|
327
|
+
- `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-kuromoji.html)
|
306
328
|
- `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
|
307
|
-
- `
|
308
|
-
- `
|
329
|
+
- `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-nori.html)
|
330
|
+
- `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-stempel.html)
|
331
|
+
- `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
|
309
332
|
- `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
|
310
333
|
|
311
|
-
|
334
|
+
You can also use a Hunspell dictionary for stemming.
|
312
335
|
|
313
336
|
```ruby
|
314
337
|
class Product < ApplicationRecord
|
315
|
-
searchkick
|
338
|
+
searchkick stemmer: {type: "hunspell", locale: "en_US"}
|
316
339
|
end
|
317
340
|
```
|
318
341
|
|
319
|
-
|
342
|
+
Disable stemming with:
|
320
343
|
|
321
|
-
|
344
|
+
```ruby
|
345
|
+
class Image < ApplicationRecord
|
346
|
+
searchkick stem: false
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
Exclude certain words from stemming with:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
class Image < ApplicationRecord
|
354
|
+
searchkick stem_exclusion: ["apples"]
|
355
|
+
end
|
356
|
+
```
|
357
|
+
|
358
|
+
Or change how words are stemmed:
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
class Image < ApplicationRecord
|
362
|
+
searchkick stemmer_override: ["apples => other"]
|
363
|
+
end
|
364
|
+
```
|
322
365
|
|
323
|
-
|
366
|
+
### Synonyms
|
324
367
|
|
325
368
|
```ruby
|
326
|
-
|
369
|
+
class Product < ApplicationRecord
|
370
|
+
searchkick search_synonyms: [["pop", "soda"], ["burger", "hamburger"]]
|
371
|
+
end
|
327
372
|
```
|
328
373
|
|
374
|
+
Call `Product.reindex` after changing synonyms. Synonyms are applied at search time before stemming, and can be a single word or multiple words.
|
375
|
+
|
329
376
|
For directional synonyms, use:
|
330
377
|
|
331
378
|
```ruby
|
332
|
-
|
379
|
+
search_synonyms: ["lightbulb => halogenlamp"]
|
333
380
|
```
|
334
381
|
|
335
|
-
###
|
382
|
+
### Dynamic Synonyms
|
336
383
|
|
337
|
-
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
|
384
|
+
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.
|
385
|
+
|
386
|
+
#### Elasticsearch 7.3+ and OpenSearch
|
387
|
+
|
388
|
+
For Elasticsearch 7.3+ and OpenSearch, we recommend placing synonyms in a file on the search server (in the `config` directory). This allows you to reload synonyms without reindexing.
|
389
|
+
|
390
|
+
```txt
|
391
|
+
pop, soda
|
392
|
+
burger, hamburger
|
393
|
+
```
|
394
|
+
|
395
|
+
Then use:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
class Product < ApplicationRecord
|
399
|
+
searchkick search_synonyms: "synonyms.txt"
|
400
|
+
end
|
401
|
+
```
|
402
|
+
|
403
|
+
And reload with:
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
Product.search_index.reload_synonyms
|
407
|
+
```
|
408
|
+
|
409
|
+
#### Elasticsearch < 7.3
|
410
|
+
|
411
|
+
You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
|
338
412
|
|
339
413
|
```ruby
|
340
414
|
class Product < ApplicationRecord
|
@@ -419,7 +493,7 @@ Search :ice_cream::cake: and get `ice cream cake`!
|
|
419
493
|
Add this line to your application’s Gemfile:
|
420
494
|
|
421
495
|
```ruby
|
422
|
-
gem
|
496
|
+
gem "gemoji-parser"
|
423
497
|
```
|
424
498
|
|
425
499
|
And use:
|
@@ -454,12 +528,10 @@ class Product < ApplicationRecord
|
|
454
528
|
end
|
455
529
|
```
|
456
530
|
|
457
|
-
By default, all records are indexed. To control which records are indexed, use the `should_index?` method
|
531
|
+
By default, all records are indexed. To control which records are indexed, use the `should_index?` method.
|
458
532
|
|
459
533
|
```ruby
|
460
534
|
class Product < ApplicationRecord
|
461
|
-
scope :search_import, -> { where(active: true) }
|
462
|
-
|
463
535
|
def should_index?
|
464
536
|
active # only index active records
|
465
537
|
end
|
@@ -486,7 +558,7 @@ For large data sets, try [parallel reindexing](#parallel-reindexing).
|
|
486
558
|
|
487
559
|
- app starts
|
488
560
|
|
489
|
-
###
|
561
|
+
### Strategies
|
490
562
|
|
491
563
|
There are four strategies for keeping the index synced with your database.
|
492
564
|
|
@@ -536,7 +608,7 @@ Searchkick.callbacks(false) do
|
|
536
608
|
end
|
537
609
|
```
|
538
610
|
|
539
|
-
|
611
|
+
### Associations
|
540
612
|
|
541
613
|
Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
|
542
614
|
|
@@ -552,11 +624,31 @@ class Image < ApplicationRecord
|
|
552
624
|
end
|
553
625
|
```
|
554
626
|
|
555
|
-
###
|
627
|
+
### Default Scopes
|
628
|
+
|
629
|
+
If you have a default scope that filters records, use the `should_index?` method to exclude them from indexing:
|
630
|
+
|
631
|
+
```ruby
|
632
|
+
class Product < ApplicationRecord
|
633
|
+
default_scope { where(deleted_at: nil) }
|
634
|
+
|
635
|
+
def should_index?
|
636
|
+
deleted_at.nil?
|
637
|
+
end
|
638
|
+
end
|
639
|
+
```
|
640
|
+
|
641
|
+
If you want to index and search filtered records, set:
|
556
642
|
|
557
|
-
|
643
|
+
```ruby
|
644
|
+
class Product < ApplicationRecord
|
645
|
+
searchkick unscope: true
|
646
|
+
end
|
647
|
+
```
|
558
648
|
|
559
|
-
|
649
|
+
## Intelligent Search
|
650
|
+
|
651
|
+
The best starting point to improve your search **by far** is to track searches and conversions. [Searchjoy](https://github.com/ankane/searchjoy) makes it easy.
|
560
652
|
|
561
653
|
```ruby
|
562
654
|
Product.search "apple", track: {user_id: current_user.id}
|
@@ -569,15 +661,9 @@ Focus on:
|
|
569
661
|
- top searches with low conversions
|
570
662
|
- top searches with no results
|
571
663
|
|
572
|
-
|
573
|
-
|
574
|
-
Searchkick can use conversion data to learn what users are looking for. If a user searches for “ice cream” and adds Ben & Jerry’s Chunky Monkey to the cart (our conversion metric at Instacart), that item gets a little more weight for similar searches.
|
575
|
-
|
576
|
-
The first step is to define your conversion metric and start tracking conversions. The database works well for low volume, but feel free to use Redis or another datastore.
|
664
|
+
Searchkick can then use the conversion data to learn what users are looking for. If a user searches for “ice cream” and adds Ben & Jerry’s Chunky Monkey to the cart (our conversion metric at Instacart), that item gets a little more weight for similar searches.
|
577
665
|
|
578
|
-
|
579
|
-
|
580
|
-
Next, add conversions to the index.
|
666
|
+
Add conversion data with:
|
581
667
|
|
582
668
|
```ruby
|
583
669
|
class Product < ApplicationRecord
|
@@ -588,7 +674,7 @@ class Product < ApplicationRecord
|
|
588
674
|
def search_data
|
589
675
|
{
|
590
676
|
name: name,
|
591
|
-
conversions: searches.group(:query).
|
677
|
+
conversions: searches.group(:query).distinct.count(:user_id)
|
592
678
|
# {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
|
593
679
|
}
|
594
680
|
end
|
@@ -601,9 +687,11 @@ Reindex and set up a cron job to add new conversions daily.
|
|
601
687
|
rake searchkick:reindex CLASS=Product
|
602
688
|
```
|
603
689
|
|
604
|
-
|
690
|
+
This can make a huge difference on the quality of your search.
|
691
|
+
|
692
|
+
For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
|
605
693
|
|
606
|
-
|
694
|
+
## Personalized Results
|
607
695
|
|
608
696
|
Order results differently for each user. For example, show a user’s previously purchased products before other results.
|
609
697
|
|
@@ -624,13 +712,13 @@ Reindex and search with:
|
|
624
712
|
Product.search "milk", boost_where: {orderer_ids: current_user.id}
|
625
713
|
```
|
626
714
|
|
627
|
-
|
715
|
+
## Instant Search / Autocomplete
|
628
716
|
|
629
717
|
Autocomplete predicts what a user will type, making the search experience faster and easier.
|
630
718
|
|
631
719
|
![Autocomplete](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/autocomplete.png)
|
632
720
|
|
633
|
-
**Note:** To autocomplete on
|
721
|
+
**Note:** To autocomplete on search terms rather than results, check out [Autosuggest](https://github.com/ankane/autosuggest).
|
634
722
|
|
635
723
|
**Note 2:** If you only have a few thousand records, don’t use Searchkick for autocomplete. It’s *much* faster to load all records into JavaScript and autocomplete there (eliminates network requests).
|
636
724
|
|
@@ -692,7 +780,7 @@ Then add the search box and JavaScript code to a view.
|
|
692
780
|
</script>
|
693
781
|
```
|
694
782
|
|
695
|
-
|
783
|
+
## Suggestions
|
696
784
|
|
697
785
|
![Suggest](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/recursion.png)
|
698
786
|
|
@@ -709,7 +797,7 @@ products = Product.search "peantu butta", suggest: true
|
|
709
797
|
products.suggestions # ["peanut butter"]
|
710
798
|
```
|
711
799
|
|
712
|
-
|
800
|
+
## Aggregations
|
713
801
|
|
714
802
|
[Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) provide aggregated search data.
|
715
803
|
|
@@ -752,8 +840,6 @@ Order
|
|
752
840
|
Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
|
753
841
|
```
|
754
842
|
|
755
|
-
**Note:** Use `_term` instead of `_key` in Elasticsearch 5
|
756
|
-
|
757
843
|
[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)
|
758
844
|
|
759
845
|
Ranges
|
@@ -784,10 +870,10 @@ Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :creat
|
|
784
870
|
For other aggregation types, including sub-aggregations, use `body_options`:
|
785
871
|
|
786
872
|
```ruby
|
787
|
-
Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}
|
873
|
+
Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}}
|
788
874
|
```
|
789
875
|
|
790
|
-
|
876
|
+
## Highlight
|
791
877
|
|
792
878
|
Specify which fields to index with highlighting.
|
793
879
|
|
@@ -838,9 +924,9 @@ Additional options can be specified for each field:
|
|
838
924
|
Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
|
839
925
|
```
|
840
926
|
|
841
|
-
You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/
|
927
|
+
You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
|
842
928
|
|
843
|
-
|
929
|
+
## Similar Items
|
844
930
|
|
845
931
|
Find similar items.
|
846
932
|
|
@@ -849,7 +935,7 @@ product = Product.first
|
|
849
935
|
product.similar(fields: [:name], where: {size: "12 oz"})
|
850
936
|
```
|
851
937
|
|
852
|
-
|
938
|
+
## Geospatial Searches
|
853
939
|
|
854
940
|
```ruby
|
855
941
|
class Restaurant < ApplicationRecord
|
@@ -889,7 +975,7 @@ Boost results by distance - closer results are boosted more
|
|
889
975
|
Restaurant.search "noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}}
|
890
976
|
```
|
891
977
|
|
892
|
-
Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#
|
978
|
+
Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
|
893
979
|
|
894
980
|
```ruby
|
895
981
|
Restaurant.search "wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}}
|
@@ -985,13 +1071,13 @@ Product.search("soap", debug: true)
|
|
985
1071
|
|
986
1072
|
This prints useful info to `stdout`.
|
987
1073
|
|
988
|
-
See how
|
1074
|
+
See how the search server scores your queries with:
|
989
1075
|
|
990
1076
|
```ruby
|
991
1077
|
Product.search("soap", explain: true).response
|
992
1078
|
```
|
993
1079
|
|
994
|
-
See how
|
1080
|
+
See how the search server tokenizes your queries with:
|
995
1081
|
|
996
1082
|
```ruby
|
997
1083
|
Product.search_index.tokens("Dish Washer Soap", analyzer: "searchkick_index")
|
@@ -1016,21 +1102,201 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search")
|
|
1016
1102
|
|
1017
1103
|
See the [complete list of analyzers](https://github.com/ankane/searchkick/blob/31780ddac7a89eab1e0552a32b403f2040a37931/lib/searchkick/index_options.rb#L32).
|
1018
1104
|
|
1105
|
+
## Testing
|
1106
|
+
|
1107
|
+
As you iterate on your search, it’s a good idea to add tests.
|
1108
|
+
|
1109
|
+
For performance, only enable Searchkick callbacks for the tests that need it.
|
1110
|
+
|
1111
|
+
### Parallel Tests
|
1112
|
+
|
1113
|
+
Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
|
1114
|
+
|
1115
|
+
```ruby
|
1116
|
+
class ActiveSupport::TestCase
|
1117
|
+
parallelize_setup do |worker|
|
1118
|
+
Searchkick.index_suffix = worker
|
1119
|
+
|
1120
|
+
# reindex models
|
1121
|
+
Product.reindex
|
1122
|
+
|
1123
|
+
# and disable callbacks
|
1124
|
+
Searchkick.disable_callbacks
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
```
|
1128
|
+
|
1129
|
+
And use:
|
1130
|
+
|
1131
|
+
```ruby
|
1132
|
+
class ProductTest < ActiveSupport::TestCase
|
1133
|
+
def setup
|
1134
|
+
Searchkick.enable_callbacks
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
def teardown
|
1138
|
+
Searchkick.disable_callbacks
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
def test_search
|
1142
|
+
Product.create!(name: "Apple")
|
1143
|
+
Product.search_index.refresh
|
1144
|
+
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
1145
|
+
end
|
1146
|
+
end
|
1147
|
+
```
|
1148
|
+
|
1149
|
+
### Minitest
|
1150
|
+
|
1151
|
+
Add to your `test/test_helper.rb`:
|
1152
|
+
|
1153
|
+
```ruby
|
1154
|
+
# reindex models
|
1155
|
+
Product.reindex
|
1156
|
+
|
1157
|
+
# and disable callbacks
|
1158
|
+
Searchkick.disable_callbacks
|
1159
|
+
```
|
1160
|
+
|
1161
|
+
And use:
|
1162
|
+
|
1163
|
+
```ruby
|
1164
|
+
class ProductTest < Minitest::Test
|
1165
|
+
def setup
|
1166
|
+
Searchkick.enable_callbacks
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
def teardown
|
1170
|
+
Searchkick.disable_callbacks
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
def test_search
|
1174
|
+
Product.create!(name: "Apple")
|
1175
|
+
Product.search_index.refresh
|
1176
|
+
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
```
|
1180
|
+
|
1181
|
+
### RSpec
|
1182
|
+
|
1183
|
+
Add to your `spec/spec_helper.rb`:
|
1184
|
+
|
1185
|
+
```ruby
|
1186
|
+
RSpec.configure do |config|
|
1187
|
+
config.before(:suite) do
|
1188
|
+
# reindex models
|
1189
|
+
Product.reindex
|
1190
|
+
|
1191
|
+
# and disable callbacks
|
1192
|
+
Searchkick.disable_callbacks
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
config.around(:each, search: true) do |example|
|
1196
|
+
Searchkick.callbacks(nil) do
|
1197
|
+
example.run
|
1198
|
+
end
|
1199
|
+
end
|
1200
|
+
end
|
1201
|
+
```
|
1202
|
+
|
1203
|
+
And use:
|
1204
|
+
|
1205
|
+
```ruby
|
1206
|
+
describe Product, search: true do
|
1207
|
+
it "searches" do
|
1208
|
+
Product.create!(name: "Apple")
|
1209
|
+
Product.search_index.refresh
|
1210
|
+
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
1211
|
+
end
|
1212
|
+
end
|
1213
|
+
```
|
1214
|
+
|
1215
|
+
### Factory Bot
|
1216
|
+
|
1217
|
+
Use a trait and an after `create` hook for each indexed model:
|
1218
|
+
|
1219
|
+
```ruby
|
1220
|
+
FactoryBot.define do
|
1221
|
+
factory :product do
|
1222
|
+
# ...
|
1223
|
+
|
1224
|
+
# Note: This should be the last trait in the list so `reindex` is called
|
1225
|
+
# after all the other callbacks complete.
|
1226
|
+
trait :reindex do
|
1227
|
+
after(:create) do |product, _evaluator|
|
1228
|
+
product.reindex(refresh: true)
|
1229
|
+
end
|
1230
|
+
end
|
1231
|
+
end
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
# use it
|
1235
|
+
FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
|
1236
|
+
```
|
1237
|
+
|
1238
|
+
### GitHub Actions
|
1239
|
+
|
1240
|
+
Check out [setup-elasticsearch](https://github.com/ankane/setup-elasticsearch) for an easy way to install Elasticsearch:
|
1241
|
+
|
1242
|
+
```yml
|
1243
|
+
- uses: ankane/setup-elasticsearch@v1
|
1244
|
+
```
|
1245
|
+
|
1246
|
+
And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy way to install OpenSearch:
|
1247
|
+
|
1248
|
+
```yml
|
1249
|
+
- uses: ankane/setup-opensearch@v1
|
1250
|
+
```
|
1251
|
+
|
1019
1252
|
## Deployment
|
1020
1253
|
|
1021
|
-
Searchkick uses `ENV["ELASTICSEARCH_URL"]` for
|
1254
|
+
For the search server, Searchkick uses `ENV["ELASTICSEARCH_URL"]` for Elasticsearch and `ENV["OPENSEARCH_URL"]` for OpenSearch. This defaults to `http://localhost:9200`.
|
1255
|
+
|
1256
|
+
- [Elastic Cloud](#elastic-cloud)
|
1257
|
+
- [Heroku](#heroku)
|
1258
|
+
- [Amazon OpenSearch Service](#amazon-opensearch-service)
|
1259
|
+
- [Self-Hosted and Other](#self-hosted-and-other)
|
1260
|
+
|
1261
|
+
### Elastic Cloud
|
1262
|
+
|
1263
|
+
Create an initializer `config/initializers/elasticsearch.rb` with:
|
1264
|
+
|
1265
|
+
```ruby
|
1266
|
+
ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
|
1267
|
+
```
|
1268
|
+
|
1269
|
+
Then deploy and reindex:
|
1270
|
+
|
1271
|
+
```sh
|
1272
|
+
rake searchkick:reindex:all
|
1273
|
+
```
|
1022
1274
|
|
1023
1275
|
### Heroku
|
1024
1276
|
|
1025
|
-
Choose an add-on: [Bonsai](https://elements.heroku.com/addons/bonsai)
|
1277
|
+
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).
|
1026
1278
|
|
1027
|
-
For Bonsai:
|
1279
|
+
For Elasticsearch on Bonsai:
|
1028
1280
|
|
1029
1281
|
```sh
|
1030
1282
|
heroku addons:create bonsai
|
1031
1283
|
heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
|
1032
1284
|
```
|
1033
1285
|
|
1286
|
+
For OpenSearch on Bonsai:
|
1287
|
+
|
1288
|
+
```sh
|
1289
|
+
heroku addons:create bonsai --engine=opensearch
|
1290
|
+
heroku config:set OPENSEARCH_URL=`heroku config:get BONSAI_URL`
|
1291
|
+
```
|
1292
|
+
|
1293
|
+
For SearchBox:
|
1294
|
+
|
1295
|
+
```sh
|
1296
|
+
heroku addons:create searchbox:starter
|
1297
|
+
heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
|
1298
|
+
```
|
1299
|
+
|
1034
1300
|
For Elastic Cloud (previously Found):
|
1035
1301
|
|
1036
1302
|
```sh
|
@@ -1044,30 +1310,30 @@ Visit the Shield page and reset your password. You’ll need to add the username
|
|
1044
1310
|
heroku config:get FOUNDELASTICSEARCH_URL
|
1045
1311
|
```
|
1046
1312
|
|
1047
|
-
And add `elastic:password@` right after `https
|
1313
|
+
And add `elastic:password@` right after `https://` and add port `9243` at the end:
|
1048
1314
|
|
1049
1315
|
```sh
|
1050
|
-
heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io
|
1316
|
+
heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io:9243
|
1051
1317
|
```
|
1052
1318
|
|
1053
1319
|
Then deploy and reindex:
|
1054
1320
|
|
1055
1321
|
```sh
|
1056
|
-
heroku run rake searchkick:reindex
|
1322
|
+
heroku run rake searchkick:reindex:all
|
1057
1323
|
```
|
1058
1324
|
|
1059
|
-
### Amazon
|
1325
|
+
### Amazon OpenSearch Service
|
1060
1326
|
|
1061
|
-
Create an initializer `config/initializers/
|
1327
|
+
Create an initializer `config/initializers/opensearch.rb` with:
|
1062
1328
|
|
1063
1329
|
```ruby
|
1064
|
-
ENV["
|
1330
|
+
ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
|
1065
1331
|
```
|
1066
1332
|
|
1067
1333
|
To use signed requests, include in your Gemfile:
|
1068
1334
|
|
1069
1335
|
```ruby
|
1070
|
-
gem
|
1336
|
+
gem "faraday_middleware-aws-sigv4"
|
1071
1337
|
```
|
1072
1338
|
|
1073
1339
|
and add to your initializer:
|
@@ -1083,28 +1349,30 @@ Searchkick.aws_credentials = {
|
|
1083
1349
|
Then deploy and reindex:
|
1084
1350
|
|
1085
1351
|
```sh
|
1086
|
-
rake searchkick:reindex
|
1352
|
+
rake searchkick:reindex:all
|
1087
1353
|
```
|
1088
1354
|
|
1089
|
-
### Other
|
1355
|
+
### Self-Hosted and Other
|
1090
1356
|
|
1091
|
-
Create an initializer
|
1357
|
+
Create an initializer with:
|
1092
1358
|
|
1093
1359
|
```ruby
|
1094
|
-
ENV["ELASTICSEARCH_URL"] = "https://user:password@host"
|
1360
|
+
ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
|
1361
|
+
# or
|
1362
|
+
ENV["OPENSEARCH_URL"] = "https://user:password@host:port"
|
1095
1363
|
```
|
1096
1364
|
|
1097
1365
|
Then deploy and reindex:
|
1098
1366
|
|
1099
1367
|
```sh
|
1100
|
-
rake searchkick:reindex
|
1368
|
+
rake searchkick:reindex:all
|
1101
1369
|
```
|
1102
1370
|
|
1103
1371
|
### Data Protection
|
1104
1372
|
|
1105
|
-
We recommend encrypting data at rest and in transit (even inside your own network). This is especially important if you send [personal data](https://en.wikipedia.org/wiki/Personally_identifiable_information) of your users to
|
1373
|
+
We recommend encrypting data at rest and in transit (even inside your own network). This is especially important if you send [personal data](https://en.wikipedia.org/wiki/Personally_identifiable_information) of your users to the search server.
|
1106
1374
|
|
1107
|
-
Bonsai, Elastic Cloud, and Amazon
|
1375
|
+
Bonsai, Elastic Cloud, and Amazon OpenSearch Service all support encryption at rest and HTTPS.
|
1108
1376
|
|
1109
1377
|
### Automatic Failover
|
1110
1378
|
|
@@ -1137,7 +1405,7 @@ See [Production Rails](https://github.com/ankane/production_rails) for other goo
|
|
1137
1405
|
Significantly increase performance with faster JSON generation. Add [Oj](https://github.com/ohler55/oj) to your Gemfile.
|
1138
1406
|
|
1139
1407
|
```ruby
|
1140
|
-
gem
|
1408
|
+
gem "oj"
|
1141
1409
|
```
|
1142
1410
|
|
1143
1411
|
This speeds up all JSON generation and parsing in your application (automatically!)
|
@@ -1147,7 +1415,7 @@ This speeds up all JSON generation and parsing in your application (automaticall
|
|
1147
1415
|
Significantly increase performance with persistent HTTP connections. Add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile and it’ll automatically be used.
|
1148
1416
|
|
1149
1417
|
```ruby
|
1150
|
-
gem
|
1418
|
+
gem "typhoeus"
|
1151
1419
|
```
|
1152
1420
|
|
1153
1421
|
To reduce log noise, create an initializer with:
|
@@ -1160,7 +1428,7 @@ If you run into issues on Windows, check out [this post](https://www.rastating.c
|
|
1160
1428
|
|
1161
1429
|
### Searchable Fields
|
1162
1430
|
|
1163
|
-
By default, all string fields are searchable (can be used in `fields` option). Speed up indexing and reduce index size by only making some fields searchable.
|
1431
|
+
By default, all string fields are searchable (can be used in `fields` option). Speed up indexing and reduce index size by only making some fields searchable.
|
1164
1432
|
|
1165
1433
|
```ruby
|
1166
1434
|
class Product < ApplicationRecord
|
@@ -1216,7 +1484,7 @@ Product.reindex(async: {wait: true})
|
|
1216
1484
|
You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
|
1217
1485
|
|
1218
1486
|
```ruby
|
1219
|
-
gem
|
1487
|
+
gem "activejob-traffic_control", ">= 0.1.3"
|
1220
1488
|
```
|
1221
1489
|
|
1222
1490
|
And create an initializer with:
|
@@ -1249,7 +1517,7 @@ Product.search_index.promote(index_name, update_refresh_interval: true)
|
|
1249
1517
|
|
1250
1518
|
### Queuing
|
1251
1519
|
|
1252
|
-
Push ids of records needing
|
1520
|
+
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).
|
1253
1521
|
|
1254
1522
|
```ruby
|
1255
1523
|
Searchkick.redis = ConnectionPool.new { Redis.new }
|
@@ -1279,7 +1547,7 @@ For more tips, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/
|
|
1279
1547
|
|
1280
1548
|
### Routing
|
1281
1549
|
|
1282
|
-
Searchkick supports [
|
1550
|
+
Searchkick supports [routing](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
|
1283
1551
|
|
1284
1552
|
```ruby
|
1285
1553
|
class Business < ApplicationRecord
|
@@ -1352,14 +1620,14 @@ class ReindexConversionsJob < ApplicationJob
|
|
1352
1620
|
# get records that have a recent conversion
|
1353
1621
|
recently_converted_ids =
|
1354
1622
|
Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
|
1355
|
-
.order(:convertable_id).
|
1623
|
+
.order(:convertable_id).distinct.pluck(:convertable_id)
|
1356
1624
|
|
1357
1625
|
# split into groups
|
1358
1626
|
recently_converted_ids.in_groups_of(1000, false) do |ids|
|
1359
1627
|
# fetch conversions
|
1360
1628
|
conversions =
|
1361
1629
|
Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
|
1362
|
-
.group(:convertable_id, :query).
|
1630
|
+
.group(:convertable_id, :query).distinct.count(:user_id)
|
1363
1631
|
|
1364
1632
|
# group conversions by record
|
1365
1633
|
conversions_by_record = {}
|
@@ -1387,7 +1655,7 @@ ReindexConversionsJob.perform_later("Product")
|
|
1387
1655
|
|
1388
1656
|
## Advanced
|
1389
1657
|
|
1390
|
-
Searchkick makes it easy to use the Elasticsearch DSL on its own.
|
1658
|
+
Searchkick makes it easy to use the Elasticsearch or OpenSearch DSL on its own.
|
1391
1659
|
|
1392
1660
|
### Advanced Mapping
|
1393
1661
|
|
@@ -1396,10 +1664,8 @@ Create a custom mapping:
|
|
1396
1664
|
```ruby
|
1397
1665
|
class Product < ApplicationRecord
|
1398
1666
|
searchkick mappings: {
|
1399
|
-
|
1400
|
-
|
1401
|
-
name: {type: "keyword"}
|
1402
|
-
}
|
1667
|
+
properties: {
|
1668
|
+
name: {type: "keyword"}
|
1403
1669
|
}
|
1404
1670
|
}
|
1405
1671
|
end
|
@@ -1443,7 +1709,7 @@ products =
|
|
1443
1709
|
end
|
1444
1710
|
```
|
1445
1711
|
|
1446
|
-
###
|
1712
|
+
### Client
|
1447
1713
|
|
1448
1714
|
Searchkick is built on top of the [elasticsearch](https://github.com/elastic/elasticsearch-ruby) gem. To access the client directly, use:
|
1449
1715
|
|
@@ -1456,8 +1722,8 @@ Searchkick.client
|
|
1456
1722
|
To batch search requests for performance, use:
|
1457
1723
|
|
1458
1724
|
```ruby
|
1459
|
-
products = Product.search("snacks"
|
1460
|
-
coupons = Coupon.search("snacks"
|
1725
|
+
products = Product.search("snacks")
|
1726
|
+
coupons = Coupon.search("snacks")
|
1461
1727
|
Searchkick.multi_search([products, coupons])
|
1462
1728
|
```
|
1463
1729
|
|
@@ -1479,6 +1745,49 @@ Boost specific models with:
|
|
1479
1745
|
indices_boost: {Category => 2, Product => 1}
|
1480
1746
|
```
|
1481
1747
|
|
1748
|
+
## Multi-Tenancy
|
1749
|
+
|
1750
|
+
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.
|
1751
|
+
|
1752
|
+
## Scroll API
|
1753
|
+
|
1754
|
+
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.
|
1755
|
+
|
1756
|
+
```ruby
|
1757
|
+
Product.search("*", scroll: "1m").scroll do |batch|
|
1758
|
+
# process batch ...
|
1759
|
+
end
|
1760
|
+
```
|
1761
|
+
|
1762
|
+
You can also scroll batches manually.
|
1763
|
+
|
1764
|
+
```ruby
|
1765
|
+
products = Product.search "*", scroll: "1m"
|
1766
|
+
while products.any?
|
1767
|
+
# process batch ...
|
1768
|
+
|
1769
|
+
products = products.scroll
|
1770
|
+
end
|
1771
|
+
|
1772
|
+
products.clear_scroll
|
1773
|
+
```
|
1774
|
+
|
1775
|
+
## Deep Paging
|
1776
|
+
|
1777
|
+
By default, Elasticsearch and OpenSearch limit 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:
|
1778
|
+
|
1779
|
+
```ruby
|
1780
|
+
class Product < ApplicationRecord
|
1781
|
+
searchkick deep_paging: true
|
1782
|
+
end
|
1783
|
+
```
|
1784
|
+
|
1785
|
+
If you just need an accurate total count, you can instead use:
|
1786
|
+
|
1787
|
+
```ruby
|
1788
|
+
Product.search("pears", body_options: {track_total_hits: true})
|
1789
|
+
```
|
1790
|
+
|
1482
1791
|
## Nested Data
|
1483
1792
|
|
1484
1793
|
To query nested data, use dot notation.
|
@@ -1570,7 +1879,7 @@ class Product < ApplicationRecord
|
|
1570
1879
|
def search_data
|
1571
1880
|
{
|
1572
1881
|
name: name,
|
1573
|
-
unique_user_conversions: searches.group(:query).
|
1882
|
+
unique_user_conversions: searches.group(:query).distinct.count(:user_id),
|
1574
1883
|
# {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
|
1575
1884
|
total_conversions: searches.group(:query).count
|
1576
1885
|
# {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
|
@@ -1646,14 +1955,6 @@ class Product < ApplicationRecord
|
|
1646
1955
|
end
|
1647
1956
|
```
|
1648
1957
|
|
1649
|
-
Turn off stemming
|
1650
|
-
|
1651
|
-
```ruby
|
1652
|
-
class Product < ApplicationRecord
|
1653
|
-
searchkick stem: false
|
1654
|
-
end
|
1655
|
-
```
|
1656
|
-
|
1657
1958
|
Turn on stemming for conversions
|
1658
1959
|
|
1659
1960
|
```ruby
|
@@ -1662,14 +1963,6 @@ class Product < ApplicationRecord
|
|
1662
1963
|
end
|
1663
1964
|
```
|
1664
1965
|
|
1665
|
-
Use a different [similarity algorithm](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html) for scoring
|
1666
|
-
|
1667
|
-
```ruby
|
1668
|
-
class Product < ApplicationRecord
|
1669
|
-
searchkick similarity: "classic"
|
1670
|
-
end
|
1671
|
-
```
|
1672
|
-
|
1673
1966
|
Make search case-sensitive
|
1674
1967
|
|
1675
1968
|
```ruby
|
@@ -1704,14 +1997,7 @@ class Product < ApplicationRecord
|
|
1704
1997
|
end
|
1705
1998
|
```
|
1706
1999
|
|
1707
|
-
|
1708
|
-
|
1709
|
-
```ruby
|
1710
|
-
products = Product.search("carrots", execute: false)
|
1711
|
-
products.each { ... } # search not executed until here
|
1712
|
-
```
|
1713
|
-
|
1714
|
-
Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html), like `search_type` and `query_cache`
|
2000
|
+
Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
|
1715
2001
|
|
1716
2002
|
```ruby
|
1717
2003
|
Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
|
@@ -1748,170 +2034,62 @@ Turn on misspellings after a certain number of characters
|
|
1748
2034
|
Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
|
1749
2035
|
```
|
1750
2036
|
|
1751
|
-
**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off
|
2037
|
+
**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch
|
1752
2038
|
|
1753
2039
|
```ruby
|
1754
2040
|
Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
|
1755
2041
|
```
|
1756
2042
|
|
1757
|
-
##
|
1758
|
-
|
1759
|
-
For performance, only enable Searchkick callbacks for the tests that need it.
|
1760
|
-
|
1761
|
-
### Minitest
|
1762
|
-
|
1763
|
-
Add to your `test/test_helper.rb`:
|
1764
|
-
|
1765
|
-
```ruby
|
1766
|
-
# reindex models
|
1767
|
-
Product.reindex
|
1768
|
-
|
1769
|
-
# and disable callbacks
|
1770
|
-
Searchkick.disable_callbacks
|
1771
|
-
```
|
1772
|
-
|
1773
|
-
And use:
|
1774
|
-
|
1775
|
-
```ruby
|
1776
|
-
class ProductTest < Minitest::Test
|
1777
|
-
def setup
|
1778
|
-
Searchkick.enable_callbacks
|
1779
|
-
end
|
1780
|
-
|
1781
|
-
def teardown
|
1782
|
-
Searchkick.disable_callbacks
|
1783
|
-
end
|
2043
|
+
## Gotchas
|
1784
2044
|
|
1785
|
-
|
1786
|
-
Product.create!(name: "Apple")
|
1787
|
-
Product.search_index.refresh
|
1788
|
-
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
1789
|
-
end
|
1790
|
-
end
|
1791
|
-
```
|
1792
|
-
|
1793
|
-
### RSpec
|
1794
|
-
|
1795
|
-
Add to your `spec/spec_helper.rb`:
|
1796
|
-
|
1797
|
-
```ruby
|
1798
|
-
RSpec.configure do |config|
|
1799
|
-
config.before(:suite) do
|
1800
|
-
# reindex models
|
1801
|
-
Product.reindex
|
1802
|
-
|
1803
|
-
# and disable callbacks
|
1804
|
-
Searchkick.disable_callbacks
|
1805
|
-
end
|
1806
|
-
|
1807
|
-
config.around(:each, search: true) do |example|
|
1808
|
-
Searchkick.callbacks(true) do
|
1809
|
-
example.run
|
1810
|
-
end
|
1811
|
-
end
|
1812
|
-
end
|
1813
|
-
```
|
2045
|
+
### Consistency
|
1814
2046
|
|
1815
|
-
|
2047
|
+
Elasticsearch and OpenSearch are eventually consistent, meaning it can take up to a second for a change to reflect in search. You can use the `refresh` method to have it show up immediately.
|
1816
2048
|
|
1817
2049
|
```ruby
|
1818
|
-
|
1819
|
-
|
1820
|
-
Product.create!(name: "Apple")
|
1821
|
-
Product.search_index.refresh
|
1822
|
-
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
1823
|
-
end
|
1824
|
-
end
|
2050
|
+
product.save!
|
2051
|
+
Product.search_index.refresh
|
1825
2052
|
```
|
1826
2053
|
|
1827
|
-
###
|
2054
|
+
### Inconsistent Scores
|
1828
2055
|
|
1829
|
-
|
2056
|
+
Due to the distributed nature of Elasticsearch and OpenSearch, you can get incorrect results when the number of documents in the index is low. You can [read more about it here](https://www.elastic.co/blog/understanding-query-then-fetch-vs-dfs-query-then-fetch). To fix this, do:
|
1830
2057
|
|
1831
2058
|
```ruby
|
1832
|
-
|
1833
|
-
|
1834
|
-
# ...
|
1835
|
-
|
1836
|
-
# Note: This should be the last trait in the list so `reindex` is called
|
1837
|
-
# after all the other callbacks complete.
|
1838
|
-
trait :reindex do
|
1839
|
-
after(:create) do |product, _evaluator|
|
1840
|
-
product.reindex(refresh: true)
|
1841
|
-
end
|
1842
|
-
end
|
1843
|
-
end
|
2059
|
+
class Product < ApplicationRecord
|
2060
|
+
searchkick settings: {number_of_shards: 1}
|
1844
2061
|
end
|
1845
|
-
|
1846
|
-
# use it
|
1847
|
-
FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
|
1848
2062
|
```
|
1849
2063
|
|
1850
|
-
|
1851
|
-
|
1852
|
-
Set:
|
1853
|
-
|
1854
|
-
```ruby
|
1855
|
-
Searchkick.index_suffix = ENV["TEST_ENV_NUMBER"]
|
1856
|
-
```
|
1857
|
-
|
1858
|
-
## Multi-Tenancy
|
1859
|
-
|
1860
|
-
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.
|
2064
|
+
For convenience, this is set by default in the test environment.
|
1861
2065
|
|
1862
2066
|
## Upgrading
|
1863
2067
|
|
1864
|
-
|
1865
|
-
|
1866
|
-
## Elasticsearch 6 to 7 Upgrade
|
2068
|
+
### 5.0
|
1867
2069
|
|
1868
|
-
|
1869
|
-
2. Upgrade your Elasticsearch cluster
|
1870
|
-
|
1871
|
-
## Elasticsearch 5 to 6 Upgrade
|
1872
|
-
|
1873
|
-
Elasticsearch 6 removes the ability to reindex with the `_all` field. Before you upgrade, we recommend disabling this field manually and specifying default fields on your models.
|
2070
|
+
Searchkick 5 supports both the `elasticsearch` and `opensearch-ruby` gems. Add the one you want to use to your Gemfile:
|
1874
2071
|
|
1875
2072
|
```ruby
|
1876
|
-
|
1877
|
-
|
1878
|
-
|
2073
|
+
gem "elasticsearch"
|
2074
|
+
# or
|
2075
|
+
gem "opensearch-ruby"
|
1879
2076
|
```
|
1880
2077
|
|
1881
|
-
If
|
2078
|
+
If using the deprecated `faraday_middleware-aws-signers-v4` gem, switch to `faraday_middleware-aws-sigv4`.
|
1882
2079
|
|
1883
|
-
|
1884
|
-
class Product < ApplicationRecord
|
1885
|
-
def search_data
|
1886
|
-
{
|
1887
|
-
all: [name, size, quantity].join(" ")
|
1888
|
-
}
|
1889
|
-
end
|
1890
|
-
end
|
1891
|
-
```
|
1892
|
-
|
1893
|
-
## Elasticsearch Gotchas
|
1894
|
-
|
1895
|
-
### Consistency
|
1896
|
-
|
1897
|
-
Elasticsearch is eventually consistent, meaning it can take up to a second for a change to reflect in search. You can use the `refresh` method to have it show up immediately.
|
2080
|
+
Also, searches now use lazy loading:
|
1898
2081
|
|
1899
2082
|
```ruby
|
1900
|
-
|
1901
|
-
Product.
|
1902
|
-
```
|
1903
|
-
|
1904
|
-
### Inconsistent Scores
|
2083
|
+
# search not executed
|
2084
|
+
Product.search("milk")
|
1905
2085
|
|
1906
|
-
|
1907
|
-
|
1908
|
-
```ruby
|
1909
|
-
class Product < ApplicationRecord
|
1910
|
-
searchkick settings: {number_of_shards: 1}
|
1911
|
-
end
|
2086
|
+
# search executed
|
2087
|
+
Product.search("milk").to_a
|
1912
2088
|
```
|
1913
2089
|
|
1914
|
-
|
2090
|
+
And there’s a [new option](#default-scopes) for models with default scopes.
|
2091
|
+
|
2092
|
+
Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md) for the full list of changes.
|
1915
2093
|
|
1916
2094
|
## History
|
1917
2095
|
|
@@ -1930,13 +2108,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
|
|
1930
2108
|
- Write, clarify, or fix documentation
|
1931
2109
|
- Suggest or add new features
|
1932
2110
|
|
1933
|
-
|
1934
|
-
|
1935
|
-
To get started with development and testing:
|
2111
|
+
To get started with development:
|
1936
2112
|
|
1937
2113
|
```sh
|
1938
2114
|
git clone https://github.com/ankane/searchkick.git
|
1939
2115
|
cd searchkick
|
1940
2116
|
bundle install
|
1941
|
-
rake test
|
2117
|
+
bundle exec rake test
|
1942
2118
|
```
|
2119
|
+
|
2120
|
+
Feel free to open an issue to get feedback on your idea before spending too much time on it.
|