searchkick 4.6.3 → 5.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -1
- data/README.md +321 -203
- data/lib/searchkick/bulk_reindex_job.rb +12 -8
- data/lib/searchkick/controller_runtime.rb +40 -0
- data/lib/searchkick/index.rb +149 -67
- data/lib/searchkick/index_cache.rb +30 -0
- data/lib/searchkick/index_options.rb +19 -69
- data/lib/searchkick/indexer.rb +15 -8
- data/lib/searchkick/log_subscriber.rb +57 -0
- data/lib/searchkick/middleware.rb +9 -2
- data/lib/searchkick/model.rb +49 -50
- data/lib/searchkick/process_batch_job.rb +9 -25
- data/lib/searchkick/process_queue_job.rb +3 -2
- data/lib/searchkick/query.rb +51 -57
- data/lib/searchkick/record_data.rb +1 -1
- data/lib/searchkick/record_indexer.rb +136 -52
- data/lib/searchkick/reindex_queue.rb +36 -8
- data/lib/searchkick/reindex_v2_job.rb +10 -34
- data/lib/searchkick/relation.rb +247 -0
- data/lib/searchkick/relation_indexer.rb +155 -0
- data/lib/searchkick/results.rb +29 -28
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick/where.rb +11 -0
- data/lib/searchkick.rb +175 -98
- data/lib/tasks/searchkick.rake +6 -3
- metadata +12 -28
- data/lib/searchkick/bulk_indexer.rb +0 -173
- data/lib/searchkick/logging.rb +0 -246
data/README.md
CHANGED
@@ -20,7 +20,7 @@ Plus:
|
|
20
20
|
- autocomplete
|
21
21
|
- “Did you mean” suggestions
|
22
22
|
- supports many languages
|
23
|
-
- works with Active Record
|
23
|
+
- works with Active Record and Mongoid
|
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
|
|
@@ -39,7 +39,7 @@ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Au
|
|
39
39
|
- [Testing](#testing)
|
40
40
|
- [Deployment](#deployment)
|
41
41
|
- [Performance](#performance)
|
42
|
-
- [
|
42
|
+
- [Advanced Search](#advanced)
|
43
43
|
- [Reference](#reference)
|
44
44
|
- [Contributing](#contributing)
|
45
45
|
|
@@ -48,17 +48,23 @@ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Au
|
|
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 elastic/tap/elasticsearch-full
|
52
|
+
brew services start elasticsearch-full
|
53
|
+
# or
|
54
|
+
brew install opensearch
|
55
|
+
brew services start opensearch
|
53
56
|
```
|
54
57
|
|
55
|
-
Add
|
58
|
+
Add these lines to your application’s Gemfile:
|
56
59
|
|
57
60
|
```ruby
|
58
|
-
gem
|
61
|
+
gem "searchkick"
|
62
|
+
|
63
|
+
gem "elasticsearch" # select one
|
64
|
+
gem "opensearch-ruby" # select one
|
59
65
|
```
|
60
66
|
|
61
|
-
The latest version works with Elasticsearch
|
67
|
+
The latest version works with Elasticsearch 7 and 8 and OpenSearch 1 and 2. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
|
62
68
|
|
63
69
|
Add searchkick to models you want to search.
|
64
70
|
|
@@ -83,14 +89,14 @@ products.each do |product|
|
|
83
89
|
end
|
84
90
|
```
|
85
91
|
|
86
|
-
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 [
|
92
|
+
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.
|
87
93
|
|
88
94
|
## Querying
|
89
95
|
|
90
96
|
Query like SQL
|
91
97
|
|
92
98
|
```ruby
|
93
|
-
Product.search
|
99
|
+
Product.search("apples", where: {in_stock: true}, limit: 10, offset: 50)
|
94
100
|
```
|
95
101
|
|
96
102
|
Search specific fields
|
@@ -144,7 +150,7 @@ select: [:name]
|
|
144
150
|
|
145
151
|
### Results
|
146
152
|
|
147
|
-
Searches return a `Searchkick::
|
153
|
+
Searches return a `Searchkick::Relation` object. This responds like an array to most methods.
|
148
154
|
|
149
155
|
```ruby
|
150
156
|
results = Product.search("milk")
|
@@ -153,7 +159,7 @@ results.any?
|
|
153
159
|
results.each { |result| ... }
|
154
160
|
```
|
155
161
|
|
156
|
-
By default, ids are fetched from
|
162
|
+
By default, ids are fetched from the search server and records are fetched from your database. To fetch everything from the search server, use:
|
157
163
|
|
158
164
|
```ruby
|
159
165
|
Product.search("apples", load: false)
|
@@ -171,13 +177,13 @@ Get the time the search took (in milliseconds)
|
|
171
177
|
results.took
|
172
178
|
```
|
173
179
|
|
174
|
-
Get the full response from
|
180
|
+
Get the full response from the search server
|
175
181
|
|
176
182
|
```ruby
|
177
183
|
results.response
|
178
184
|
```
|
179
185
|
|
180
|
-
**Note:** By default, Elasticsearch [
|
186
|
+
**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.
|
181
187
|
|
182
188
|
### Boosting
|
183
189
|
|
@@ -218,7 +224,7 @@ You can also boost by:
|
|
218
224
|
Use a `*` for the query.
|
219
225
|
|
220
226
|
```ruby
|
221
|
-
Product.search
|
227
|
+
Product.search("*")
|
222
228
|
```
|
223
229
|
|
224
230
|
### Pagination
|
@@ -227,7 +233,7 @@ Plays nicely with kaminari and will_paginate.
|
|
227
233
|
|
228
234
|
```ruby
|
229
235
|
# controller
|
230
|
-
@products = Product.search
|
236
|
+
@products = Product.search("milk", page: params[:page], per_page: 20)
|
231
237
|
```
|
232
238
|
|
233
239
|
View with kaminari
|
@@ -247,13 +253,13 @@ View with will_paginate
|
|
247
253
|
By default, results must match all words in the query.
|
248
254
|
|
249
255
|
```ruby
|
250
|
-
Product.search
|
256
|
+
Product.search("fresh honey") # fresh AND honey
|
251
257
|
```
|
252
258
|
|
253
259
|
To change this, use:
|
254
260
|
|
255
261
|
```ruby
|
256
|
-
Product.search
|
262
|
+
Product.search("fresh honey", operator: "or") # fresh OR honey
|
257
263
|
```
|
258
264
|
|
259
265
|
By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
|
@@ -267,7 +273,7 @@ end
|
|
267
273
|
And to search (after you reindex):
|
268
274
|
|
269
275
|
```ruby
|
270
|
-
Product.search
|
276
|
+
Product.search("back", fields: [:name], match: :word_start)
|
271
277
|
```
|
272
278
|
|
273
279
|
Available options are:
|
@@ -284,12 +290,18 @@ Option | Matches | Example
|
|
284
290
|
|
285
291
|
The default is `:word`. The most matches will happen with `:word_middle`.
|
286
292
|
|
293
|
+
To specify different matching for different fields, use:
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
Product.search(query, fields: [{name: :word_start}, {brand: :word_middle}])
|
297
|
+
```
|
298
|
+
|
287
299
|
### Exact Matches
|
288
300
|
|
289
301
|
To match a field exactly (case-sensitive), use:
|
290
302
|
|
291
303
|
```ruby
|
292
|
-
|
304
|
+
Product.search(query, fields: [{name: :exact}])
|
293
305
|
```
|
294
306
|
|
295
307
|
### Phrase Matches
|
@@ -297,7 +309,7 @@ User.search query, fields: [{email: :exact}, :name]
|
|
297
309
|
To only match the exact order, use:
|
298
310
|
|
299
311
|
```ruby
|
300
|
-
|
312
|
+
Product.search("fresh honey", match: :phrase)
|
301
313
|
```
|
302
314
|
|
303
315
|
### Stemming and Language
|
@@ -315,11 +327,11 @@ end
|
|
315
327
|
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:
|
316
328
|
|
317
329
|
- `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
|
318
|
-
- `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/
|
319
|
-
- `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/
|
330
|
+
- `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-smartcn.html)
|
331
|
+
- `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-kuromoji.html)
|
320
332
|
- `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
|
321
|
-
- `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/
|
322
|
-
- `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/
|
333
|
+
- `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-nori.html)
|
334
|
+
- `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-stempel.html)
|
323
335
|
- `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
|
324
336
|
- `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
|
325
337
|
|
@@ -375,9 +387,9 @@ search_synonyms: ["lightbulb => halogenlamp"]
|
|
375
387
|
|
376
388
|
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.
|
377
389
|
|
378
|
-
#### Elasticsearch 7.3+
|
390
|
+
#### Elasticsearch 7.3+ and OpenSearch
|
379
391
|
|
380
|
-
For Elasticsearch 7.3+
|
392
|
+
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.
|
381
393
|
|
382
394
|
```txt
|
383
395
|
pop, soda
|
@@ -418,7 +430,7 @@ end
|
|
418
430
|
Search with:
|
419
431
|
|
420
432
|
```ruby
|
421
|
-
Product.search
|
433
|
+
Product.search(query, fields: [:name_tagged])
|
422
434
|
```
|
423
435
|
|
424
436
|
### Misspellings
|
@@ -428,13 +440,13 @@ By default, Searchkick handles misspelled queries by returning results with an [
|
|
428
440
|
You can change this with:
|
429
441
|
|
430
442
|
```ruby
|
431
|
-
Product.search
|
443
|
+
Product.search("zucini", misspellings: {edit_distance: 2}) # zucchini
|
432
444
|
```
|
433
445
|
|
434
446
|
To prevent poor precision and improve performance for correctly spelled queries (which should be a majority for most applications), Searchkick can first perform a search without misspellings, and if there are too few results, perform another with them.
|
435
447
|
|
436
448
|
```ruby
|
437
|
-
Product.search
|
449
|
+
Product.search("zuchini", misspellings: {below: 5})
|
438
450
|
```
|
439
451
|
|
440
452
|
If there are fewer than 5 results, a 2nd search is performed with misspellings enabled. The result of this query is returned.
|
@@ -442,13 +454,13 @@ If there are fewer than 5 results, a 2nd search is performed with misspellings e
|
|
442
454
|
Turn off misspellings with:
|
443
455
|
|
444
456
|
```ruby
|
445
|
-
Product.search
|
457
|
+
Product.search("zuchini", misspellings: false) # no zucchini
|
446
458
|
```
|
447
459
|
|
448
460
|
Specify which fields can include misspellings with:
|
449
461
|
|
450
462
|
```ruby
|
451
|
-
Product.search
|
463
|
+
Product.search("zucini", fields: [:name, :color], misspellings: {fields: [:name]})
|
452
464
|
```
|
453
465
|
|
454
466
|
> When doing this, you must also specify fields to search
|
@@ -458,7 +470,7 @@ Product.search "zucini", fields: [:name, :color], misspellings: {fields: [:name]
|
|
458
470
|
If a user searches `butter`, they may also get results for `peanut butter`. To prevent this, use:
|
459
471
|
|
460
472
|
```ruby
|
461
|
-
Product.search
|
473
|
+
Product.search("butter", exclude: ["peanut butter"])
|
462
474
|
```
|
463
475
|
|
464
476
|
You can map queries and terms to exclude with:
|
@@ -469,7 +481,7 @@ exclude_queries = {
|
|
469
481
|
"cream" => ["ice cream", "whipped cream"]
|
470
482
|
}
|
471
483
|
|
472
|
-
Product.search
|
484
|
+
Product.search(query, exclude: exclude_queries[query])
|
473
485
|
```
|
474
486
|
|
475
487
|
You can demote results by boosting by a factor less than one:
|
@@ -485,13 +497,13 @@ Search :ice_cream::cake: and get `ice cream cake`!
|
|
485
497
|
Add this line to your application’s Gemfile:
|
486
498
|
|
487
499
|
```ruby
|
488
|
-
gem
|
500
|
+
gem "gemoji-parser"
|
489
501
|
```
|
490
502
|
|
491
503
|
And use:
|
492
504
|
|
493
505
|
```ruby
|
494
|
-
Product.search
|
506
|
+
Product.search("🍨🍰", emoji: true)
|
495
507
|
```
|
496
508
|
|
497
509
|
## Indexing
|
@@ -520,12 +532,10 @@ class Product < ApplicationRecord
|
|
520
532
|
end
|
521
533
|
```
|
522
534
|
|
523
|
-
By default, all records are indexed. To control which records are indexed, use the `should_index?` method
|
535
|
+
By default, all records are indexed. To control which records are indexed, use the `should_index?` method.
|
524
536
|
|
525
537
|
```ruby
|
526
538
|
class Product < ApplicationRecord
|
527
|
-
scope :search_import, -> { where(active: true) }
|
528
|
-
|
529
539
|
def should_index?
|
530
540
|
active # only index active records
|
531
541
|
end
|
@@ -586,11 +596,19 @@ There are four strategies for keeping the index synced with your database.
|
|
586
596
|
end
|
587
597
|
```
|
588
598
|
|
599
|
+
And reindex a record or relation manually.
|
600
|
+
|
601
|
+
```ruby
|
602
|
+
product.reindex
|
603
|
+
# or
|
604
|
+
store.products.reindex(mode: :async)
|
605
|
+
```
|
606
|
+
|
589
607
|
You can also do bulk updates.
|
590
608
|
|
591
609
|
```ruby
|
592
610
|
Searchkick.callbacks(:bulk) do
|
593
|
-
|
611
|
+
Product.find_each(&:update_fields)
|
594
612
|
end
|
595
613
|
```
|
596
614
|
|
@@ -598,10 +616,16 @@ Or temporarily skip updates.
|
|
598
616
|
|
599
617
|
```ruby
|
600
618
|
Searchkick.callbacks(false) do
|
601
|
-
|
619
|
+
Product.find_each(&:update_fields)
|
602
620
|
end
|
603
621
|
```
|
604
622
|
|
623
|
+
Or override the model’s strategy.
|
624
|
+
|
625
|
+
```ruby
|
626
|
+
product.reindex(mode: :async) # :inline or :queue
|
627
|
+
```
|
628
|
+
|
605
629
|
### Associations
|
606
630
|
|
607
631
|
Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
|
@@ -618,28 +642,46 @@ class Image < ApplicationRecord
|
|
618
642
|
end
|
619
643
|
```
|
620
644
|
|
621
|
-
|
645
|
+
### Default Scopes
|
622
646
|
|
623
|
-
|
647
|
+
If you have a default scope that filters records, use the `should_index?` method to exclude them from indexing:
|
624
648
|
|
625
649
|
```ruby
|
626
|
-
Product
|
650
|
+
class Product < ApplicationRecord
|
651
|
+
default_scope { where(deleted_at: nil) }
|
652
|
+
|
653
|
+
def should_index?
|
654
|
+
deleted_at.nil?
|
655
|
+
end
|
656
|
+
end
|
627
657
|
```
|
628
658
|
|
629
|
-
|
659
|
+
If you want to index and search filtered records, set:
|
630
660
|
|
631
|
-
|
661
|
+
```ruby
|
662
|
+
class Product < ApplicationRecord
|
663
|
+
searchkick unscope: true
|
664
|
+
end
|
665
|
+
```
|
632
666
|
|
633
|
-
|
634
|
-
- top searches with no results
|
667
|
+
## Intelligent Search
|
635
668
|
|
636
|
-
|
669
|
+
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.
|
670
|
+
|
671
|
+
```ruby
|
672
|
+
Product.search("apple", track: {user_id: current_user.id})
|
673
|
+
```
|
674
|
+
|
675
|
+
[See the docs](https://github.com/ankane/searchjoy) for how to install and use. Focus on top searches with a low conversion rate.
|
676
|
+
|
677
|
+
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. This can make a huge difference on the quality of your search.
|
637
678
|
|
638
679
|
Add conversion data with:
|
639
680
|
|
640
681
|
```ruby
|
641
682
|
class Product < ApplicationRecord
|
642
|
-
has_many :
|
683
|
+
has_many :conversions, class_name: "Searchjoy::Conversion", as: :convertable
|
684
|
+
has_many :searches, class_name: "Searchjoy::Search", through: :conversions
|
643
685
|
|
644
686
|
searchkick conversions: [:conversions] # name of field
|
645
687
|
|
@@ -653,15 +695,100 @@ class Product < ApplicationRecord
|
|
653
695
|
end
|
654
696
|
```
|
655
697
|
|
656
|
-
Reindex and set up a cron job to add new conversions daily.
|
698
|
+
Reindex and set up a cron job to add new conversions daily. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
|
699
|
+
|
700
|
+
### Performant Conversions
|
701
|
+
|
702
|
+
A performant way to do conversions is to cache them to prevent N+1 queries. For Postgres, create a migration with:
|
703
|
+
|
704
|
+
```ruby
|
705
|
+
add_column :products, :search_conversions, :jsonb
|
706
|
+
```
|
707
|
+
|
708
|
+
For MySQL, use `:json`, and for others, use `:text` with a [JSON serializer](https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html).
|
709
|
+
|
710
|
+
Next, update your model. Create a separate method for conversion data so you can use [partial reindexing](#partial-reindexing).
|
711
|
+
|
712
|
+
```ruby
|
713
|
+
class Product < ApplicationRecord
|
714
|
+
searchkick conversions: [:conversions]
|
715
|
+
|
716
|
+
def search_data
|
717
|
+
{
|
718
|
+
name: name,
|
719
|
+
category: category
|
720
|
+
}.merge(conversions_data)
|
721
|
+
end
|
722
|
+
|
723
|
+
def conversions_data
|
724
|
+
{
|
725
|
+
conversions: search_conversions || {}
|
726
|
+
}
|
727
|
+
end
|
728
|
+
end
|
729
|
+
```
|
730
|
+
|
731
|
+
Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
|
732
|
+
|
733
|
+
```ruby
|
734
|
+
Product.reindex
|
735
|
+
```
|
736
|
+
|
737
|
+
Then, create a job to update the conversions column and reindex records with new conversions. Here’s one you can use for Searchjoy:
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
class UpdateConversionsJob < ApplicationJob
|
741
|
+
def perform(class_name, since: nil, update: true, reindex: true)
|
742
|
+
model = Searchkick.load_model(class_name)
|
743
|
+
|
744
|
+
# get records that have a recent conversion
|
745
|
+
recently_converted_ids =
|
746
|
+
Searchjoy::Conversion.where(convertable_type: class_name).where(created_at: since..)
|
747
|
+
.order(:convertable_id).distinct.pluck(:convertable_id)
|
748
|
+
|
749
|
+
# split into batches
|
750
|
+
recently_converted_ids.in_groups_of(1000, false) do |ids|
|
751
|
+
if update
|
752
|
+
# fetch conversions
|
753
|
+
conversions =
|
754
|
+
Searchjoy::Conversion.where(convertable_id: ids, convertable_type: class_name)
|
755
|
+
.joins(:search).where.not(searchjoy_searches: {user_id: nil})
|
756
|
+
.group(:convertable_id, :query).distinct.count(:user_id)
|
757
|
+
|
758
|
+
# group by record
|
759
|
+
conversions_by_record = {}
|
760
|
+
conversions.each do |(id, query), count|
|
761
|
+
(conversions_by_record[id] ||= {})[query] = count
|
762
|
+
end
|
763
|
+
|
764
|
+
# update conversions column
|
765
|
+
model.transaction do
|
766
|
+
conversions_by_record.each do |id, conversions|
|
767
|
+
model.where(id: id).update_all(search_conversions: conversions)
|
768
|
+
end
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
if reindex
|
773
|
+
# reindex conversions data
|
774
|
+
model.where(id: ids).reindex(:conversions_data)
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
```
|
780
|
+
|
781
|
+
Run the job:
|
657
782
|
|
658
783
|
```ruby
|
659
|
-
|
784
|
+
UpdateConversionsJob.perform_now("Product")
|
660
785
|
```
|
661
786
|
|
662
|
-
|
787
|
+
And set it up to run daily.
|
663
788
|
|
664
|
-
|
789
|
+
```ruby
|
790
|
+
UpdateConversionsJob.perform_later("Product", since: 1.day.ago)
|
791
|
+
```
|
665
792
|
|
666
793
|
## Personalized Results
|
667
794
|
|
@@ -681,7 +808,7 @@ end
|
|
681
808
|
Reindex and search with:
|
682
809
|
|
683
810
|
```ruby
|
684
|
-
Product.search
|
811
|
+
Product.search("milk", boost_where: {orderer_ids: current_user.id})
|
685
812
|
```
|
686
813
|
|
687
814
|
## Instant Search / Autocomplete
|
@@ -705,7 +832,7 @@ end
|
|
705
832
|
Reindex and search with:
|
706
833
|
|
707
834
|
```ruby
|
708
|
-
Movie.search
|
835
|
+
Movie.search("jurassic pa", fields: [:title], match: :word_start)
|
709
836
|
```
|
710
837
|
|
711
838
|
Typically, you want to use a JavaScript library like [typeahead.js](https://twitter.github.io/typeahead.js/) or [jQuery UI](https://jqueryui.com/autocomplete/).
|
@@ -765,7 +892,7 @@ end
|
|
765
892
|
Reindex and search with:
|
766
893
|
|
767
894
|
```ruby
|
768
|
-
products = Product.search
|
895
|
+
products = Product.search("peantu butta", suggest: true)
|
769
896
|
products.suggestions # ["peanut butter"]
|
770
897
|
```
|
771
898
|
|
@@ -776,40 +903,40 @@ products.suggestions # ["peanut butter"]
|
|
776
903
|
![Aggregations](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/facets.png)
|
777
904
|
|
778
905
|
```ruby
|
779
|
-
products = Product.search
|
906
|
+
products = Product.search("chuck taylor", aggs: [:product_type, :gender, :brand])
|
780
907
|
products.aggs
|
781
908
|
```
|
782
909
|
|
783
910
|
By default, `where` conditions apply to aggregations.
|
784
911
|
|
785
912
|
```ruby
|
786
|
-
Product.search
|
913
|
+
Product.search("wingtips", where: {color: "brandy"}, aggs: [:size])
|
787
914
|
# aggregations for brandy wingtips are returned
|
788
915
|
```
|
789
916
|
|
790
917
|
Change this with:
|
791
918
|
|
792
919
|
```ruby
|
793
|
-
Product.search
|
920
|
+
Product.search("wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false)
|
794
921
|
# aggregations for all wingtips are returned
|
795
922
|
```
|
796
923
|
|
797
924
|
Set `where` conditions for each aggregation separately with:
|
798
925
|
|
799
926
|
```ruby
|
800
|
-
Product.search
|
927
|
+
Product.search("wingtips", aggs: {size: {where: {color: "brandy"}}})
|
801
928
|
```
|
802
929
|
|
803
930
|
Limit
|
804
931
|
|
805
932
|
```ruby
|
806
|
-
Product.search
|
933
|
+
Product.search("apples", aggs: {store_id: {limit: 10}})
|
807
934
|
```
|
808
935
|
|
809
936
|
Order
|
810
937
|
|
811
938
|
```ruby
|
812
|
-
Product.search
|
939
|
+
Product.search("wingtips", aggs: {color: {order: {"_key" => "asc"}}}) # alphabetically
|
813
940
|
```
|
814
941
|
|
815
942
|
[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)
|
@@ -818,31 +945,31 @@ Ranges
|
|
818
945
|
|
819
946
|
```ruby
|
820
947
|
price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
|
821
|
-
Product.search
|
948
|
+
Product.search("*", aggs: {price: {ranges: price_ranges}})
|
822
949
|
```
|
823
950
|
|
824
951
|
Minimum document count
|
825
952
|
|
826
953
|
```ruby
|
827
|
-
Product.search
|
954
|
+
Product.search("apples", aggs: {store_id: {min_doc_count: 2}})
|
828
955
|
```
|
829
956
|
|
830
957
|
Script support
|
831
958
|
|
832
959
|
```ruby
|
833
|
-
Product.search
|
960
|
+
Product.search("*", aggs: {color: {script: {source: "'Color: ' + _value"}}})
|
834
961
|
```
|
835
962
|
|
836
963
|
Date histogram
|
837
964
|
|
838
965
|
```ruby
|
839
|
-
Product.search
|
966
|
+
Product.search("pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}})
|
840
967
|
```
|
841
968
|
|
842
969
|
For other aggregation types, including sub-aggregations, use `body_options`:
|
843
970
|
|
844
971
|
```ruby
|
845
|
-
Product.search
|
972
|
+
Product.search("orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}})
|
846
973
|
```
|
847
974
|
|
848
975
|
## Highlight
|
@@ -850,7 +977,7 @@ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price
|
|
850
977
|
Specify which fields to index with highlighting.
|
851
978
|
|
852
979
|
```ruby
|
853
|
-
class
|
980
|
+
class Band < ApplicationRecord
|
854
981
|
searchkick highlight: [:name]
|
855
982
|
end
|
856
983
|
```
|
@@ -858,7 +985,7 @@ end
|
|
858
985
|
Highlight the search query in the results.
|
859
986
|
|
860
987
|
```ruby
|
861
|
-
bands = Band.search
|
988
|
+
bands = Band.search("cinema", highlight: true)
|
862
989
|
```
|
863
990
|
|
864
991
|
View the highlighted fields with:
|
@@ -872,19 +999,19 @@ end
|
|
872
999
|
To change the tag, use:
|
873
1000
|
|
874
1001
|
```ruby
|
875
|
-
Band.search
|
1002
|
+
Band.search("cinema", highlight: {tag: "<strong>"})
|
876
1003
|
```
|
877
1004
|
|
878
1005
|
To highlight and search different fields, use:
|
879
1006
|
|
880
1007
|
```ruby
|
881
|
-
Band.search
|
1008
|
+
Band.search("cinema", fields: [:name], highlight: {fields: [:description]})
|
882
1009
|
```
|
883
1010
|
|
884
1011
|
By default, the entire field is highlighted. To get small snippets instead, use:
|
885
1012
|
|
886
1013
|
```ruby
|
887
|
-
bands = Band.search
|
1014
|
+
bands = Band.search("cinema", highlight: {fragment_size: 20})
|
888
1015
|
bands.with_highlights(multiple: true).each do |band, highlights|
|
889
1016
|
highlights[:name].join(" and ")
|
890
1017
|
end
|
@@ -893,7 +1020,7 @@ end
|
|
893
1020
|
Additional options can be specified for each field:
|
894
1021
|
|
895
1022
|
```ruby
|
896
|
-
Band.search
|
1023
|
+
Band.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}})
|
897
1024
|
```
|
898
1025
|
|
899
1026
|
You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
|
@@ -922,13 +1049,13 @@ end
|
|
922
1049
|
Reindex and search with:
|
923
1050
|
|
924
1051
|
```ruby
|
925
|
-
Restaurant.search
|
1052
|
+
Restaurant.search("pizza", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}}) # or 160km
|
926
1053
|
```
|
927
1054
|
|
928
1055
|
Bounded by a box
|
929
1056
|
|
930
1057
|
```ruby
|
931
|
-
Restaurant.search
|
1058
|
+
Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}})
|
932
1059
|
```
|
933
1060
|
|
934
1061
|
**Note:** `top_right` and `bottom_left` also work
|
@@ -936,7 +1063,7 @@ Restaurant.search "sushi", where: {location: {top_left: {lat: 38, lon: -123}, bo
|
|
936
1063
|
Bounded by a polygon
|
937
1064
|
|
938
1065
|
```ruby
|
939
|
-
Restaurant.search
|
1066
|
+
Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}})
|
940
1067
|
```
|
941
1068
|
|
942
1069
|
### Boost By Distance
|
@@ -944,13 +1071,13 @@ Restaurant.search "dessert", where: {location: {geo_polygon: {points: [{lat: 38,
|
|
944
1071
|
Boost results by distance - closer results are boosted more
|
945
1072
|
|
946
1073
|
```ruby
|
947
|
-
Restaurant.search
|
1074
|
+
Restaurant.search("noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}})
|
948
1075
|
```
|
949
1076
|
|
950
1077
|
Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
|
951
1078
|
|
952
1079
|
```ruby
|
953
|
-
Restaurant.search
|
1080
|
+
Restaurant.search("wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}})
|
954
1081
|
```
|
955
1082
|
|
956
1083
|
### Geo Shapes
|
@@ -977,19 +1104,19 @@ See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsea
|
|
977
1104
|
Find shapes intersecting with the query shape
|
978
1105
|
|
979
1106
|
```ruby
|
980
|
-
Restaurant.search
|
1107
|
+
Restaurant.search("soup", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}})
|
981
1108
|
```
|
982
1109
|
|
983
1110
|
Falling entirely within the query shape
|
984
1111
|
|
985
1112
|
```ruby
|
986
|
-
Restaurant.search
|
1113
|
+
Restaurant.search("salad", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: {lat: 38, lon: -123}, radius: "1km"}}})
|
987
1114
|
```
|
988
1115
|
|
989
1116
|
Not touching the query shape
|
990
1117
|
|
991
1118
|
```ruby
|
992
|
-
Restaurant.search
|
1119
|
+
Restaurant.search("burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}})
|
993
1120
|
```
|
994
1121
|
|
995
1122
|
## Inheritance
|
@@ -1019,9 +1146,9 @@ Dog.reindex # equivalent, all animals reindexed
|
|
1019
1146
|
And to search, use:
|
1020
1147
|
|
1021
1148
|
```ruby
|
1022
|
-
Animal.search
|
1023
|
-
Dog.search
|
1024
|
-
Animal.search
|
1149
|
+
Animal.search("*") # all animals
|
1150
|
+
Dog.search("*") # just dogs
|
1151
|
+
Animal.search("*", type: [Dog, Cat]) # just cats and dogs
|
1025
1152
|
```
|
1026
1153
|
|
1027
1154
|
**Notes:**
|
@@ -1029,7 +1156,7 @@ Animal.search "*", type: [Dog, Cat] # just cats and dogs
|
|
1029
1156
|
1. The `suggest` option retrieves suggestions from the parent at the moment.
|
1030
1157
|
|
1031
1158
|
```ruby
|
1032
|
-
Dog.search
|
1159
|
+
Dog.search("airbudd", suggest: true) # suggestions for all animals
|
1033
1160
|
```
|
1034
1161
|
2. This relies on a `type` field that is automatically added to the indexed document. Be wary of defining your own `type` field in `search_data`, as it will take precedence.
|
1035
1162
|
|
@@ -1043,13 +1170,13 @@ Product.search("soap", debug: true)
|
|
1043
1170
|
|
1044
1171
|
This prints useful info to `stdout`.
|
1045
1172
|
|
1046
|
-
See how
|
1173
|
+
See how the search server scores your queries with:
|
1047
1174
|
|
1048
1175
|
```ruby
|
1049
1176
|
Product.search("soap", explain: true).response
|
1050
1177
|
```
|
1051
1178
|
|
1052
|
-
See how
|
1179
|
+
See how the search server tokenizes your queries with:
|
1053
1180
|
|
1054
1181
|
```ruby
|
1055
1182
|
Product.search_index.tokens("Dish Washer Soap", analyzer: "searchkick_index")
|
@@ -1072,7 +1199,7 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search")
|
|
1072
1199
|
# ["dieg"] - match!!
|
1073
1200
|
```
|
1074
1201
|
|
1075
|
-
See the [complete list of analyzers](
|
1202
|
+
See the [complete list of analyzers](lib/searchkick/index_options.rb#L36).
|
1076
1203
|
|
1077
1204
|
## Testing
|
1078
1205
|
|
@@ -1223,7 +1350,7 @@ And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy w
|
|
1223
1350
|
|
1224
1351
|
## Deployment
|
1225
1352
|
|
1226
|
-
Searchkick uses `ENV["ELASTICSEARCH_URL"]` for
|
1353
|
+
For the search server, Searchkick uses `ENV["ELASTICSEARCH_URL"]` for Elasticsearch and `ENV["OPENSEARCH_URL"]` for OpenSearch. This defaults to `http://localhost:9200`.
|
1227
1354
|
|
1228
1355
|
- [Elastic Cloud](#elastic-cloud)
|
1229
1356
|
- [Heroku](#heroku)
|
@@ -1248,13 +1375,20 @@ rake searchkick:reindex:all
|
|
1248
1375
|
|
1249
1376
|
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).
|
1250
1377
|
|
1251
|
-
For Bonsai:
|
1378
|
+
For Elasticsearch on Bonsai:
|
1252
1379
|
|
1253
1380
|
```sh
|
1254
|
-
heroku addons:create bonsai
|
1381
|
+
heroku addons:create bonsai
|
1255
1382
|
heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
|
1256
1383
|
```
|
1257
1384
|
|
1385
|
+
For OpenSearch on Bonsai:
|
1386
|
+
|
1387
|
+
```sh
|
1388
|
+
heroku addons:create bonsai --engine=opensearch
|
1389
|
+
heroku config:set OPENSEARCH_URL=`heroku config:get BONSAI_URL`
|
1390
|
+
```
|
1391
|
+
|
1258
1392
|
For SearchBox:
|
1259
1393
|
|
1260
1394
|
```sh
|
@@ -1289,16 +1423,16 @@ heroku run rake searchkick:reindex:all
|
|
1289
1423
|
|
1290
1424
|
### Amazon OpenSearch Service
|
1291
1425
|
|
1292
|
-
Create an initializer `config/initializers/
|
1426
|
+
Create an initializer `config/initializers/opensearch.rb` with:
|
1293
1427
|
|
1294
1428
|
```ruby
|
1295
|
-
ENV["
|
1429
|
+
ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
|
1296
1430
|
```
|
1297
1431
|
|
1298
1432
|
To use signed requests, include in your Gemfile:
|
1299
1433
|
|
1300
1434
|
```ruby
|
1301
|
-
gem
|
1435
|
+
gem "faraday_middleware-aws-sigv4"
|
1302
1436
|
```
|
1303
1437
|
|
1304
1438
|
and add to your initializer:
|
@@ -1319,10 +1453,12 @@ rake searchkick:reindex:all
|
|
1319
1453
|
|
1320
1454
|
### Self-Hosted and Other
|
1321
1455
|
|
1322
|
-
Create an initializer
|
1456
|
+
Create an initializer with:
|
1323
1457
|
|
1324
1458
|
```ruby
|
1325
1459
|
ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
|
1460
|
+
# or
|
1461
|
+
ENV["OPENSEARCH_URL"] = "https://user:password@host:port"
|
1326
1462
|
```
|
1327
1463
|
|
1328
1464
|
Then deploy and reindex:
|
@@ -1333,19 +1469,21 @@ rake searchkick:reindex:all
|
|
1333
1469
|
|
1334
1470
|
### Data Protection
|
1335
1471
|
|
1336
|
-
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
|
1472
|
+
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.
|
1337
1473
|
|
1338
|
-
Bonsai, Elastic Cloud, and Amazon
|
1474
|
+
Bonsai, Elastic Cloud, and Amazon OpenSearch Service all support encryption at rest and HTTPS.
|
1339
1475
|
|
1340
1476
|
### Automatic Failover
|
1341
1477
|
|
1342
|
-
Create an initializer
|
1478
|
+
Create an initializer with multiple hosts:
|
1343
1479
|
|
1344
1480
|
```ruby
|
1345
1481
|
ENV["ELASTICSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
|
1482
|
+
# or
|
1483
|
+
ENV["OPENSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
|
1346
1484
|
```
|
1347
1485
|
|
1348
|
-
See [
|
1486
|
+
See [elastic-transport](https://github.com/elastic/elastic-transport-ruby) or [opensearch-transport](https://github.com/opensearch-project/opensearch-ruby/tree/main/opensearch-transport) for a complete list of options.
|
1349
1487
|
|
1350
1488
|
### Lograge
|
1351
1489
|
|
@@ -1368,7 +1506,7 @@ See [Production Rails](https://github.com/ankane/production_rails) for other goo
|
|
1368
1506
|
Significantly increase performance with faster JSON generation. Add [Oj](https://github.com/ohler55/oj) to your Gemfile.
|
1369
1507
|
|
1370
1508
|
```ruby
|
1371
|
-
gem
|
1509
|
+
gem "oj"
|
1372
1510
|
```
|
1373
1511
|
|
1374
1512
|
This speeds up all JSON generation and parsing in your application (automatically!)
|
@@ -1378,7 +1516,7 @@ This speeds up all JSON generation and parsing in your application (automaticall
|
|
1378
1516
|
Significantly increase performance with persistent HTTP connections. Add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile and it’ll automatically be used.
|
1379
1517
|
|
1380
1518
|
```ruby
|
1381
|
-
gem
|
1519
|
+
gem "typhoeus"
|
1382
1520
|
```
|
1383
1521
|
|
1384
1522
|
To reduce log noise, create an initializer with:
|
@@ -1416,7 +1554,7 @@ end
|
|
1416
1554
|
For large data sets, you can use background jobs to parallelize reindexing.
|
1417
1555
|
|
1418
1556
|
```ruby
|
1419
|
-
Product.reindex(
|
1557
|
+
Product.reindex(mode: :async)
|
1420
1558
|
# {index_name: "products_production_20170111210018065"}
|
1421
1559
|
```
|
1422
1560
|
|
@@ -1441,13 +1579,13 @@ Searchkick.reindex_status(index_name)
|
|
1441
1579
|
You can also have Searchkick wait for reindexing to complete
|
1442
1580
|
|
1443
1581
|
```ruby
|
1444
|
-
Product.reindex(
|
1582
|
+
Product.reindex(mode: :async, wait: true)
|
1445
1583
|
```
|
1446
1584
|
|
1447
1585
|
You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
|
1448
1586
|
|
1449
1587
|
```ruby
|
1450
|
-
gem
|
1588
|
+
gem "activejob-traffic_control", ">= 0.1.3"
|
1451
1589
|
```
|
1452
1590
|
|
1453
1591
|
And create an initializer with:
|
@@ -1467,7 +1605,7 @@ This will allow only 3 jobs to run at once.
|
|
1467
1605
|
You can specify a longer refresh interval while reindexing to increase performance.
|
1468
1606
|
|
1469
1607
|
```ruby
|
1470
|
-
Product.reindex(
|
1608
|
+
Product.reindex(mode: :async, refresh_interval: "30s")
|
1471
1609
|
```
|
1472
1610
|
|
1473
1611
|
**Note:** This only makes a noticable difference with parallel reindexing.
|
@@ -1510,7 +1648,7 @@ For more tips, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/
|
|
1510
1648
|
|
1511
1649
|
### Routing
|
1512
1650
|
|
1513
|
-
Searchkick supports [
|
1651
|
+
Searchkick supports [routing](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
|
1514
1652
|
|
1515
1653
|
```ruby
|
1516
1654
|
class Business < ApplicationRecord
|
@@ -1525,7 +1663,7 @@ end
|
|
1525
1663
|
Reindex and search with:
|
1526
1664
|
|
1527
1665
|
```ruby
|
1528
|
-
Business.search
|
1666
|
+
Business.search("ice cream", routing: params[:city_id])
|
1529
1667
|
```
|
1530
1668
|
|
1531
1669
|
### Partial Reindexing
|
@@ -1536,11 +1674,12 @@ Reindex a subset of attributes to reduce time spent generating search data and c
|
|
1536
1674
|
class Product < ApplicationRecord
|
1537
1675
|
def search_data
|
1538
1676
|
{
|
1539
|
-
name: name
|
1540
|
-
|
1677
|
+
name: name,
|
1678
|
+
category: category
|
1679
|
+
}.merge(prices_data)
|
1541
1680
|
end
|
1542
1681
|
|
1543
|
-
def
|
1682
|
+
def prices_data
|
1544
1683
|
{
|
1545
1684
|
price: price,
|
1546
1685
|
sale_price: sale_price
|
@@ -1552,73 +1691,12 @@ end
|
|
1552
1691
|
And use:
|
1553
1692
|
|
1554
1693
|
```ruby
|
1555
|
-
Product.reindex(:
|
1556
|
-
```
|
1557
|
-
|
1558
|
-
### Performant Conversions
|
1559
|
-
|
1560
|
-
Split out conversions into a separate method so you can use partial reindexing, and cache conversions to prevent N+1 queries. Be sure to use a centralized cache store like Memcached or Redis.
|
1561
|
-
|
1562
|
-
```ruby
|
1563
|
-
class Product < ApplicationRecord
|
1564
|
-
def search_data
|
1565
|
-
{
|
1566
|
-
name: name
|
1567
|
-
}.merge(search_conversions)
|
1568
|
-
end
|
1569
|
-
|
1570
|
-
def search_conversions
|
1571
|
-
{
|
1572
|
-
conversions: Rails.cache.read("search_conversions:#{self.class.name}:#{id}") || {}
|
1573
|
-
}
|
1574
|
-
end
|
1575
|
-
end
|
1576
|
-
```
|
1577
|
-
|
1578
|
-
Create a job to update the cache and reindex records with new conversions.
|
1579
|
-
|
1580
|
-
```ruby
|
1581
|
-
class ReindexConversionsJob < ApplicationJob
|
1582
|
-
def perform(class_name)
|
1583
|
-
# get records that have a recent conversion
|
1584
|
-
recently_converted_ids =
|
1585
|
-
Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
|
1586
|
-
.order(:convertable_id).distinct.pluck(:convertable_id)
|
1587
|
-
|
1588
|
-
# split into groups
|
1589
|
-
recently_converted_ids.in_groups_of(1000, false) do |ids|
|
1590
|
-
# fetch conversions
|
1591
|
-
conversions =
|
1592
|
-
Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
|
1593
|
-
.group(:convertable_id, :query).distinct.count(:user_id)
|
1594
|
-
|
1595
|
-
# group conversions by record
|
1596
|
-
conversions_by_record = {}
|
1597
|
-
conversions.each do |(id, query), count|
|
1598
|
-
(conversions_by_record[id] ||= {})[query] = count
|
1599
|
-
end
|
1600
|
-
|
1601
|
-
# write to cache
|
1602
|
-
conversions_by_record.each do |id, conversions|
|
1603
|
-
Rails.cache.write("search_conversions:#{class_name}:#{id}", conversions)
|
1604
|
-
end
|
1605
|
-
|
1606
|
-
# partial reindex
|
1607
|
-
class_name.constantize.where(id: ids).reindex(:search_conversions)
|
1608
|
-
end
|
1609
|
-
end
|
1610
|
-
end
|
1611
|
-
```
|
1612
|
-
|
1613
|
-
Run the job with:
|
1614
|
-
|
1615
|
-
```ruby
|
1616
|
-
ReindexConversionsJob.perform_later("Product")
|
1694
|
+
Product.reindex(:prices_data)
|
1617
1695
|
```
|
1618
1696
|
|
1619
1697
|
## Advanced
|
1620
1698
|
|
1621
|
-
Searchkick makes it easy to use the Elasticsearch DSL on its own.
|
1699
|
+
Searchkick makes it easy to use the Elasticsearch or OpenSearch DSL on its own.
|
1622
1700
|
|
1623
1701
|
### Advanced Mapping
|
1624
1702
|
|
@@ -1648,7 +1726,7 @@ end
|
|
1648
1726
|
And use the `body` option to search:
|
1649
1727
|
|
1650
1728
|
```ruby
|
1651
|
-
products = Product.search
|
1729
|
+
products = Product.search(body: {query: {match: {name: "milk"}}})
|
1652
1730
|
```
|
1653
1731
|
|
1654
1732
|
View the response with:
|
@@ -1660,21 +1738,21 @@ products.response
|
|
1660
1738
|
To modify the query generated by Searchkick, use:
|
1661
1739
|
|
1662
1740
|
```ruby
|
1663
|
-
products = Product.search
|
1741
|
+
products = Product.search("milk", body_options: {min_score: 1})
|
1664
1742
|
```
|
1665
1743
|
|
1666
1744
|
or
|
1667
1745
|
|
1668
1746
|
```ruby
|
1669
1747
|
products =
|
1670
|
-
Product.search
|
1748
|
+
Product.search("apples") do |body|
|
1671
1749
|
body[:min_score] = 1
|
1672
1750
|
end
|
1673
1751
|
```
|
1674
1752
|
|
1675
|
-
###
|
1753
|
+
### Client
|
1676
1754
|
|
1677
|
-
|
1755
|
+
To access the `Elasticsearch::Client` or `OpenSearch::Client` directly, use:
|
1678
1756
|
|
1679
1757
|
```ruby
|
1680
1758
|
Searchkick.client
|
@@ -1685,8 +1763,8 @@ Searchkick.client
|
|
1685
1763
|
To batch search requests for performance, use:
|
1686
1764
|
|
1687
1765
|
```ruby
|
1688
|
-
products = Product.search("snacks"
|
1689
|
-
coupons = Coupon.search("snacks"
|
1766
|
+
products = Product.search("snacks")
|
1767
|
+
coupons = Coupon.search("snacks")
|
1690
1768
|
Searchkick.multi_search([products, coupons])
|
1691
1769
|
```
|
1692
1770
|
|
@@ -1699,7 +1777,7 @@ Then use `products` and `coupons` as typical results.
|
|
1699
1777
|
Search across multiple models with:
|
1700
1778
|
|
1701
1779
|
```ruby
|
1702
|
-
Searchkick.search
|
1780
|
+
Searchkick.search("milk", models: [Product, Category])
|
1703
1781
|
```
|
1704
1782
|
|
1705
1783
|
Boost specific models with:
|
@@ -1725,7 +1803,7 @@ end
|
|
1725
1803
|
You can also scroll batches manually.
|
1726
1804
|
|
1727
1805
|
```ruby
|
1728
|
-
products = Product.search
|
1806
|
+
products = Product.search("*", scroll: "1m")
|
1729
1807
|
while products.any?
|
1730
1808
|
# process batch ...
|
1731
1809
|
|
@@ -1737,7 +1815,7 @@ products.clear_scroll
|
|
1737
1815
|
|
1738
1816
|
## Deep Paging
|
1739
1817
|
|
1740
|
-
By default, Elasticsearch
|
1818
|
+
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:
|
1741
1819
|
|
1742
1820
|
```ruby
|
1743
1821
|
class Product < ApplicationRecord
|
@@ -1745,7 +1823,7 @@ class Product < ApplicationRecord
|
|
1745
1823
|
end
|
1746
1824
|
```
|
1747
1825
|
|
1748
|
-
If you just need an accurate total count
|
1826
|
+
If you just need an accurate total count, you can instead use:
|
1749
1827
|
|
1750
1828
|
```ruby
|
1751
1829
|
Product.search("pears", body_options: {track_total_hits: true})
|
@@ -1756,9 +1834,13 @@ Product.search("pears", body_options: {track_total_hits: true})
|
|
1756
1834
|
To query nested data, use dot notation.
|
1757
1835
|
|
1758
1836
|
```ruby
|
1759
|
-
|
1837
|
+
Product.search("san", fields: ["store.city"], where: {"store.zip_code" => 12345})
|
1760
1838
|
```
|
1761
1839
|
|
1840
|
+
## Nearest Neighbors
|
1841
|
+
|
1842
|
+
You can use custom mapping and searching to index vectors and perform k-nearest neighbor search. See the examples for [Elasticsearch](examples/elasticsearch_knn.rb) and [OpenSearch](examples/opensearch_knn.rb).
|
1843
|
+
|
1762
1844
|
## Reference
|
1763
1845
|
|
1764
1846
|
Reindex one record
|
@@ -1886,7 +1968,7 @@ Searchkick.queue_name = :search_reindex
|
|
1886
1968
|
Eager load associations
|
1887
1969
|
|
1888
1970
|
```ruby
|
1889
|
-
Product.search
|
1971
|
+
Product.search("milk", includes: [:brand, :stores])
|
1890
1972
|
```
|
1891
1973
|
|
1892
1974
|
Eager load different associations by model
|
@@ -1898,7 +1980,7 @@ Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:
|
|
1898
1980
|
Run additional scopes on results
|
1899
1981
|
|
1900
1982
|
```ruby
|
1901
|
-
Product.search
|
1983
|
+
Product.search("milk", scope_results: ->(r) { r.with_attached_images })
|
1902
1984
|
```
|
1903
1985
|
|
1904
1986
|
Specify default fields to search
|
@@ -1960,13 +2042,6 @@ class Product < ApplicationRecord
|
|
1960
2042
|
end
|
1961
2043
|
```
|
1962
2044
|
|
1963
|
-
Lazy searching
|
1964
|
-
|
1965
|
-
```ruby
|
1966
|
-
products = Product.search("carrots", execute: false)
|
1967
|
-
products.each { ... } # search not executed until here
|
1968
|
-
```
|
1969
|
-
|
1970
2045
|
Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
|
1971
2046
|
|
1972
2047
|
```ruby
|
@@ -2001,25 +2076,32 @@ rake searchkick:reindex:all
|
|
2001
2076
|
Turn on misspellings after a certain number of characters
|
2002
2077
|
|
2003
2078
|
```ruby
|
2004
|
-
Product.search
|
2079
|
+
Product.search("api", misspellings: {prefix_length: 2}) # api, apt, no ahi
|
2005
2080
|
```
|
2006
2081
|
|
2007
|
-
**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off
|
2082
|
+
**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch 1
|
2008
2083
|
|
2009
2084
|
```ruby
|
2010
|
-
Product.search
|
2085
|
+
Product.search("ah", misspellings: {prefix_length: 2}) # ah, no aha
|
2011
2086
|
```
|
2012
2087
|
|
2013
|
-
|
2088
|
+
BigDecimal values are indexed as floats by default so they can be used for boosting. Convert them to strings to keep full precision.
|
2014
2089
|
|
2015
|
-
|
2016
|
-
|
2090
|
+
```ruby
|
2091
|
+
class Product < ApplicationRecord
|
2092
|
+
def search_data
|
2093
|
+
{
|
2094
|
+
units: units.to_s("F")
|
2095
|
+
}
|
2096
|
+
end
|
2097
|
+
end
|
2098
|
+
```
|
2017
2099
|
|
2018
|
-
##
|
2100
|
+
## Gotchas
|
2019
2101
|
|
2020
2102
|
### Consistency
|
2021
2103
|
|
2022
|
-
Elasticsearch
|
2104
|
+
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.
|
2023
2105
|
|
2024
2106
|
```ruby
|
2025
2107
|
product.save!
|
@@ -2028,7 +2110,7 @@ Product.search_index.refresh
|
|
2028
2110
|
|
2029
2111
|
### Inconsistent Scores
|
2030
2112
|
|
2031
|
-
Due to the distributed nature of Elasticsearch, 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:
|
2113
|
+
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:
|
2032
2114
|
|
2033
2115
|
```ruby
|
2034
2116
|
class Product < ApplicationRecord
|
@@ -2038,6 +2120,42 @@ end
|
|
2038
2120
|
|
2039
2121
|
For convenience, this is set by default in the test environment.
|
2040
2122
|
|
2123
|
+
## Upgrading
|
2124
|
+
|
2125
|
+
### 5.0
|
2126
|
+
|
2127
|
+
Searchkick 5 supports both the `elasticsearch` and `opensearch-ruby` gems. Add the one you want to use to your Gemfile:
|
2128
|
+
|
2129
|
+
```ruby
|
2130
|
+
gem "elasticsearch"
|
2131
|
+
# or
|
2132
|
+
gem "opensearch-ruby"
|
2133
|
+
```
|
2134
|
+
|
2135
|
+
If using the deprecated `faraday_middleware-aws-signers-v4` gem, switch to `faraday_middleware-aws-sigv4`.
|
2136
|
+
|
2137
|
+
Also, searches now use lazy loading:
|
2138
|
+
|
2139
|
+
```ruby
|
2140
|
+
# search not executed
|
2141
|
+
Product.search("milk")
|
2142
|
+
|
2143
|
+
# search executed
|
2144
|
+
Product.search("milk").to_a
|
2145
|
+
```
|
2146
|
+
|
2147
|
+
You can reindex relations in the background:
|
2148
|
+
|
2149
|
+
```ruby
|
2150
|
+
store.products.reindex(mode: :async)
|
2151
|
+
# or
|
2152
|
+
store.products.reindex(mode: :queue)
|
2153
|
+
```
|
2154
|
+
|
2155
|
+
And there’s a [new option](#default-scopes) for models with default scopes.
|
2156
|
+
|
2157
|
+
Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md#500-2022-02-21) for the full list of changes.
|
2158
|
+
|
2041
2159
|
## History
|
2042
2160
|
|
2043
2161
|
View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
|