searchkick 5.5.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +249 -209
- data/lib/searchkick/bulk_reindex_job.rb +3 -5
- data/lib/searchkick/hash_wrapper.rb +30 -9
- data/lib/searchkick/index.rb +25 -16
- data/lib/searchkick/index_options.rb +34 -18
- data/lib/searchkick/indexer.rb +10 -2
- data/lib/searchkick/log_subscriber.rb +1 -1
- data/lib/searchkick/model.rb +18 -8
- data/lib/searchkick/multi_search.rb +7 -2
- data/lib/searchkick/process_batch_job.rb +2 -2
- data/lib/searchkick/process_queue_job.rb +4 -3
- data/lib/searchkick/query.rb +90 -100
- data/lib/searchkick/record_data.rb +19 -0
- data/lib/searchkick/record_indexer.rb +20 -10
- data/lib/searchkick/reindex_queue.rb +1 -24
- data/lib/searchkick/reindex_v2_job.rb +3 -3
- data/lib/searchkick/relation.rb +504 -72
- data/lib/searchkick/relation_indexer.rb +39 -10
- data/lib/searchkick/results.rb +14 -9
- data/lib/searchkick/version.rb +1 -1
- data/lib/searchkick.rb +12 -31
- metadata +4 -18
data/README.md
CHANGED
@@ -43,14 +43,13 @@ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Au
|
|
43
43
|
- [Reference](#reference)
|
44
44
|
- [Contributing](#contributing)
|
45
45
|
|
46
|
+
Searchkick 6.0 was recently released! See [how to upgrade](#upgrading)
|
47
|
+
|
46
48
|
## Getting Started
|
47
49
|
|
48
50
|
Install [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) or [OpenSearch](https://opensearch.org/downloads.html). For Homebrew, use:
|
49
51
|
|
50
52
|
```sh
|
51
|
-
brew install elastic/tap/elasticsearch-full
|
52
|
-
brew services start elasticsearch-full
|
53
|
-
# or
|
54
53
|
brew install opensearch
|
55
54
|
brew services start opensearch
|
56
55
|
```
|
@@ -64,9 +63,9 @@ gem "elasticsearch" # select one
|
|
64
63
|
gem "opensearch-ruby" # select one
|
65
64
|
```
|
66
65
|
|
67
|
-
The latest version works with Elasticsearch
|
66
|
+
The latest version works with Elasticsearch 8 and 9 and OpenSearch 2 and 3. For Elasticsearch 7 and OpenSearch 1, use version 5.5.2 and [this readme](https://github.com/ankane/searchkick/blob/v5.5.2/README.md).
|
68
67
|
|
69
|
-
Add searchkick to models you want to search.
|
68
|
+
Add `searchkick` to models you want to search.
|
70
69
|
|
71
70
|
```ruby
|
72
71
|
class Product < ApplicationRecord
|
@@ -96,19 +95,19 @@ Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.
|
|
96
95
|
Query like SQL
|
97
96
|
|
98
97
|
```ruby
|
99
|
-
Product.search("apples"
|
98
|
+
Product.search("apples").where(in_stock: true).limit(10).offset(50)
|
100
99
|
```
|
101
100
|
|
102
101
|
Search specific fields
|
103
102
|
|
104
103
|
```ruby
|
105
|
-
fields:
|
104
|
+
fields(:name, :brand)
|
106
105
|
```
|
107
106
|
|
108
107
|
Where
|
109
108
|
|
110
109
|
```ruby
|
111
|
-
where
|
110
|
+
where(
|
112
111
|
expires_at: {gt: Time.now}, # lt, gte, lte also available
|
113
112
|
orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
|
114
113
|
aisle_id: [25, 30], # in
|
@@ -123,13 +122,13 @@ where: {
|
|
123
122
|
_not: {store_id: 1}, # negate a condition
|
124
123
|
_or: [{in_stock: true}, {backordered: true}],
|
125
124
|
_and: [{in_stock: true}, {backordered: true}]
|
126
|
-
|
125
|
+
)
|
127
126
|
```
|
128
127
|
|
129
128
|
Order
|
130
129
|
|
131
130
|
```ruby
|
132
|
-
order
|
131
|
+
order(_score: :desc) # most relevant first - default
|
133
132
|
```
|
134
133
|
|
135
134
|
[All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
|
@@ -137,13 +136,13 @@ order: {_score: :desc} # most relevant first - default
|
|
137
136
|
Limit / offset
|
138
137
|
|
139
138
|
```ruby
|
140
|
-
limit
|
139
|
+
limit(20).offset(40)
|
141
140
|
```
|
142
141
|
|
143
142
|
Select
|
144
143
|
|
145
144
|
```ruby
|
146
|
-
select:
|
145
|
+
select(:name)
|
147
146
|
```
|
148
147
|
|
149
148
|
[These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering)
|
@@ -162,7 +161,7 @@ results.each { |result| ... }
|
|
162
161
|
By default, ids are fetched from the search server and records are fetched from your database. To fetch everything from the search server, use:
|
163
162
|
|
164
163
|
```ruby
|
165
|
-
Product.search("apples"
|
164
|
+
Product.search("apples").load(false)
|
166
165
|
```
|
167
166
|
|
168
167
|
Get total results
|
@@ -190,28 +189,28 @@ results.response
|
|
190
189
|
Boost important fields
|
191
190
|
|
192
191
|
```ruby
|
193
|
-
fields
|
192
|
+
fields("title^10", "description")
|
194
193
|
```
|
195
194
|
|
196
195
|
Boost by the value of a field (field must be numeric)
|
197
196
|
|
198
197
|
```ruby
|
199
|
-
boost_by:
|
200
|
-
boost_by
|
198
|
+
boost_by(:orders_count) # give popular documents a little boost
|
199
|
+
boost_by(orders_count: {factor: 10}) # default factor is 1
|
201
200
|
```
|
202
201
|
|
203
202
|
Boost matching documents
|
204
203
|
|
205
204
|
```ruby
|
206
|
-
boost_where
|
207
|
-
boost_where
|
208
|
-
boost_where
|
205
|
+
boost_where(user_id: 1)
|
206
|
+
boost_where(user_id: {value: 1, factor: 100}) # default factor is 1000
|
207
|
+
boost_where(user_id: [{value: 1, factor: 100}, {value: 2, factor: 200}])
|
209
208
|
```
|
210
209
|
|
211
210
|
Boost by recency
|
212
211
|
|
213
212
|
```ruby
|
214
|
-
boost_by_recency
|
213
|
+
boost_by_recency(created_at: {scale: "7d", decay: 0.5})
|
215
214
|
```
|
216
215
|
|
217
216
|
You can also boost by:
|
@@ -233,7 +232,7 @@ Plays nicely with kaminari and will_paginate.
|
|
233
232
|
|
234
233
|
```ruby
|
235
234
|
# controller
|
236
|
-
@products = Product.search("milk"
|
235
|
+
@products = Product.search("milk").page(params[:page]).per_page(20)
|
237
236
|
```
|
238
237
|
|
239
238
|
View with kaminari
|
@@ -259,7 +258,7 @@ Product.search("fresh honey") # fresh AND honey
|
|
259
258
|
To change this, use:
|
260
259
|
|
261
260
|
```ruby
|
262
|
-
Product.search("fresh honey"
|
261
|
+
Product.search("fresh honey").operator("or") # fresh OR honey
|
263
262
|
```
|
264
263
|
|
265
264
|
By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
|
@@ -273,7 +272,7 @@ end
|
|
273
272
|
And to search (after you reindex):
|
274
273
|
|
275
274
|
```ruby
|
276
|
-
Product.search("back"
|
275
|
+
Product.search("back").fields(:name).match(:word_start)
|
277
276
|
```
|
278
277
|
|
279
278
|
Available options are:
|
@@ -293,7 +292,7 @@ The default is `:word`. The most matches will happen with `:word_middle`.
|
|
293
292
|
To specify different matching for different fields, use:
|
294
293
|
|
295
294
|
```ruby
|
296
|
-
Product.search(query
|
295
|
+
Product.search(query).fields({name: :word_start}, {brand: :word_middle})
|
297
296
|
```
|
298
297
|
|
299
298
|
### Exact Matches
|
@@ -301,7 +300,7 @@ Product.search(query, fields: [{name: :word_start}, {brand: :word_middle}])
|
|
301
300
|
To match a field exactly (case-sensitive), use:
|
302
301
|
|
303
302
|
```ruby
|
304
|
-
Product.search(query
|
303
|
+
Product.search(query).fields({name: :exact})
|
305
304
|
```
|
306
305
|
|
307
306
|
### Phrase Matches
|
@@ -309,7 +308,7 @@ Product.search(query, fields: [{name: :exact}])
|
|
309
308
|
To only match the exact order, use:
|
310
309
|
|
311
310
|
```ruby
|
312
|
-
Product.search("fresh honey"
|
311
|
+
Product.search("fresh honey").match(:phrase)
|
313
312
|
```
|
314
313
|
|
315
314
|
### Stemming and Language
|
@@ -385,11 +384,7 @@ search_synonyms: ["lightbulb => halogenlamp"]
|
|
385
384
|
|
386
385
|
### Dynamic Synonyms
|
387
386
|
|
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.
|
389
|
-
|
390
|
-
#### Elasticsearch 7.3+ and OpenSearch
|
391
|
-
|
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.
|
387
|
+
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. We recommend placing synonyms in a file on the search server (in the `config` directory). This allows you to reload synonyms without reindexing.
|
393
388
|
|
394
389
|
```txt
|
395
390
|
pop, soda
|
@@ -410,29 +405,6 @@ And reload with:
|
|
410
405
|
Product.search_index.reload_synonyms
|
411
406
|
```
|
412
407
|
|
413
|
-
#### Elasticsearch < 7.3
|
414
|
-
|
415
|
-
You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
|
416
|
-
|
417
|
-
```ruby
|
418
|
-
class Product < ApplicationRecord
|
419
|
-
acts_as_taggable
|
420
|
-
scope :search_import, -> { includes(:tags) }
|
421
|
-
|
422
|
-
def search_data
|
423
|
-
{
|
424
|
-
name_tagged: "#{name} #{tags.map(&:name).join(" ")}"
|
425
|
-
}
|
426
|
-
end
|
427
|
-
end
|
428
|
-
```
|
429
|
-
|
430
|
-
Search with:
|
431
|
-
|
432
|
-
```ruby
|
433
|
-
Product.search(query, fields: [:name_tagged])
|
434
|
-
```
|
435
|
-
|
436
408
|
### Misspellings
|
437
409
|
|
438
410
|
By default, Searchkick handles misspelled queries by returning results with an [edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance) of one.
|
@@ -440,13 +412,13 @@ By default, Searchkick handles misspelled queries by returning results with an [
|
|
440
412
|
You can change this with:
|
441
413
|
|
442
414
|
```ruby
|
443
|
-
Product.search("zucini"
|
415
|
+
Product.search("zucini").misspellings(edit_distance: 2) # zucchini
|
444
416
|
```
|
445
417
|
|
446
418
|
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.
|
447
419
|
|
448
420
|
```ruby
|
449
|
-
Product.search("zuchini"
|
421
|
+
Product.search("zuchini").misspellings(below: 5)
|
450
422
|
```
|
451
423
|
|
452
424
|
If there are fewer than 5 results, a 2nd search is performed with misspellings enabled. The result of this query is returned.
|
@@ -454,13 +426,13 @@ If there are fewer than 5 results, a 2nd search is performed with misspellings e
|
|
454
426
|
Turn off misspellings with:
|
455
427
|
|
456
428
|
```ruby
|
457
|
-
Product.search("zuchini"
|
429
|
+
Product.search("zuchini").misspellings(false) # no zucchini
|
458
430
|
```
|
459
431
|
|
460
432
|
Specify which fields can include misspellings with:
|
461
433
|
|
462
434
|
```ruby
|
463
|
-
Product.search("zucini"
|
435
|
+
Product.search("zucini").fields(:name, :color).misspellings(fields: [:name])
|
464
436
|
```
|
465
437
|
|
466
438
|
> When doing this, you must also specify fields to search
|
@@ -470,7 +442,7 @@ Product.search("zucini", fields: [:name, :color], misspellings: {fields: [:name]
|
|
470
442
|
If a user searches `butter`, they may also get results for `peanut butter`. To prevent this, use:
|
471
443
|
|
472
444
|
```ruby
|
473
|
-
Product.search("butter"
|
445
|
+
Product.search("butter").exclude("peanut butter")
|
474
446
|
```
|
475
447
|
|
476
448
|
You can map queries and terms to exclude with:
|
@@ -481,13 +453,13 @@ exclude_queries = {
|
|
481
453
|
"cream" => ["ice cream", "whipped cream"]
|
482
454
|
}
|
483
455
|
|
484
|
-
Product.search(query
|
456
|
+
Product.search(query).exclude(exclude_queries[query])
|
485
457
|
```
|
486
458
|
|
487
459
|
You can demote results by boosting by a factor less than one:
|
488
460
|
|
489
461
|
```ruby
|
490
|
-
Product.search("butter"
|
462
|
+
Product.search("butter").boost_where(category: {value: "pantry", factor: 0.5})
|
491
463
|
```
|
492
464
|
|
493
465
|
### Emoji
|
@@ -503,7 +475,7 @@ gem "gemoji-parser"
|
|
503
475
|
And use:
|
504
476
|
|
505
477
|
```ruby
|
506
|
-
Product.search("🍨🍰"
|
478
|
+
Product.search("🍨🍰").emoji
|
507
479
|
```
|
508
480
|
|
509
481
|
## Indexing
|
@@ -669,7 +641,7 @@ end
|
|
669
641
|
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
642
|
|
671
643
|
```ruby
|
672
|
-
Product.search("apple"
|
644
|
+
Product.search("apple").track(user_id: current_user.id)
|
673
645
|
```
|
674
646
|
|
675
647
|
[See the docs](https://github.com/ankane/searchjoy) for how to install and use. Focus on top searches with a low conversion rate.
|
@@ -683,7 +655,7 @@ class Product < ApplicationRecord
|
|
683
655
|
has_many :conversions, class_name: "Searchjoy::Conversion", as: :convertable
|
684
656
|
has_many :searches, class_name: "Searchjoy::Search", through: :conversions
|
685
657
|
|
686
|
-
searchkick
|
658
|
+
searchkick conversions_v2: [:conversions] # name of field
|
687
659
|
|
688
660
|
def search_data
|
689
661
|
{
|
@@ -695,7 +667,7 @@ class Product < ApplicationRecord
|
|
695
667
|
end
|
696
668
|
```
|
697
669
|
|
698
|
-
Reindex and set up a cron job to add new conversions daily. For zero downtime deployment, temporarily set `
|
670
|
+
Reindex and set up a cron job to add new conversions daily. For zero downtime deployment, temporarily set `conversions_v2(false)` in your search calls until the data is reindexed.
|
699
671
|
|
700
672
|
### Performant Conversions
|
701
673
|
|
@@ -711,7 +683,7 @@ Next, update your model. Create a separate method for conversion data so you can
|
|
711
683
|
|
712
684
|
```ruby
|
713
685
|
class Product < ApplicationRecord
|
714
|
-
searchkick
|
686
|
+
searchkick conversions_v2: [:conversions]
|
715
687
|
|
716
688
|
def search_data
|
717
689
|
{
|
@@ -728,7 +700,7 @@ class Product < ApplicationRecord
|
|
728
700
|
end
|
729
701
|
```
|
730
702
|
|
731
|
-
Deploy and reindex your data. For zero downtime deployment, temporarily set `
|
703
|
+
Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions_v2(false)` in your search calls until the data is reindexed.
|
732
704
|
|
733
705
|
```ruby
|
734
706
|
Product.reindex
|
@@ -743,8 +715,8 @@ class UpdateConversionsJob < ApplicationJob
|
|
743
715
|
|
744
716
|
# get records that have a recent conversion
|
745
717
|
recently_converted_ids =
|
746
|
-
Searchjoy::Conversion.where(convertable_type: class_name
|
747
|
-
|
718
|
+
Searchjoy::Conversion.where(convertable_type: class_name, created_at: since..)
|
719
|
+
.order(:convertable_id).distinct.pluck(:convertable_id)
|
748
720
|
|
749
721
|
# split into batches
|
750
722
|
recently_converted_ids.in_groups_of(1000, false) do |ids|
|
@@ -752,8 +724,8 @@ class UpdateConversionsJob < ApplicationJob
|
|
752
724
|
# fetch conversions
|
753
725
|
conversions =
|
754
726
|
Searchjoy::Conversion.where(convertable_id: ids, convertable_type: class_name)
|
755
|
-
|
756
|
-
|
727
|
+
.joins(:search).where.not(searchjoy_searches: {user_id: nil})
|
728
|
+
.group(:convertable_id, :query).distinct.count(:user_id)
|
757
729
|
|
758
730
|
# group by record
|
759
731
|
conversions_by_record = {}
|
@@ -771,7 +743,7 @@ class UpdateConversionsJob < ApplicationJob
|
|
771
743
|
|
772
744
|
if reindex
|
773
745
|
# reindex conversions data
|
774
|
-
model.where(id: ids).reindex(:conversions_data)
|
746
|
+
model.where(id: ids).reindex(:conversions_data, ignore_missing: true)
|
775
747
|
end
|
776
748
|
end
|
777
749
|
end
|
@@ -808,7 +780,7 @@ end
|
|
808
780
|
Reindex and search with:
|
809
781
|
|
810
782
|
```ruby
|
811
|
-
Product.search("milk"
|
783
|
+
Product.search("milk").boost_where(orderer_ids: current_user.id)
|
812
784
|
```
|
813
785
|
|
814
786
|
## Instant Search / Autocomplete
|
@@ -832,7 +804,7 @@ end
|
|
832
804
|
Reindex and search with:
|
833
805
|
|
834
806
|
```ruby
|
835
|
-
Movie.search("jurassic pa"
|
807
|
+
Movie.search("jurassic pa").fields(:title).match(:word_start)
|
836
808
|
```
|
837
809
|
|
838
810
|
Use a front-end library like [typeahead.js](https://twitter.github.io/typeahead.js/) to show the results.
|
@@ -844,19 +816,13 @@ First, add a route and controller action.
|
|
844
816
|
```ruby
|
845
817
|
class MoviesController < ApplicationController
|
846
818
|
def autocomplete
|
847
|
-
render json: Movie.search(
|
848
|
-
|
849
|
-
fields: ["title^5", "director"],
|
850
|
-
match: :word_start,
|
851
|
-
limit: 10,
|
852
|
-
load: false,
|
853
|
-
misspellings: {below: 5}
|
854
|
-
).map(&:title)
|
819
|
+
render json: Movie.search(params[:query]).fields("title^5", "director")
|
820
|
+
.match(:word_start).limit(10).load(false).misspellings(below: 5).map(&:title)
|
855
821
|
end
|
856
822
|
end
|
857
823
|
```
|
858
824
|
|
859
|
-
**Note:** Use `load
|
825
|
+
**Note:** Use `load(false)` and `misspellings(below: n)` (or `misspellings(false)`) for best performance.
|
860
826
|
|
861
827
|
Then add the search box and JavaScript code to a view.
|
862
828
|
|
@@ -893,7 +859,7 @@ end
|
|
893
859
|
Reindex and search with:
|
894
860
|
|
895
861
|
```ruby
|
896
|
-
products = Product.search("peantu butta"
|
862
|
+
products = Product.search("peantu butta").suggest
|
897
863
|
products.suggestions # ["peanut butter"]
|
898
864
|
```
|
899
865
|
|
@@ -904,40 +870,40 @@ products.suggestions # ["peanut butter"]
|
|
904
870
|

|
905
871
|
|
906
872
|
```ruby
|
907
|
-
products = Product.search("chuck taylor"
|
873
|
+
products = Product.search("chuck taylor").aggs(:product_type, :gender, :brand)
|
908
874
|
products.aggs
|
909
875
|
```
|
910
876
|
|
911
877
|
By default, `where` conditions apply to aggregations.
|
912
878
|
|
913
879
|
```ruby
|
914
|
-
Product.search("wingtips"
|
880
|
+
Product.search("wingtips").where(color: "brandy").aggs(:size)
|
915
881
|
# aggregations for brandy wingtips are returned
|
916
882
|
```
|
917
883
|
|
918
884
|
Change this with:
|
919
885
|
|
920
886
|
```ruby
|
921
|
-
Product.search("wingtips"
|
887
|
+
Product.search("wingtips").where(color: "brandy").aggs(:size).smart_aggs(false)
|
922
888
|
# aggregations for all wingtips are returned
|
923
889
|
```
|
924
890
|
|
925
891
|
Set `where` conditions for each aggregation separately with:
|
926
892
|
|
927
893
|
```ruby
|
928
|
-
Product.search("wingtips"
|
894
|
+
Product.search("wingtips").aggs(size: {where: {color: "brandy"}})
|
929
895
|
```
|
930
896
|
|
931
897
|
Limit
|
932
898
|
|
933
899
|
```ruby
|
934
|
-
Product.search("apples"
|
900
|
+
Product.search("apples").aggs(store_id: {limit: 10})
|
935
901
|
```
|
936
902
|
|
937
903
|
Order
|
938
904
|
|
939
905
|
```ruby
|
940
|
-
Product.search("wingtips"
|
906
|
+
Product.search("wingtips").aggs(color: {order: {"_key" => "asc"}}) # alphabetically
|
941
907
|
```
|
942
908
|
|
943
909
|
[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)
|
@@ -946,31 +912,31 @@ Ranges
|
|
946
912
|
|
947
913
|
```ruby
|
948
914
|
price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
|
949
|
-
Product.search("*"
|
915
|
+
Product.search("*").aggs(price: {ranges: price_ranges})
|
950
916
|
```
|
951
917
|
|
952
918
|
Minimum document count
|
953
919
|
|
954
920
|
```ruby
|
955
|
-
Product.search("apples"
|
921
|
+
Product.search("apples").aggs(store_id: {min_doc_count: 2})
|
956
922
|
```
|
957
923
|
|
958
924
|
Script support
|
959
925
|
|
960
926
|
```ruby
|
961
|
-
Product.search("*"
|
927
|
+
Product.search("*").aggs(color: {script: {source: "'Color: ' + _value"}})
|
962
928
|
```
|
963
929
|
|
964
930
|
Date histogram
|
965
931
|
|
966
932
|
```ruby
|
967
|
-
Product.search("pear"
|
933
|
+
Product.search("pear").aggs(products_per_year: {date_histogram: {field: :created_at, interval: :year}})
|
968
934
|
```
|
969
935
|
|
970
936
|
For other aggregation types, including sub-aggregations, use `body_options`:
|
971
937
|
|
972
938
|
```ruby
|
973
|
-
Product.search("orange"
|
939
|
+
Product.search("orange").body_options(aggs: {price: {histogram: {field: :price, interval: 10}}})
|
974
940
|
```
|
975
941
|
|
976
942
|
## Highlight
|
@@ -986,7 +952,7 @@ end
|
|
986
952
|
Highlight the search query in the results.
|
987
953
|
|
988
954
|
```ruby
|
989
|
-
bands = Band.search("cinema"
|
955
|
+
bands = Band.search("cinema").highlight
|
990
956
|
```
|
991
957
|
|
992
958
|
View the highlighted fields with:
|
@@ -1000,19 +966,19 @@ end
|
|
1000
966
|
To change the tag, use:
|
1001
967
|
|
1002
968
|
```ruby
|
1003
|
-
Band.search("cinema"
|
969
|
+
Band.search("cinema").highlight(tag: "<strong>")
|
1004
970
|
```
|
1005
971
|
|
1006
972
|
To highlight and search different fields, use:
|
1007
973
|
|
1008
974
|
```ruby
|
1009
|
-
Band.search("cinema"
|
975
|
+
Band.search("cinema").fields(:name).highlight(fields: [:description])
|
1010
976
|
```
|
1011
977
|
|
1012
978
|
By default, the entire field is highlighted. To get small snippets instead, use:
|
1013
979
|
|
1014
980
|
```ruby
|
1015
|
-
bands = Band.search("cinema"
|
981
|
+
bands = Band.search("cinema").highlight(fragment_size: 20)
|
1016
982
|
bands.with_highlights(multiple: true).each do |band, highlights|
|
1017
983
|
highlights[:name].join(" and ")
|
1018
984
|
end
|
@@ -1021,18 +987,18 @@ end
|
|
1021
987
|
Additional options can be specified for each field:
|
1022
988
|
|
1023
989
|
```ruby
|
1024
|
-
Band.search("cinema"
|
990
|
+
Band.search("cinema").fields(:name).highlight(fields: {name: {fragment_size: 200}})
|
1025
991
|
```
|
1026
992
|
|
1027
993
|
You can find available highlight options in the [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html) or [OpenSearch](https://opensearch.org/docs/latest/search-plugins/searching-data/highlight/) reference.
|
1028
994
|
|
1029
995
|
## Similar Items
|
1030
996
|
|
1031
|
-
Find similar items
|
997
|
+
Find similar items
|
1032
998
|
|
1033
999
|
```ruby
|
1034
1000
|
product = Product.first
|
1035
|
-
product.similar(
|
1001
|
+
product.similar.fields(:name).where(size: "12 oz")
|
1036
1002
|
```
|
1037
1003
|
|
1038
1004
|
## Geospatial Searches
|
@@ -1050,13 +1016,13 @@ end
|
|
1050
1016
|
Reindex and search with:
|
1051
1017
|
|
1052
1018
|
```ruby
|
1053
|
-
Restaurant.search("pizza"
|
1019
|
+
Restaurant.search("pizza").where(location: {near: {lat: 37, lon: -114}, within: "100mi"}) # or 160km
|
1054
1020
|
```
|
1055
1021
|
|
1056
1022
|
Bounded by a box
|
1057
1023
|
|
1058
1024
|
```ruby
|
1059
|
-
Restaurant.search("sushi"
|
1025
|
+
Restaurant.search("sushi").where(location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}})
|
1060
1026
|
```
|
1061
1027
|
|
1062
1028
|
**Note:** `top_right` and `bottom_left` also work
|
@@ -1064,7 +1030,7 @@ Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bo
|
|
1064
1030
|
Bounded by a polygon
|
1065
1031
|
|
1066
1032
|
```ruby
|
1067
|
-
Restaurant.search("dessert"
|
1033
|
+
Restaurant.search("dessert").where(location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}})
|
1068
1034
|
```
|
1069
1035
|
|
1070
1036
|
### Boost By Distance
|
@@ -1072,13 +1038,13 @@ Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38,
|
|
1072
1038
|
Boost results by distance - closer results are boosted more
|
1073
1039
|
|
1074
1040
|
```ruby
|
1075
|
-
Restaurant.search("noodles"
|
1041
|
+
Restaurant.search("noodles").boost_by_distance(location: {origin: {lat: 37, lon: -122}})
|
1076
1042
|
```
|
1077
1043
|
|
1078
1044
|
Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
|
1079
1045
|
|
1080
1046
|
```ruby
|
1081
|
-
Restaurant.search("wings"
|
1047
|
+
Restaurant.search("wings").boost_by_distance(location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5})
|
1082
1048
|
```
|
1083
1049
|
|
1084
1050
|
### Geo Shapes
|
@@ -1105,19 +1071,19 @@ See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsea
|
|
1105
1071
|
Find shapes intersecting with the query shape
|
1106
1072
|
|
1107
1073
|
```ruby
|
1108
|
-
Restaurant.search("soup"
|
1074
|
+
Restaurant.search("soup").where(bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}})
|
1109
1075
|
```
|
1110
1076
|
|
1111
1077
|
Falling entirely within the query shape
|
1112
1078
|
|
1113
1079
|
```ruby
|
1114
|
-
Restaurant.search("salad"
|
1080
|
+
Restaurant.search("salad").where(bounds: {geo_shape: {type: "circle", relation: "within", coordinates: {lat: 38, lon: -123}, radius: "1km"}})
|
1115
1081
|
```
|
1116
1082
|
|
1117
1083
|
Not touching the query shape
|
1118
1084
|
|
1119
1085
|
```ruby
|
1120
|
-
Restaurant.search("burger"
|
1086
|
+
Restaurant.search("burger").where(bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}})
|
1121
1087
|
```
|
1122
1088
|
|
1123
1089
|
## Inheritance
|
@@ -1147,9 +1113,9 @@ Dog.reindex # equivalent, all animals reindexed
|
|
1147
1113
|
And to search, use:
|
1148
1114
|
|
1149
1115
|
```ruby
|
1150
|
-
Animal.search("*")
|
1151
|
-
Dog.search("*")
|
1152
|
-
Animal.search("*"
|
1116
|
+
Animal.search("*") # all animals
|
1117
|
+
Dog.search("*") # just dogs
|
1118
|
+
Animal.search("*").type(Dog, Cat) # just cats and dogs
|
1153
1119
|
```
|
1154
1120
|
|
1155
1121
|
**Notes:**
|
@@ -1157,7 +1123,7 @@ Animal.search("*", type: [Dog, Cat]) # just cats and dogs
|
|
1157
1123
|
1. The `suggest` option retrieves suggestions from the parent at the moment.
|
1158
1124
|
|
1159
1125
|
```ruby
|
1160
|
-
Dog.search("airbudd"
|
1126
|
+
Dog.search("airbudd").suggest # suggestions for all animals
|
1161
1127
|
```
|
1162
1128
|
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.
|
1163
1129
|
|
@@ -1166,7 +1132,7 @@ Animal.search("*", type: [Dog, Cat]) # just cats and dogs
|
|
1166
1132
|
To help with debugging queries, you can use:
|
1167
1133
|
|
1168
1134
|
```ruby
|
1169
|
-
Product.search("soap"
|
1135
|
+
Product.search("soap").debug
|
1170
1136
|
```
|
1171
1137
|
|
1172
1138
|
This prints useful info to `stdout`.
|
@@ -1174,7 +1140,7 @@ This prints useful info to `stdout`.
|
|
1174
1140
|
See how the search server scores your queries with:
|
1175
1141
|
|
1176
1142
|
```ruby
|
1177
|
-
Product.search("soap"
|
1143
|
+
Product.search("soap").explain.response
|
1178
1144
|
```
|
1179
1145
|
|
1180
1146
|
See how the search server tokenizes your queries with:
|
@@ -1208,37 +1174,42 @@ As you iterate on your search, it’s a good idea to add tests.
|
|
1208
1174
|
|
1209
1175
|
For performance, only enable Searchkick callbacks for the tests that need it.
|
1210
1176
|
|
1211
|
-
###
|
1177
|
+
### Rails
|
1212
1178
|
|
1213
|
-
|
1179
|
+
Add to your `test/test_helper.rb`:
|
1214
1180
|
|
1215
1181
|
```ruby
|
1216
|
-
|
1217
|
-
|
1218
|
-
|
1182
|
+
module ActiveSupport
|
1183
|
+
class TestCase
|
1184
|
+
parallelize_setup do |worker|
|
1185
|
+
Searchkick.index_suffix = worker
|
1219
1186
|
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
# and disable callbacks
|
1224
|
-
Searchkick.disable_callbacks
|
1187
|
+
# reindex models for parallel tests
|
1188
|
+
Product.reindex
|
1189
|
+
end
|
1225
1190
|
end
|
1226
1191
|
end
|
1192
|
+
|
1193
|
+
# reindex models for non-parallel tests
|
1194
|
+
Product.reindex
|
1195
|
+
|
1196
|
+
# and disable callbacks
|
1197
|
+
Searchkick.disable_callbacks
|
1227
1198
|
```
|
1228
1199
|
|
1229
1200
|
And use:
|
1230
1201
|
|
1231
1202
|
```ruby
|
1232
1203
|
class ProductTest < ActiveSupport::TestCase
|
1233
|
-
|
1204
|
+
setup do
|
1234
1205
|
Searchkick.enable_callbacks
|
1235
1206
|
end
|
1236
1207
|
|
1237
|
-
|
1208
|
+
teardown do
|
1238
1209
|
Searchkick.disable_callbacks
|
1239
1210
|
end
|
1240
1211
|
|
1241
|
-
|
1212
|
+
test "search" do
|
1242
1213
|
Product.create!(name: "Apple")
|
1243
1214
|
Product.search_index.refresh
|
1244
1215
|
assert_equal ["Apple"], Product.search("apple").map(&:name)
|
@@ -1314,25 +1285,24 @@ end
|
|
1314
1285
|
|
1315
1286
|
### Factory Bot
|
1316
1287
|
|
1317
|
-
|
1288
|
+
Define a trait for each model:
|
1318
1289
|
|
1319
1290
|
```ruby
|
1320
1291
|
FactoryBot.define do
|
1321
1292
|
factory :product do
|
1322
|
-
# ...
|
1323
|
-
|
1324
|
-
# Note: This should be the last trait in the list so `reindex` is called
|
1325
|
-
# after all the other callbacks complete.
|
1326
1293
|
trait :reindex do
|
1327
|
-
after(:create) do |product,
|
1294
|
+
after(:create) do |product, _|
|
1328
1295
|
product.reindex(refresh: true)
|
1329
1296
|
end
|
1330
1297
|
end
|
1331
1298
|
end
|
1332
1299
|
end
|
1300
|
+
```
|
1333
1301
|
|
1334
|
-
|
1335
|
-
|
1302
|
+
And use:
|
1303
|
+
|
1304
|
+
```ruby
|
1305
|
+
FactoryBot.create(:product, :reindex)
|
1336
1306
|
```
|
1337
1307
|
|
1338
1308
|
### GitHub Actions
|
@@ -1354,8 +1324,8 @@ And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy w
|
|
1354
1324
|
For the search server, Searchkick uses `ENV["ELASTICSEARCH_URL"]` for Elasticsearch and `ENV["OPENSEARCH_URL"]` for OpenSearch. This defaults to `http://localhost:9200`.
|
1355
1325
|
|
1356
1326
|
- [Elastic Cloud](#elastic-cloud)
|
1357
|
-
- [Heroku](#heroku)
|
1358
1327
|
- [Amazon OpenSearch Service](#amazon-opensearch-service)
|
1328
|
+
- [Heroku](#heroku)
|
1359
1329
|
- [Self-Hosted and Other](#self-hosted-and-other)
|
1360
1330
|
|
1361
1331
|
### Elastic Cloud
|
@@ -1372,6 +1342,36 @@ Then deploy and reindex:
|
|
1372
1342
|
rake searchkick:reindex:all
|
1373
1343
|
```
|
1374
1344
|
|
1345
|
+
### Amazon OpenSearch Service
|
1346
|
+
|
1347
|
+
Create an initializer `config/initializers/opensearch.rb` with:
|
1348
|
+
|
1349
|
+
```ruby
|
1350
|
+
ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
|
1351
|
+
```
|
1352
|
+
|
1353
|
+
To use signed requests, include in your Gemfile:
|
1354
|
+
|
1355
|
+
```ruby
|
1356
|
+
gem "faraday_middleware-aws-sigv4"
|
1357
|
+
```
|
1358
|
+
|
1359
|
+
and add to your initializer:
|
1360
|
+
|
1361
|
+
```ruby
|
1362
|
+
Searchkick.aws_credentials = {
|
1363
|
+
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
1364
|
+
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
1365
|
+
region: "us-east-1"
|
1366
|
+
}
|
1367
|
+
```
|
1368
|
+
|
1369
|
+
Then deploy and reindex:
|
1370
|
+
|
1371
|
+
```sh
|
1372
|
+
rake searchkick:reindex:all
|
1373
|
+
```
|
1374
|
+
|
1375
1375
|
### Heroku
|
1376
1376
|
|
1377
1377
|
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).
|
@@ -1422,36 +1422,6 @@ Then deploy and reindex:
|
|
1422
1422
|
heroku run rake searchkick:reindex:all
|
1423
1423
|
```
|
1424
1424
|
|
1425
|
-
### Amazon OpenSearch Service
|
1426
|
-
|
1427
|
-
Create an initializer `config/initializers/opensearch.rb` with:
|
1428
|
-
|
1429
|
-
```ruby
|
1430
|
-
ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
|
1431
|
-
```
|
1432
|
-
|
1433
|
-
To use signed requests, include in your Gemfile:
|
1434
|
-
|
1435
|
-
```ruby
|
1436
|
-
gem "faraday_middleware-aws-sigv4"
|
1437
|
-
```
|
1438
|
-
|
1439
|
-
and add to your initializer:
|
1440
|
-
|
1441
|
-
```ruby
|
1442
|
-
Searchkick.aws_credentials = {
|
1443
|
-
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
1444
|
-
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
1445
|
-
region: "us-east-1"
|
1446
|
-
}
|
1447
|
-
```
|
1448
|
-
|
1449
|
-
Then deploy and reindex:
|
1450
|
-
|
1451
|
-
```sh
|
1452
|
-
rake searchkick:reindex:all
|
1453
|
-
```
|
1454
|
-
|
1455
1425
|
### Self-Hosted and Other
|
1456
1426
|
|
1457
1427
|
Create an initializer with:
|
@@ -1682,7 +1652,7 @@ end
|
|
1682
1652
|
Reindex and search with:
|
1683
1653
|
|
1684
1654
|
```ruby
|
1685
|
-
Business.search("ice cream"
|
1655
|
+
Business.search("ice cream").routing(params[:city_id])
|
1686
1656
|
```
|
1687
1657
|
|
1688
1658
|
### Partial Reindexing
|
@@ -1713,6 +1683,12 @@ And use:
|
|
1713
1683
|
Product.reindex(:prices_data)
|
1714
1684
|
```
|
1715
1685
|
|
1686
|
+
Ignore errors for missing documents with:
|
1687
|
+
|
1688
|
+
```ruby
|
1689
|
+
Product.reindex(:prices_data, ignore_missing: true)
|
1690
|
+
```
|
1691
|
+
|
1716
1692
|
## Advanced
|
1717
1693
|
|
1718
1694
|
Searchkick makes it easy to use the Elasticsearch or OpenSearch DSL on its own.
|
@@ -1745,7 +1721,7 @@ end
|
|
1745
1721
|
And use the `body` option to search:
|
1746
1722
|
|
1747
1723
|
```ruby
|
1748
|
-
products = Product.search(
|
1724
|
+
products = Product.search.body(query: {match: {name: "milk"}})
|
1749
1725
|
```
|
1750
1726
|
|
1751
1727
|
View the response with:
|
@@ -1757,7 +1733,7 @@ products.response
|
|
1757
1733
|
To modify the query generated by Searchkick, use:
|
1758
1734
|
|
1759
1735
|
```ruby
|
1760
|
-
products = Product.search("milk"
|
1736
|
+
products = Product.search("milk").body_options(min_score: 1)
|
1761
1737
|
```
|
1762
1738
|
|
1763
1739
|
or
|
@@ -1796,13 +1772,13 @@ Then use `products` and `coupons` as typical results.
|
|
1796
1772
|
Search across multiple models with:
|
1797
1773
|
|
1798
1774
|
```ruby
|
1799
|
-
Searchkick.search("milk"
|
1775
|
+
Searchkick.search("milk").models(Product, Category)
|
1800
1776
|
```
|
1801
1777
|
|
1802
1778
|
Boost specific models with:
|
1803
1779
|
|
1804
1780
|
```ruby
|
1805
|
-
indices_boost
|
1781
|
+
indices_boost(Category => 2, Product => 1)
|
1806
1782
|
```
|
1807
1783
|
|
1808
1784
|
## Multi-Tenancy
|
@@ -1814,7 +1790,7 @@ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenan
|
|
1814
1790
|
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.
|
1815
1791
|
|
1816
1792
|
```ruby
|
1817
|
-
Product.search("*"
|
1793
|
+
Product.search("*").scroll("1m") do |batch|
|
1818
1794
|
# process batch ...
|
1819
1795
|
end
|
1820
1796
|
```
|
@@ -1822,7 +1798,7 @@ end
|
|
1822
1798
|
You can also scroll batches manually.
|
1823
1799
|
|
1824
1800
|
```ruby
|
1825
|
-
products = Product.search("*"
|
1801
|
+
products = Product.search("*").scroll("1m")
|
1826
1802
|
while products.any?
|
1827
1803
|
# process batch ...
|
1828
1804
|
|
@@ -1845,7 +1821,7 @@ end
|
|
1845
1821
|
If you just need an accurate total count, you can instead use:
|
1846
1822
|
|
1847
1823
|
```ruby
|
1848
|
-
Product.search("pears"
|
1824
|
+
Product.search("pears").body_options(track_total_hits: true)
|
1849
1825
|
```
|
1850
1826
|
|
1851
1827
|
## Nested Data
|
@@ -1853,7 +1829,7 @@ Product.search("pears", body_options: {track_total_hits: true})
|
|
1853
1829
|
To query nested data, use dot notation.
|
1854
1830
|
|
1855
1831
|
```ruby
|
1856
|
-
Product.search("san"
|
1832
|
+
Product.search("san").fields("store.city").where("store.zip_code" => 12345)
|
1857
1833
|
```
|
1858
1834
|
|
1859
1835
|
## Nearest Neighbor Search
|
@@ -1871,7 +1847,7 @@ Also supports `euclidean` and `inner_product`
|
|
1871
1847
|
Reindex and search with:
|
1872
1848
|
|
1873
1849
|
```ruby
|
1874
|
-
Product.search(
|
1850
|
+
Product.search.knn(field: :embedding, vector: [1, 2, 3]).limit(10)
|
1875
1851
|
```
|
1876
1852
|
|
1877
1853
|
### HNSW Options
|
@@ -1889,7 +1865,7 @@ end
|
|
1889
1865
|
Specify `ef_search`
|
1890
1866
|
|
1891
1867
|
```ruby
|
1892
|
-
Product.search(
|
1868
|
+
Product.search.knn(field: :embedding, vector: [1, 2, 3], ef_search: 40).limit(10)
|
1893
1869
|
```
|
1894
1870
|
|
1895
1871
|
## Semantic Search
|
@@ -1924,7 +1900,7 @@ query_embedding = embed.(query_prefix + query, **embed_options)
|
|
1924
1900
|
And perform nearest neighbor search
|
1925
1901
|
|
1926
1902
|
```ruby
|
1927
|
-
Product.search(
|
1903
|
+
Product.search.knn(field: :embedding, vector: query_embedding).limit(20)
|
1928
1904
|
```
|
1929
1905
|
|
1930
1906
|
See a [full example](examples/semantic.rb)
|
@@ -1934,8 +1910,8 @@ See a [full example](examples/semantic.rb)
|
|
1934
1910
|
Perform keyword search and semantic search in parallel
|
1935
1911
|
|
1936
1912
|
```ruby
|
1937
|
-
keyword_search = Product.search(query
|
1938
|
-
semantic_search = Product.search(
|
1913
|
+
keyword_search = Product.search(query).limit(20)
|
1914
|
+
semantic_search = Product.search.knn(field: :embedding, vector: query_embedding).limit(20)
|
1939
1915
|
Searchkick.multi_search([keyword_search, semantic_search])
|
1940
1916
|
```
|
1941
1917
|
|
@@ -2023,36 +1999,33 @@ Searchkick.index_prefix = "datakick"
|
|
2023
1999
|
Use a different term for boosting by conversions
|
2024
2000
|
|
2025
2001
|
```ruby
|
2026
|
-
Product.search("banana"
|
2002
|
+
Product.search("banana").conversions_v2(term: "organic banana")
|
2027
2003
|
```
|
2028
2004
|
|
2029
|
-
|
2005
|
+
Define multiple conversion fields
|
2030
2006
|
|
2031
2007
|
```ruby
|
2032
2008
|
class Product < ApplicationRecord
|
2033
2009
|
has_many :searches, class_name: "Searchjoy::Search"
|
2034
2010
|
|
2035
|
-
|
2036
|
-
searchkick conversions: ["unique_user_conversions", "total_conversions"]
|
2011
|
+
searchkick conversions_v2: ["unique_conversions", "total_conversions"]
|
2037
2012
|
|
2038
2013
|
def search_data
|
2039
2014
|
{
|
2040
2015
|
name: name,
|
2041
|
-
|
2042
|
-
# {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
|
2016
|
+
unique_conversions: searches.group(:query).distinct.count(:user_id),
|
2043
2017
|
total_conversions: searches.group(:query).count
|
2044
|
-
# {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
|
2045
2018
|
}
|
2046
2019
|
end
|
2047
2020
|
end
|
2048
2021
|
```
|
2049
2022
|
|
2050
|
-
|
2023
|
+
And specify which to use
|
2051
2024
|
|
2052
2025
|
```ruby
|
2053
2026
|
Product.search("banana") # boost by both fields (default)
|
2054
|
-
Product.search("banana"
|
2055
|
-
Product.search("banana"
|
2027
|
+
Product.search("banana").conversions_v2("total_conversions") # only boost by total_conversions
|
2028
|
+
Product.search("banana").conversions_v2(false) # no conversion boosting
|
2056
2029
|
```
|
2057
2030
|
|
2058
2031
|
Change timeout
|
@@ -2073,28 +2046,56 @@ Change the search method name
|
|
2073
2046
|
Searchkick.search_method_name = :lookup
|
2074
2047
|
```
|
2075
2048
|
|
2076
|
-
Change
|
2049
|
+
Change the queue name
|
2050
|
+
|
2051
|
+
```ruby
|
2052
|
+
Searchkick.queue_name = :search_reindex # defaults to :searchkick
|
2053
|
+
```
|
2054
|
+
|
2055
|
+
Change the queue name or priority for a model
|
2056
|
+
|
2057
|
+
```ruby
|
2058
|
+
class Product < ApplicationRecord
|
2059
|
+
searchkick job_options: {queue: "critical", priority: 10}
|
2060
|
+
end
|
2061
|
+
```
|
2062
|
+
|
2063
|
+
Change the queue name or priority for a specific call
|
2064
|
+
|
2065
|
+
```ruby
|
2066
|
+
Product.reindex(mode: :async, job_options: {queue: "critical", priority: 10})
|
2067
|
+
```
|
2068
|
+
|
2069
|
+
Change the parent job
|
2077
2070
|
|
2078
2071
|
```ruby
|
2079
|
-
Searchkick.
|
2072
|
+
Searchkick.parent_job = "ApplicationJob" # defaults to "ActiveJob::Base"
|
2080
2073
|
```
|
2081
2074
|
|
2082
2075
|
Eager load associations
|
2083
2076
|
|
2084
2077
|
```ruby
|
2085
|
-
Product.search("milk"
|
2078
|
+
Product.search("milk").includes(:brand, :stores)
|
2086
2079
|
```
|
2087
2080
|
|
2088
2081
|
Eager load different associations by model
|
2089
2082
|
|
2090
2083
|
```ruby
|
2091
|
-
Searchkick.search("*"
|
2084
|
+
Searchkick.search("*").models(Product, Store).model_includes(Product => [:store], Store => [:product])
|
2092
2085
|
```
|
2093
2086
|
|
2094
2087
|
Run additional scopes on results
|
2095
2088
|
|
2096
2089
|
```ruby
|
2097
|
-
Product.search("milk"
|
2090
|
+
Product.search("milk").scope_results(->(r) { r.with_attached_images })
|
2091
|
+
```
|
2092
|
+
|
2093
|
+
Set opaque id for slow logs
|
2094
|
+
|
2095
|
+
```ruby
|
2096
|
+
Product.search("milk").opaque_id("some-id")
|
2097
|
+
# or
|
2098
|
+
Searchkick.multi_search(searches, opaque_id: "some-id")
|
2098
2099
|
```
|
2099
2100
|
|
2100
2101
|
Specify default fields to search
|
@@ -2159,7 +2160,7 @@ end
|
|
2159
2160
|
Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
|
2160
2161
|
|
2161
2162
|
```ruby
|
2162
|
-
Product.search("carrots"
|
2163
|
+
Product.search("carrots").request_params(search_type: "dfs_query_then_fetch")
|
2163
2164
|
```
|
2164
2165
|
|
2165
2166
|
Set options across all models
|
@@ -2174,10 +2175,11 @@ Reindex conditionally
|
|
2174
2175
|
|
2175
2176
|
```ruby
|
2176
2177
|
class Product < ApplicationRecord
|
2177
|
-
searchkick
|
2178
|
+
searchkick callback_options: {if: :search_data_changed?}
|
2178
2179
|
|
2179
|
-
|
2180
|
-
|
2180
|
+
def search_data_changed?
|
2181
|
+
previous_changes.include?("name")
|
2182
|
+
end
|
2181
2183
|
end
|
2182
2184
|
```
|
2183
2185
|
|
@@ -2190,13 +2192,7 @@ rake searchkick:reindex:all
|
|
2190
2192
|
Turn on misspellings after a certain number of characters
|
2191
2193
|
|
2192
2194
|
```ruby
|
2193
|
-
Product.search("api"
|
2194
|
-
```
|
2195
|
-
|
2196
|
-
**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch 1
|
2197
|
-
|
2198
|
-
```ruby
|
2199
|
-
Product.search("ah", misspellings: {prefix_length: 2}) # ah, no aha
|
2195
|
+
Product.search("api").misspellings(prefix_length: 2) # api, apt, no ahi
|
2200
2196
|
```
|
2201
2197
|
|
2202
2198
|
BigDecimal values are indexed as floats by default so they can be used for boosting. Convert them to strings to keep full precision.
|
@@ -2234,9 +2230,53 @@ end
|
|
2234
2230
|
|
2235
2231
|
For convenience, this is set by default in the test environment.
|
2236
2232
|
|
2233
|
+
## Upgrading
|
2234
|
+
|
2235
|
+
### 6.0
|
2236
|
+
|
2237
|
+
Searchkick 6 brings a new query builder API:
|
2238
|
+
|
2239
|
+
```ruby
|
2240
|
+
Product.search("apples").where(in_stock: true).limit(10).offset(50)
|
2241
|
+
```
|
2242
|
+
|
2243
|
+
All existing options can be used as methods, or you can continue to use the existing API.
|
2244
|
+
|
2245
|
+
This release also significantly improves the performance of searches when using conversions. To upgrade without downtime, add `conversions_v2` to your model and an additional field to `search_data`:
|
2246
|
+
|
2247
|
+
```ruby
|
2248
|
+
class Product < ApplicationRecord
|
2249
|
+
searchkick conversions: [:conversions], conversions_v2: [:conversions_v2]
|
2250
|
+
|
2251
|
+
def search_data
|
2252
|
+
conversions = searches.group(:query).distinct.count(:user_id)
|
2253
|
+
{
|
2254
|
+
conversions: conversions,
|
2255
|
+
conversions_v2: conversions
|
2256
|
+
}
|
2257
|
+
end
|
2258
|
+
end
|
2259
|
+
```
|
2260
|
+
|
2261
|
+
Reindex, then remove `conversions`:
|
2262
|
+
|
2263
|
+
```ruby
|
2264
|
+
class Product < ApplicationRecord
|
2265
|
+
searchkick conversions_v2: [:conversions_v2]
|
2266
|
+
|
2267
|
+
def search_data
|
2268
|
+
{
|
2269
|
+
conversions_v2: searches.group(:query).distinct.count(:user_id)
|
2270
|
+
}
|
2271
|
+
end
|
2272
|
+
end
|
2273
|
+
```
|
2274
|
+
|
2275
|
+
Other improvements include the option to ignore errors for missing documents with partial reindexing and more customization for background jobs. Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md) for the full list of changes.
|
2276
|
+
|
2237
2277
|
## History
|
2238
2278
|
|
2239
|
-
View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md)
|
2279
|
+
View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md)
|
2240
2280
|
|
2241
2281
|
## Thanks
|
2242
2282
|
|