searchkick 5.5.2 → 6.0.1

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.
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 7, 8, and 9 and OpenSearch 1, 2, and 3. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
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,40 +95,27 @@ Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.
96
95
  Query like SQL
97
96
 
98
97
  ```ruby
99
- Product.search("apples", where: {in_stock: true}, limit: 10, offset: 50)
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: [:name, :brand]
104
+ fields(:name, :brand)
106
105
  ```
107
106
 
108
107
  Where
109
108
 
110
109
  ```ruby
111
- where: {
112
- expires_at: {gt: Time.now}, # lt, gte, lte also available
113
- orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
114
- aisle_id: [25, 30], # in
115
- store_id: {not: 2}, # not
116
- aisle_id: {not: [25, 30]}, # not in
117
- user_ids: {all: [1, 3]}, # all elements in array
118
- category: {like: "%frozen%"}, # like
119
- category: {ilike: "%frozen%"}, # ilike
120
- category: /frozen .+/, # regexp
121
- category: {prefix: "frozen"}, # prefix
122
- store_id: {exists: true}, # exists
123
- _not: {store_id: 1}, # negate a condition
124
- _or: [{in_stock: true}, {backordered: true}],
125
- _and: [{in_stock: true}, {backordered: true}]
126
- }
110
+ where(store_id: 1, expires_at: Time.now..)
127
111
  ```
128
112
 
113
+ [These types of filters are supported](#filtering)
114
+
129
115
  Order
130
116
 
131
117
  ```ruby
132
- order: {_score: :desc} # most relevant first - default
118
+ order(_score: :desc) # most relevant first - default
133
119
  ```
134
120
 
135
121
  [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
@@ -137,13 +123,13 @@ order: {_score: :desc} # most relevant first - default
137
123
  Limit / offset
138
124
 
139
125
  ```ruby
140
- limit: 20, offset: 40
126
+ limit(20).offset(40)
141
127
  ```
142
128
 
143
129
  Select
144
130
 
145
131
  ```ruby
146
- select: [:name]
132
+ select(:name)
147
133
  ```
148
134
 
149
135
  [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering)
@@ -162,7 +148,7 @@ results.each { |result| ... }
162
148
  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
149
 
164
150
  ```ruby
165
- Product.search("apples", load: false)
151
+ Product.search("apples").load(false)
166
152
  ```
167
153
 
168
154
  Get total results
@@ -185,33 +171,113 @@ results.response
185
171
 
186
172
  **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.
187
173
 
174
+ ### Filtering
175
+
176
+ Equal
177
+
178
+ ```ruby
179
+ where(store_id: 1)
180
+ ```
181
+
182
+ Not equal
183
+
184
+ ```ruby
185
+ where.not(store_id: 2)
186
+ ```
187
+
188
+ Greater than (`gt`), less than (`lt`), greater than or equal (`gte`), less than or equal (`lte`)
189
+
190
+ ```ruby
191
+ where(expires_at: {gt: Time.now})
192
+ ```
193
+
194
+ Range
195
+
196
+ ```ruby
197
+ where(orders_count: 1..10)
198
+ ```
199
+
200
+ In
201
+
202
+ ```ruby
203
+ where(aisle_id: [25, 30])
204
+ ```
205
+
206
+ Not in
207
+
208
+ ```ruby
209
+ where.not(aisle_id: [25, 30])
210
+ ```
211
+
212
+ Contains all
213
+
214
+ ```ruby
215
+ where(user_ids: {all: [1, 3]})
216
+ ```
217
+
218
+ Like
219
+
220
+ ```ruby
221
+ where(category: {like: "%frozen%"})
222
+ ```
223
+
224
+ Case-insensitive like
225
+
226
+ ```ruby
227
+ where(category: {ilike: "%frozen%"})
228
+ ```
229
+
230
+ Regular expression
231
+
232
+ ```ruby
233
+ where(category: /frozen .+/)
234
+ ```
235
+
236
+ Prefix
237
+
238
+ ```ruby
239
+ where(category: {prefix: "frozen"})
240
+ ```
241
+
242
+ Exists
243
+
244
+ ```ruby
245
+ where(store_id: {exists: true})
246
+ ```
247
+
248
+ Combine filters with OR
249
+
250
+ ```ruby
251
+ where(_or: [{in_stock: true}, {backordered: true}])
252
+ ```
253
+
188
254
  ### Boosting
189
255
 
190
256
  Boost important fields
191
257
 
192
258
  ```ruby
193
- fields: ["title^10", "description"]
259
+ fields("title^10", "description")
194
260
  ```
195
261
 
196
262
  Boost by the value of a field (field must be numeric)
197
263
 
198
264
  ```ruby
199
- boost_by: [:orders_count] # give popular documents a little boost
200
- boost_by: {orders_count: {factor: 10}} # default factor is 1
265
+ boost_by(:orders_count) # give popular documents a little boost
266
+ boost_by(orders_count: {factor: 10}) # default factor is 1
201
267
  ```
202
268
 
203
269
  Boost matching documents
204
270
 
205
271
  ```ruby
206
- boost_where: {user_id: 1}
207
- boost_where: {user_id: {value: 1, factor: 100}} # default factor is 1000
208
- boost_where: {user_id: [{value: 1, factor: 100}, {value: 2, factor: 200}]}
272
+ boost_where(user_id: 1)
273
+ boost_where(user_id: {value: 1, factor: 100}) # default factor is 1000
274
+ boost_where(user_id: [{value: 1, factor: 100}, {value: 2, factor: 200}])
209
275
  ```
210
276
 
211
277
  Boost by recency
212
278
 
213
279
  ```ruby
214
- boost_by_recency: {created_at: {scale: "7d", decay: 0.5}}
280
+ boost_by_recency(created_at: {scale: "7d", decay: 0.5})
215
281
  ```
216
282
 
217
283
  You can also boost by:
@@ -233,7 +299,7 @@ Plays nicely with kaminari and will_paginate.
233
299
 
234
300
  ```ruby
235
301
  # controller
236
- @products = Product.search("milk", page: params[:page], per_page: 20)
302
+ @products = Product.search("milk").page(params[:page]).per_page(20)
237
303
  ```
238
304
 
239
305
  View with kaminari
@@ -259,7 +325,7 @@ Product.search("fresh honey") # fresh AND honey
259
325
  To change this, use:
260
326
 
261
327
  ```ruby
262
- Product.search("fresh honey", operator: "or") # fresh OR honey
328
+ Product.search("fresh honey").operator("or") # fresh OR honey
263
329
  ```
264
330
 
265
331
  By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
@@ -273,7 +339,7 @@ end
273
339
  And to search (after you reindex):
274
340
 
275
341
  ```ruby
276
- Product.search("back", fields: [:name], match: :word_start)
342
+ Product.search("back").fields(:name).match(:word_start)
277
343
  ```
278
344
 
279
345
  Available options are:
@@ -293,7 +359,7 @@ The default is `:word`. The most matches will happen with `:word_middle`.
293
359
  To specify different matching for different fields, use:
294
360
 
295
361
  ```ruby
296
- Product.search(query, fields: [{name: :word_start}, {brand: :word_middle}])
362
+ Product.search(query).fields({name: :word_start}, {brand: :word_middle})
297
363
  ```
298
364
 
299
365
  ### Exact Matches
@@ -301,7 +367,7 @@ Product.search(query, fields: [{name: :word_start}, {brand: :word_middle}])
301
367
  To match a field exactly (case-sensitive), use:
302
368
 
303
369
  ```ruby
304
- Product.search(query, fields: [{name: :exact}])
370
+ Product.search(query).fields({name: :exact})
305
371
  ```
306
372
 
307
373
  ### Phrase Matches
@@ -309,7 +375,7 @@ Product.search(query, fields: [{name: :exact}])
309
375
  To only match the exact order, use:
310
376
 
311
377
  ```ruby
312
- Product.search("fresh honey", match: :phrase)
378
+ Product.search("fresh honey").match(:phrase)
313
379
  ```
314
380
 
315
381
  ### Stemming and Language
@@ -385,11 +451,7 @@ search_synonyms: ["lightbulb => halogenlamp"]
385
451
 
386
452
  ### Dynamic Synonyms
387
453
 
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.
454
+ 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
455
 
394
456
  ```txt
395
457
  pop, soda
@@ -410,29 +472,6 @@ And reload with:
410
472
  Product.search_index.reload_synonyms
411
473
  ```
412
474
 
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
475
  ### Misspellings
437
476
 
438
477
  By default, Searchkick handles misspelled queries by returning results with an [edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance) of one.
@@ -440,13 +479,13 @@ By default, Searchkick handles misspelled queries by returning results with an [
440
479
  You can change this with:
441
480
 
442
481
  ```ruby
443
- Product.search("zucini", misspellings: {edit_distance: 2}) # zucchini
482
+ Product.search("zucini").misspellings(edit_distance: 2) # zucchini
444
483
  ```
445
484
 
446
485
  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
486
 
448
487
  ```ruby
449
- Product.search("zuchini", misspellings: {below: 5})
488
+ Product.search("zuchini").misspellings(below: 5)
450
489
  ```
451
490
 
452
491
  If there are fewer than 5 results, a 2nd search is performed with misspellings enabled. The result of this query is returned.
@@ -454,13 +493,13 @@ If there are fewer than 5 results, a 2nd search is performed with misspellings e
454
493
  Turn off misspellings with:
455
494
 
456
495
  ```ruby
457
- Product.search("zuchini", misspellings: false) # no zucchini
496
+ Product.search("zuchini").misspellings(false) # no zucchini
458
497
  ```
459
498
 
460
499
  Specify which fields can include misspellings with:
461
500
 
462
501
  ```ruby
463
- Product.search("zucini", fields: [:name, :color], misspellings: {fields: [:name]})
502
+ Product.search("zucini").fields(:name, :color).misspellings(fields: [:name])
464
503
  ```
465
504
 
466
505
  > When doing this, you must also specify fields to search
@@ -470,7 +509,7 @@ Product.search("zucini", fields: [:name, :color], misspellings: {fields: [:name]
470
509
  If a user searches `butter`, they may also get results for `peanut butter`. To prevent this, use:
471
510
 
472
511
  ```ruby
473
- Product.search("butter", exclude: ["peanut butter"])
512
+ Product.search("butter").exclude("peanut butter")
474
513
  ```
475
514
 
476
515
  You can map queries and terms to exclude with:
@@ -481,13 +520,13 @@ exclude_queries = {
481
520
  "cream" => ["ice cream", "whipped cream"]
482
521
  }
483
522
 
484
- Product.search(query, exclude: exclude_queries[query])
523
+ Product.search(query).exclude(exclude_queries[query])
485
524
  ```
486
525
 
487
526
  You can demote results by boosting by a factor less than one:
488
527
 
489
528
  ```ruby
490
- Product.search("butter", boost_where: {category: {value: "pantry", factor: 0.5}})
529
+ Product.search("butter").boost_where(category: {value: "pantry", factor: 0.5})
491
530
  ```
492
531
 
493
532
  ### Emoji
@@ -503,7 +542,7 @@ gem "gemoji-parser"
503
542
  And use:
504
543
 
505
544
  ```ruby
506
- Product.search("🍨🍰", emoji: true)
545
+ Product.search("🍨🍰").emoji
507
546
  ```
508
547
 
509
548
  ## Indexing
@@ -669,7 +708,7 @@ end
669
708
  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
709
 
671
710
  ```ruby
672
- Product.search("apple", track: {user_id: current_user.id})
711
+ Product.search("apple").track(user_id: current_user.id)
673
712
  ```
674
713
 
675
714
  [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 +722,7 @@ class Product < ApplicationRecord
683
722
  has_many :conversions, class_name: "Searchjoy::Conversion", as: :convertable
684
723
  has_many :searches, class_name: "Searchjoy::Search", through: :conversions
685
724
 
686
- searchkick conversions: [:conversions] # name of field
725
+ searchkick conversions_v2: [:conversions] # name of field
687
726
 
688
727
  def search_data
689
728
  {
@@ -695,7 +734,7 @@ class Product < ApplicationRecord
695
734
  end
696
735
  ```
697
736
 
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.
737
+ 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
738
 
700
739
  ### Performant Conversions
701
740
 
@@ -711,7 +750,7 @@ Next, update your model. Create a separate method for conversion data so you can
711
750
 
712
751
  ```ruby
713
752
  class Product < ApplicationRecord
714
- searchkick conversions: [:conversions]
753
+ searchkick conversions_v2: [:conversions]
715
754
 
716
755
  def search_data
717
756
  {
@@ -728,7 +767,7 @@ class Product < ApplicationRecord
728
767
  end
729
768
  ```
730
769
 
731
- Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions: false` in your search calls until the data is reindexed.
770
+ Deploy and reindex your data. For zero downtime deployment, temporarily set `conversions_v2(false)` in your search calls until the data is reindexed.
732
771
 
733
772
  ```ruby
734
773
  Product.reindex
@@ -743,8 +782,8 @@ class UpdateConversionsJob < ApplicationJob
743
782
 
744
783
  # get records that have a recent conversion
745
784
  recently_converted_ids =
746
- Searchjoy::Conversion.where(convertable_type: class_name).where(created_at: since..)
747
- .order(:convertable_id).distinct.pluck(:convertable_id)
785
+ Searchjoy::Conversion.where(convertable_type: class_name, created_at: since..)
786
+ .order(:convertable_id).distinct.pluck(:convertable_id)
748
787
 
749
788
  # split into batches
750
789
  recently_converted_ids.in_groups_of(1000, false) do |ids|
@@ -752,8 +791,8 @@ class UpdateConversionsJob < ApplicationJob
752
791
  # fetch conversions
753
792
  conversions =
754
793
  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)
794
+ .joins(:search).where.not(searchjoy_searches: {user_id: nil})
795
+ .group(:convertable_id, :query).distinct.count(:user_id)
757
796
 
758
797
  # group by record
759
798
  conversions_by_record = {}
@@ -771,7 +810,7 @@ class UpdateConversionsJob < ApplicationJob
771
810
 
772
811
  if reindex
773
812
  # reindex conversions data
774
- model.where(id: ids).reindex(:conversions_data)
813
+ model.where(id: ids).reindex(:conversions_data, ignore_missing: true)
775
814
  end
776
815
  end
777
816
  end
@@ -808,7 +847,7 @@ end
808
847
  Reindex and search with:
809
848
 
810
849
  ```ruby
811
- Product.search("milk", boost_where: {orderer_ids: current_user.id})
850
+ Product.search("milk").boost_where(orderer_ids: current_user.id)
812
851
  ```
813
852
 
814
853
  ## Instant Search / Autocomplete
@@ -832,7 +871,7 @@ end
832
871
  Reindex and search with:
833
872
 
834
873
  ```ruby
835
- Movie.search("jurassic pa", fields: [:title], match: :word_start)
874
+ Movie.search("jurassic pa").fields(:title).match(:word_start)
836
875
  ```
837
876
 
838
877
  Use a front-end library like [typeahead.js](https://twitter.github.io/typeahead.js/) to show the results.
@@ -844,19 +883,13 @@ First, add a route and controller action.
844
883
  ```ruby
845
884
  class MoviesController < ApplicationController
846
885
  def autocomplete
847
- render json: Movie.search(
848
- params[:query],
849
- fields: ["title^5", "director"],
850
- match: :word_start,
851
- limit: 10,
852
- load: false,
853
- misspellings: {below: 5}
854
- ).map(&:title)
886
+ render json: Movie.search(params[:query]).fields("title^5", "director")
887
+ .match(:word_start).limit(10).load(false).misspellings(below: 5).map(&:title)
855
888
  end
856
889
  end
857
890
  ```
858
891
 
859
- **Note:** Use `load: false` and `misspellings: {below: n}` (or `misspellings: false`) for best performance.
892
+ **Note:** Use `load(false)` and `misspellings(below: n)` (or `misspellings(false)`) for best performance.
860
893
 
861
894
  Then add the search box and JavaScript code to a view.
862
895
 
@@ -893,7 +926,7 @@ end
893
926
  Reindex and search with:
894
927
 
895
928
  ```ruby
896
- products = Product.search("peantu butta", suggest: true)
929
+ products = Product.search("peantu butta").suggest
897
930
  products.suggestions # ["peanut butter"]
898
931
  ```
899
932
 
@@ -904,40 +937,40 @@ products.suggestions # ["peanut butter"]
904
937
  ![Aggregations](https://gist.githubusercontent.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/facets.png)
905
938
 
906
939
  ```ruby
907
- products = Product.search("chuck taylor", aggs: [:product_type, :gender, :brand])
940
+ products = Product.search("chuck taylor").aggs(:product_type, :gender, :brand)
908
941
  products.aggs
909
942
  ```
910
943
 
911
944
  By default, `where` conditions apply to aggregations.
912
945
 
913
946
  ```ruby
914
- Product.search("wingtips", where: {color: "brandy"}, aggs: [:size])
947
+ Product.search("wingtips").where(color: "brandy").aggs(:size)
915
948
  # aggregations for brandy wingtips are returned
916
949
  ```
917
950
 
918
951
  Change this with:
919
952
 
920
953
  ```ruby
921
- Product.search("wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false)
954
+ Product.search("wingtips").where(color: "brandy").aggs(:size).smart_aggs(false)
922
955
  # aggregations for all wingtips are returned
923
956
  ```
924
957
 
925
958
  Set `where` conditions for each aggregation separately with:
926
959
 
927
960
  ```ruby
928
- Product.search("wingtips", aggs: {size: {where: {color: "brandy"}}})
961
+ Product.search("wingtips").aggs(size: {where: {color: "brandy"}})
929
962
  ```
930
963
 
931
964
  Limit
932
965
 
933
966
  ```ruby
934
- Product.search("apples", aggs: {store_id: {limit: 10}})
967
+ Product.search("apples").aggs(store_id: {limit: 10})
935
968
  ```
936
969
 
937
970
  Order
938
971
 
939
972
  ```ruby
940
- Product.search("wingtips", aggs: {color: {order: {"_key" => "asc"}}}) # alphabetically
973
+ Product.search("wingtips").aggs(color: {order: {"_key" => "asc"}}) # alphabetically
941
974
  ```
942
975
 
943
976
  [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 +979,31 @@ Ranges
946
979
 
947
980
  ```ruby
948
981
  price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
949
- Product.search("*", aggs: {price: {ranges: price_ranges}})
982
+ Product.search("*").aggs(price: {ranges: price_ranges})
950
983
  ```
951
984
 
952
985
  Minimum document count
953
986
 
954
987
  ```ruby
955
- Product.search("apples", aggs: {store_id: {min_doc_count: 2}})
988
+ Product.search("apples").aggs(store_id: {min_doc_count: 2})
956
989
  ```
957
990
 
958
991
  Script support
959
992
 
960
993
  ```ruby
961
- Product.search("*", aggs: {color: {script: {source: "'Color: ' + _value"}}})
994
+ Product.search("*").aggs(color: {script: {source: "'Color: ' + _value"}})
962
995
  ```
963
996
 
964
997
  Date histogram
965
998
 
966
999
  ```ruby
967
- Product.search("pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}})
1000
+ Product.search("pear").aggs(products_per_year: {date_histogram: {field: :created_at, interval: :year}})
968
1001
  ```
969
1002
 
970
1003
  For other aggregation types, including sub-aggregations, use `body_options`:
971
1004
 
972
1005
  ```ruby
973
- Product.search("orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}})
1006
+ Product.search("orange").body_options(aggs: {price: {histogram: {field: :price, interval: 10}}})
974
1007
  ```
975
1008
 
976
1009
  ## Highlight
@@ -986,7 +1019,7 @@ end
986
1019
  Highlight the search query in the results.
987
1020
 
988
1021
  ```ruby
989
- bands = Band.search("cinema", highlight: true)
1022
+ bands = Band.search("cinema").highlight
990
1023
  ```
991
1024
 
992
1025
  View the highlighted fields with:
@@ -1000,19 +1033,19 @@ end
1000
1033
  To change the tag, use:
1001
1034
 
1002
1035
  ```ruby
1003
- Band.search("cinema", highlight: {tag: "<strong>"})
1036
+ Band.search("cinema").highlight(tag: "<strong>")
1004
1037
  ```
1005
1038
 
1006
1039
  To highlight and search different fields, use:
1007
1040
 
1008
1041
  ```ruby
1009
- Band.search("cinema", fields: [:name], highlight: {fields: [:description]})
1042
+ Band.search("cinema").fields(:name).highlight(fields: [:description])
1010
1043
  ```
1011
1044
 
1012
1045
  By default, the entire field is highlighted. To get small snippets instead, use:
1013
1046
 
1014
1047
  ```ruby
1015
- bands = Band.search("cinema", highlight: {fragment_size: 20})
1048
+ bands = Band.search("cinema").highlight(fragment_size: 20)
1016
1049
  bands.with_highlights(multiple: true).each do |band, highlights|
1017
1050
  highlights[:name].join(" and ")
1018
1051
  end
@@ -1021,18 +1054,18 @@ end
1021
1054
  Additional options can be specified for each field:
1022
1055
 
1023
1056
  ```ruby
1024
- Band.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}})
1057
+ Band.search("cinema").fields(:name).highlight(fields: {name: {fragment_size: 200}})
1025
1058
  ```
1026
1059
 
1027
1060
  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
1061
 
1029
1062
  ## Similar Items
1030
1063
 
1031
- Find similar items.
1064
+ Find similar items
1032
1065
 
1033
1066
  ```ruby
1034
1067
  product = Product.first
1035
- product.similar(fields: [:name], where: {size: "12 oz"})
1068
+ product.similar.fields(:name).where(size: "12 oz")
1036
1069
  ```
1037
1070
 
1038
1071
  ## Geospatial Searches
@@ -1050,13 +1083,13 @@ end
1050
1083
  Reindex and search with:
1051
1084
 
1052
1085
  ```ruby
1053
- Restaurant.search("pizza", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}}) # or 160km
1086
+ Restaurant.search("pizza").where(location: {near: {lat: 37, lon: -114}, within: "100mi"}) # or 160km
1054
1087
  ```
1055
1088
 
1056
1089
  Bounded by a box
1057
1090
 
1058
1091
  ```ruby
1059
- Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}})
1092
+ Restaurant.search("sushi").where(location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}})
1060
1093
  ```
1061
1094
 
1062
1095
  **Note:** `top_right` and `bottom_left` also work
@@ -1064,7 +1097,7 @@ Restaurant.search("sushi", where: {location: {top_left: {lat: 38, lon: -123}, bo
1064
1097
  Bounded by a polygon
1065
1098
 
1066
1099
  ```ruby
1067
- Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}})
1100
+ Restaurant.search("dessert").where(location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}})
1068
1101
  ```
1069
1102
 
1070
1103
  ### Boost By Distance
@@ -1072,13 +1105,13 @@ Restaurant.search("dessert", where: {location: {geo_polygon: {points: [{lat: 38,
1072
1105
  Boost results by distance - closer results are boosted more
1073
1106
 
1074
1107
  ```ruby
1075
- Restaurant.search("noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}})
1108
+ Restaurant.search("noodles").boost_by_distance(location: {origin: {lat: 37, lon: -122}})
1076
1109
  ```
1077
1110
 
1078
1111
  Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
1079
1112
 
1080
1113
  ```ruby
1081
- Restaurant.search("wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}})
1114
+ Restaurant.search("wings").boost_by_distance(location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5})
1082
1115
  ```
1083
1116
 
1084
1117
  ### Geo Shapes
@@ -1105,19 +1138,19 @@ See the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsea
1105
1138
  Find shapes intersecting with the query shape
1106
1139
 
1107
1140
  ```ruby
1108
- Restaurant.search("soup", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}})
1141
+ Restaurant.search("soup").where(bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}})
1109
1142
  ```
1110
1143
 
1111
1144
  Falling entirely within the query shape
1112
1145
 
1113
1146
  ```ruby
1114
- Restaurant.search("salad", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: {lat: 38, lon: -123}, radius: "1km"}}})
1147
+ Restaurant.search("salad").where(bounds: {geo_shape: {type: "circle", relation: "within", coordinates: {lat: 38, lon: -123}, radius: "1km"}})
1115
1148
  ```
1116
1149
 
1117
1150
  Not touching the query shape
1118
1151
 
1119
1152
  ```ruby
1120
- Restaurant.search("burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}})
1153
+ Restaurant.search("burger").where(bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}})
1121
1154
  ```
1122
1155
 
1123
1156
  ## Inheritance
@@ -1147,9 +1180,9 @@ Dog.reindex # equivalent, all animals reindexed
1147
1180
  And to search, use:
1148
1181
 
1149
1182
  ```ruby
1150
- Animal.search("*") # all animals
1151
- Dog.search("*") # just dogs
1152
- Animal.search("*", type: [Dog, Cat]) # just cats and dogs
1183
+ Animal.search("*") # all animals
1184
+ Dog.search("*") # just dogs
1185
+ Animal.search("*").type(Dog, Cat) # just cats and dogs
1153
1186
  ```
1154
1187
 
1155
1188
  **Notes:**
@@ -1157,7 +1190,7 @@ Animal.search("*", type: [Dog, Cat]) # just cats and dogs
1157
1190
  1. The `suggest` option retrieves suggestions from the parent at the moment.
1158
1191
 
1159
1192
  ```ruby
1160
- Dog.search("airbudd", suggest: true) # suggestions for all animals
1193
+ Dog.search("airbudd").suggest # suggestions for all animals
1161
1194
  ```
1162
1195
  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
1196
 
@@ -1166,7 +1199,7 @@ Animal.search("*", type: [Dog, Cat]) # just cats and dogs
1166
1199
  To help with debugging queries, you can use:
1167
1200
 
1168
1201
  ```ruby
1169
- Product.search("soap", debug: true)
1202
+ Product.search("soap").debug
1170
1203
  ```
1171
1204
 
1172
1205
  This prints useful info to `stdout`.
@@ -1174,7 +1207,7 @@ This prints useful info to `stdout`.
1174
1207
  See how the search server scores your queries with:
1175
1208
 
1176
1209
  ```ruby
1177
- Product.search("soap", explain: true).response
1210
+ Product.search("soap").explain.response
1178
1211
  ```
1179
1212
 
1180
1213
  See how the search server tokenizes your queries with:
@@ -1208,37 +1241,42 @@ As you iterate on your search, it’s a good idea to add tests.
1208
1241
 
1209
1242
  For performance, only enable Searchkick callbacks for the tests that need it.
1210
1243
 
1211
- ### Parallel Tests
1244
+ ### Rails
1212
1245
 
1213
- Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
1246
+ Add to your `test/test_helper.rb`:
1214
1247
 
1215
1248
  ```ruby
1216
- class ActiveSupport::TestCase
1217
- parallelize_setup do |worker|
1218
- Searchkick.index_suffix = worker
1249
+ module ActiveSupport
1250
+ class TestCase
1251
+ parallelize_setup do |worker|
1252
+ Searchkick.index_suffix = worker
1219
1253
 
1220
- # reindex models
1221
- Product.reindex
1222
-
1223
- # and disable callbacks
1224
- Searchkick.disable_callbacks
1254
+ # reindex models for parallel tests
1255
+ Product.reindex
1256
+ end
1225
1257
  end
1226
1258
  end
1259
+
1260
+ # reindex models for non-parallel tests
1261
+ Product.reindex
1262
+
1263
+ # and disable callbacks
1264
+ Searchkick.disable_callbacks
1227
1265
  ```
1228
1266
 
1229
1267
  And use:
1230
1268
 
1231
1269
  ```ruby
1232
1270
  class ProductTest < ActiveSupport::TestCase
1233
- def setup
1271
+ setup do
1234
1272
  Searchkick.enable_callbacks
1235
1273
  end
1236
1274
 
1237
- def teardown
1275
+ teardown do
1238
1276
  Searchkick.disable_callbacks
1239
1277
  end
1240
1278
 
1241
- def test_search
1279
+ test "search" do
1242
1280
  Product.create!(name: "Apple")
1243
1281
  Product.search_index.refresh
1244
1282
  assert_equal ["Apple"], Product.search("apple").map(&:name)
@@ -1314,25 +1352,24 @@ end
1314
1352
 
1315
1353
  ### Factory Bot
1316
1354
 
1317
- Use a trait and an after `create` hook for each indexed model:
1355
+ Define a trait for each model:
1318
1356
 
1319
1357
  ```ruby
1320
1358
  FactoryBot.define do
1321
1359
  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
1360
  trait :reindex do
1327
- after(:create) do |product, _evaluator|
1361
+ after(:create) do |product, _|
1328
1362
  product.reindex(refresh: true)
1329
1363
  end
1330
1364
  end
1331
1365
  end
1332
1366
  end
1367
+ ```
1333
1368
 
1334
- # use it
1335
- FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1369
+ And use:
1370
+
1371
+ ```ruby
1372
+ FactoryBot.create(:product, :reindex)
1336
1373
  ```
1337
1374
 
1338
1375
  ### GitHub Actions
@@ -1354,8 +1391,8 @@ And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy w
1354
1391
  For the search server, Searchkick uses `ENV["ELASTICSEARCH_URL"]` for Elasticsearch and `ENV["OPENSEARCH_URL"]` for OpenSearch. This defaults to `http://localhost:9200`.
1355
1392
 
1356
1393
  - [Elastic Cloud](#elastic-cloud)
1357
- - [Heroku](#heroku)
1358
1394
  - [Amazon OpenSearch Service](#amazon-opensearch-service)
1395
+ - [Heroku](#heroku)
1359
1396
  - [Self-Hosted and Other](#self-hosted-and-other)
1360
1397
 
1361
1398
  ### Elastic Cloud
@@ -1372,6 +1409,36 @@ Then deploy and reindex:
1372
1409
  rake searchkick:reindex:all
1373
1410
  ```
1374
1411
 
1412
+ ### Amazon OpenSearch Service
1413
+
1414
+ Create an initializer `config/initializers/opensearch.rb` with:
1415
+
1416
+ ```ruby
1417
+ ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
1418
+ ```
1419
+
1420
+ To use signed requests, include in your Gemfile:
1421
+
1422
+ ```ruby
1423
+ gem "faraday_middleware-aws-sigv4"
1424
+ ```
1425
+
1426
+ and add to your initializer:
1427
+
1428
+ ```ruby
1429
+ Searchkick.aws_credentials = {
1430
+ access_key_id: ENV["AWS_ACCESS_KEY_ID"],
1431
+ secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
1432
+ region: "us-east-1"
1433
+ }
1434
+ ```
1435
+
1436
+ Then deploy and reindex:
1437
+
1438
+ ```sh
1439
+ rake searchkick:reindex:all
1440
+ ```
1441
+
1375
1442
  ### Heroku
1376
1443
 
1377
1444
  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 +1489,6 @@ Then deploy and reindex:
1422
1489
  heroku run rake searchkick:reindex:all
1423
1490
  ```
1424
1491
 
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
1492
  ### Self-Hosted and Other
1456
1493
 
1457
1494
  Create an initializer with:
@@ -1522,8 +1559,10 @@ And create an initializer with:
1522
1559
 
1523
1560
  ```ruby
1524
1561
  class SearchSerializer
1562
+ CODER = JSON::Coder.new { |v, _| v.is_a?(Time) ? v.as_json : v }
1563
+
1525
1564
  def dump(object)
1526
- JSON.generate(object)
1565
+ CODER.generate(object)
1527
1566
  end
1528
1567
  end
1529
1568
 
@@ -1682,7 +1721,7 @@ end
1682
1721
  Reindex and search with:
1683
1722
 
1684
1723
  ```ruby
1685
- Business.search("ice cream", routing: params[:city_id])
1724
+ Business.search("ice cream").routing(params[:city_id])
1686
1725
  ```
1687
1726
 
1688
1727
  ### Partial Reindexing
@@ -1713,6 +1752,12 @@ And use:
1713
1752
  Product.reindex(:prices_data)
1714
1753
  ```
1715
1754
 
1755
+ Ignore errors for missing documents with:
1756
+
1757
+ ```ruby
1758
+ Product.reindex(:prices_data, ignore_missing: true)
1759
+ ```
1760
+
1716
1761
  ## Advanced
1717
1762
 
1718
1763
  Searchkick makes it easy to use the Elasticsearch or OpenSearch DSL on its own.
@@ -1745,7 +1790,7 @@ end
1745
1790
  And use the `body` option to search:
1746
1791
 
1747
1792
  ```ruby
1748
- products = Product.search(body: {query: {match: {name: "milk"}}})
1793
+ products = Product.search.body(query: {match: {name: "milk"}})
1749
1794
  ```
1750
1795
 
1751
1796
  View the response with:
@@ -1757,7 +1802,7 @@ products.response
1757
1802
  To modify the query generated by Searchkick, use:
1758
1803
 
1759
1804
  ```ruby
1760
- products = Product.search("milk", body_options: {min_score: 1})
1805
+ products = Product.search("milk").body_options(min_score: 1)
1761
1806
  ```
1762
1807
 
1763
1808
  or
@@ -1796,13 +1841,13 @@ Then use `products` and `coupons` as typical results.
1796
1841
  Search across multiple models with:
1797
1842
 
1798
1843
  ```ruby
1799
- Searchkick.search("milk", models: [Product, Category])
1844
+ Searchkick.search("milk").models(Product, Category)
1800
1845
  ```
1801
1846
 
1802
1847
  Boost specific models with:
1803
1848
 
1804
1849
  ```ruby
1805
- indices_boost: {Category => 2, Product => 1}
1850
+ indices_boost(Category => 2, Product => 1)
1806
1851
  ```
1807
1852
 
1808
1853
  ## Multi-Tenancy
@@ -1814,7 +1859,7 @@ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenan
1814
1859
  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
1860
 
1816
1861
  ```ruby
1817
- Product.search("*", scroll: "1m").scroll do |batch|
1862
+ Product.search("*").scroll("1m") do |batch|
1818
1863
  # process batch ...
1819
1864
  end
1820
1865
  ```
@@ -1822,7 +1867,7 @@ end
1822
1867
  You can also scroll batches manually.
1823
1868
 
1824
1869
  ```ruby
1825
- products = Product.search("*", scroll: "1m")
1870
+ products = Product.search("*").scroll("1m")
1826
1871
  while products.any?
1827
1872
  # process batch ...
1828
1873
 
@@ -1845,7 +1890,7 @@ end
1845
1890
  If you just need an accurate total count, you can instead use:
1846
1891
 
1847
1892
  ```ruby
1848
- Product.search("pears", body_options: {track_total_hits: true})
1893
+ Product.search("pears").body_options(track_total_hits: true)
1849
1894
  ```
1850
1895
 
1851
1896
  ## Nested Data
@@ -1853,7 +1898,7 @@ Product.search("pears", body_options: {track_total_hits: true})
1853
1898
  To query nested data, use dot notation.
1854
1899
 
1855
1900
  ```ruby
1856
- Product.search("san", fields: ["store.city"], where: {"store.zip_code" => 12345})
1901
+ Product.search("san").fields("store.city").where("store.zip_code" => 12345)
1857
1902
  ```
1858
1903
 
1859
1904
  ## Nearest Neighbor Search
@@ -1871,7 +1916,7 @@ Also supports `euclidean` and `inner_product`
1871
1916
  Reindex and search with:
1872
1917
 
1873
1918
  ```ruby
1874
- Product.search(knn: {field: :embedding, vector: [1, 2, 3]}, limit: 10)
1919
+ Product.search.knn(field: :embedding, vector: [1, 2, 3]).limit(10)
1875
1920
  ```
1876
1921
 
1877
1922
  ### HNSW Options
@@ -1889,7 +1934,7 @@ end
1889
1934
  Specify `ef_search`
1890
1935
 
1891
1936
  ```ruby
1892
- Product.search(knn: {field: :embedding, vector: [1, 2, 3], ef_search: 40}, limit: 10)
1937
+ Product.search.knn(field: :embedding, vector: [1, 2, 3], ef_search: 40).limit(10)
1893
1938
  ```
1894
1939
 
1895
1940
  ## Semantic Search
@@ -1924,7 +1969,7 @@ query_embedding = embed.(query_prefix + query, **embed_options)
1924
1969
  And perform nearest neighbor search
1925
1970
 
1926
1971
  ```ruby
1927
- Product.search(knn: {field: :embedding, vector: query_embedding}, limit: 20)
1972
+ Product.search.knn(field: :embedding, vector: query_embedding).limit(20)
1928
1973
  ```
1929
1974
 
1930
1975
  See a [full example](examples/semantic.rb)
@@ -1934,8 +1979,8 @@ See a [full example](examples/semantic.rb)
1934
1979
  Perform keyword search and semantic search in parallel
1935
1980
 
1936
1981
  ```ruby
1937
- keyword_search = Product.search(query, limit: 20)
1938
- semantic_search = Product.search(knn: {field: :embedding, vector: query_embedding}, limit: 20)
1982
+ keyword_search = Product.search(query).limit(20)
1983
+ semantic_search = Product.search.knn(field: :embedding, vector: query_embedding).limit(20)
1939
1984
  Searchkick.multi_search([keyword_search, semantic_search])
1940
1985
  ```
1941
1986
 
@@ -2023,36 +2068,33 @@ Searchkick.index_prefix = "datakick"
2023
2068
  Use a different term for boosting by conversions
2024
2069
 
2025
2070
  ```ruby
2026
- Product.search("banana", conversions_term: "organic banana")
2071
+ Product.search("banana").conversions_v2(term: "organic banana")
2027
2072
  ```
2028
2073
 
2029
- Multiple conversion fields
2074
+ Define multiple conversion fields
2030
2075
 
2031
2076
  ```ruby
2032
2077
  class Product < ApplicationRecord
2033
2078
  has_many :searches, class_name: "Searchjoy::Search"
2034
2079
 
2035
- # searchkick also supports multiple "conversions" fields
2036
- searchkick conversions: ["unique_user_conversions", "total_conversions"]
2080
+ searchkick conversions_v2: ["unique_conversions", "total_conversions"]
2037
2081
 
2038
2082
  def search_data
2039
2083
  {
2040
2084
  name: name,
2041
- unique_user_conversions: searches.group(:query).distinct.count(:user_id),
2042
- # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
2085
+ unique_conversions: searches.group(:query).distinct.count(:user_id),
2043
2086
  total_conversions: searches.group(:query).count
2044
- # {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
2045
2087
  }
2046
2088
  end
2047
2089
  end
2048
2090
  ```
2049
2091
 
2050
- and during query time:
2092
+ And specify which to use
2051
2093
 
2052
2094
  ```ruby
2053
2095
  Product.search("banana") # boost by both fields (default)
2054
- Product.search("banana", conversions: "total_conversions") # only boost by total_conversions
2055
- Product.search("banana", conversions: false) # no conversion boosting
2096
+ Product.search("banana").conversions_v2("total_conversions") # only boost by total_conversions
2097
+ Product.search("banana").conversions_v2(false) # no conversion boosting
2056
2098
  ```
2057
2099
 
2058
2100
  Change timeout
@@ -2073,28 +2115,56 @@ Change the search method name
2073
2115
  Searchkick.search_method_name = :lookup
2074
2116
  ```
2075
2117
 
2076
- Change search queue name
2118
+ Change the queue name
2077
2119
 
2078
2120
  ```ruby
2079
- Searchkick.queue_name = :search_reindex
2121
+ Searchkick.queue_name = :search_reindex # defaults to :searchkick
2122
+ ```
2123
+
2124
+ Change the queue name or priority for a model
2125
+
2126
+ ```ruby
2127
+ class Product < ApplicationRecord
2128
+ searchkick job_options: {queue: "critical", priority: 10}
2129
+ end
2130
+ ```
2131
+
2132
+ Change the queue name or priority for a specific call
2133
+
2134
+ ```ruby
2135
+ Product.reindex(mode: :async, job_options: {queue: "critical", priority: 10})
2136
+ ```
2137
+
2138
+ Change the parent job
2139
+
2140
+ ```ruby
2141
+ Searchkick.parent_job = "ApplicationJob" # defaults to "ActiveJob::Base"
2080
2142
  ```
2081
2143
 
2082
2144
  Eager load associations
2083
2145
 
2084
2146
  ```ruby
2085
- Product.search("milk", includes: [:brand, :stores])
2147
+ Product.search("milk").includes(:brand, :stores)
2086
2148
  ```
2087
2149
 
2088
2150
  Eager load different associations by model
2089
2151
 
2090
2152
  ```ruby
2091
- Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
2153
+ Searchkick.search("*").models(Product, Store).model_includes(Product => [:store], Store => [:product])
2092
2154
  ```
2093
2155
 
2094
2156
  Run additional scopes on results
2095
2157
 
2096
2158
  ```ruby
2097
- Product.search("milk", scope_results: ->(r) { r.with_attached_images })
2159
+ Product.search("milk").scope_results(->(r) { r.with_attached_images })
2160
+ ```
2161
+
2162
+ Set opaque id for slow logs
2163
+
2164
+ ```ruby
2165
+ Product.search("milk").opaque_id("some-id")
2166
+ # or
2167
+ Searchkick.multi_search(searches, opaque_id: "some-id")
2098
2168
  ```
2099
2169
 
2100
2170
  Specify default fields to search
@@ -2159,7 +2229,7 @@ end
2159
2229
  Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
2160
2230
 
2161
2231
  ```ruby
2162
- Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
2232
+ Product.search("carrots").request_params(search_type: "dfs_query_then_fetch")
2163
2233
  ```
2164
2234
 
2165
2235
  Set options across all models
@@ -2174,10 +2244,11 @@ Reindex conditionally
2174
2244
 
2175
2245
  ```ruby
2176
2246
  class Product < ApplicationRecord
2177
- searchkick callbacks: false
2247
+ searchkick callback_options: {if: :search_data_changed?}
2178
2248
 
2179
- # add the callbacks manually
2180
- after_commit :reindex, if: -> (model) { model.previous_changes.key?("name") } # use your own condition
2249
+ def search_data_changed?
2250
+ previous_changes.include?("name")
2251
+ end
2181
2252
  end
2182
2253
  ```
2183
2254
 
@@ -2190,13 +2261,7 @@ rake searchkick:reindex:all
2190
2261
  Turn on misspellings after a certain number of characters
2191
2262
 
2192
2263
  ```ruby
2193
- Product.search("api", misspellings: {prefix_length: 2}) # api, apt, no ahi
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
2264
+ Product.search("api").misspellings(prefix_length: 2) # api, apt, no ahi
2200
2265
  ```
2201
2266
 
2202
2267
  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 +2299,53 @@ end
2234
2299
 
2235
2300
  For convenience, this is set by default in the test environment.
2236
2301
 
2302
+ ## Upgrading
2303
+
2304
+ ### 6.0
2305
+
2306
+ Searchkick 6 brings a new query builder API:
2307
+
2308
+ ```ruby
2309
+ Product.search("apples").where(in_stock: true).limit(10).offset(50)
2310
+ ```
2311
+
2312
+ All existing options can be used as methods, or you can continue to use the existing API.
2313
+
2314
+ 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`:
2315
+
2316
+ ```ruby
2317
+ class Product < ApplicationRecord
2318
+ searchkick conversions: [:conversions], conversions_v2: [:conversions_v2]
2319
+
2320
+ def search_data
2321
+ conversions = searches.group(:query).distinct.count(:user_id)
2322
+ {
2323
+ conversions: conversions,
2324
+ conversions_v2: conversions
2325
+ }
2326
+ end
2327
+ end
2328
+ ```
2329
+
2330
+ Reindex, then remove `conversions`:
2331
+
2332
+ ```ruby
2333
+ class Product < ApplicationRecord
2334
+ searchkick conversions_v2: [:conversions_v2]
2335
+
2336
+ def search_data
2337
+ {
2338
+ conversions_v2: searches.group(:query).distinct.count(:user_id)
2339
+ }
2340
+ end
2341
+ end
2342
+ ```
2343
+
2344
+ 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.
2345
+
2237
2346
  ## History
2238
2347
 
2239
- View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
2348
+ View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md)
2240
2349
 
2241
2350
  ## Thanks
2242
2351