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.
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,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", 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: {
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: {_score: :desc} # most relevant first - default
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: 20, offset: 40
139
+ limit(20).offset(40)
141
140
  ```
142
141
 
143
142
  Select
144
143
 
145
144
  ```ruby
146
- select: [:name]
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", load: false)
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: ["title^10", "description"]
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: [:orders_count] # give popular documents a little boost
200
- boost_by: {orders_count: {factor: 10}} # default factor is 1
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: {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}]}
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: {created_at: {scale: "7d", decay: 0.5}}
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", page: params[:page], per_page: 20)
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", operator: "or") # fresh OR 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", fields: [:name], match: :word_start)
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, fields: [{name: :word_start}, {brand: :word_middle}])
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, fields: [{name: :exact}])
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", match: :phrase)
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", misspellings: {edit_distance: 2}) # zucchini
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", misspellings: {below: 5})
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", misspellings: false) # no zucchini
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", fields: [:name, :color], misspellings: {fields: [:name]})
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", exclude: ["peanut 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, exclude: exclude_queries[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", boost_where: {category: {value: "pantry", factor: 0.5}})
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("🍨🍰", emoji: true)
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", track: {user_id: current_user.id})
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 conversions: [:conversions] # name of field
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 `conversions: false` in your search calls until the data is reindexed.
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 conversions: [:conversions]
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 `conversions: false` in your search calls until the data is reindexed.
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).where(created_at: since..)
747
- .order(:convertable_id).distinct.pluck(:convertable_id)
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
- .joins(:search).where.not(searchjoy_searches: {user_id: nil})
756
- .group(:convertable_id, :query).distinct.count(:user_id)
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", boost_where: {orderer_ids: current_user.id})
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", fields: [:title], match: :word_start)
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
- params[:query],
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: false` and `misspellings: {below: n}` (or `misspellings: false`) for best performance.
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", suggest: true)
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
  ![Aggregations](https://gist.githubusercontent.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/facets.png)
905
871
 
906
872
  ```ruby
907
- products = Product.search("chuck taylor", aggs: [:product_type, :gender, :brand])
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", where: {color: "brandy"}, aggs: [:size])
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", where: {color: "brandy"}, aggs: [:size], smart_aggs: false)
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", aggs: {size: {where: {color: "brandy"}}})
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", aggs: {store_id: {limit: 10}})
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", aggs: {color: {order: {"_key" => "asc"}}}) # alphabetically
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("*", aggs: {price: {ranges: price_ranges}})
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", aggs: {store_id: {min_doc_count: 2}})
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("*", aggs: {color: {script: {source: "'Color: ' + _value"}}})
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", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}})
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", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}})
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", highlight: true)
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", highlight: {tag: "<strong>"})
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", fields: [:name], highlight: {fields: [:description]})
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", highlight: {fragment_size: 20})
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", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}})
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(fields: [:name], where: {size: "12 oz"})
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", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}}) # or 160km
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", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}})
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", where: {location: {geo_polygon: {points: [{lat: 38, lon: -123}, {lat: 39, lon: -123}, {lat: 37, lon: 122}]}}})
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", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}})
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", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}})
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", where: {bounds: {geo_shape: {type: "polygon", coordinates: [[{lat: 38, lon: -123}, ...]]}}})
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", where: {bounds: {geo_shape: {type: "circle", relation: "within", coordinates: {lat: 38, lon: -123}, radius: "1km"}}})
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", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}})
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("*") # all animals
1151
- Dog.search("*") # just dogs
1152
- Animal.search("*", type: [Dog, Cat]) # just cats and dogs
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", suggest: true) # suggestions for all animals
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", debug: true)
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", explain: true).response
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
- ### Parallel Tests
1177
+ ### Rails
1212
1178
 
1213
- Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
1179
+ Add to your `test/test_helper.rb`:
1214
1180
 
1215
1181
  ```ruby
1216
- class ActiveSupport::TestCase
1217
- parallelize_setup do |worker|
1218
- Searchkick.index_suffix = worker
1182
+ module ActiveSupport
1183
+ class TestCase
1184
+ parallelize_setup do |worker|
1185
+ Searchkick.index_suffix = worker
1219
1186
 
1220
- # reindex models
1221
- Product.reindex
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
- def setup
1204
+ setup do
1234
1205
  Searchkick.enable_callbacks
1235
1206
  end
1236
1207
 
1237
- def teardown
1208
+ teardown do
1238
1209
  Searchkick.disable_callbacks
1239
1210
  end
1240
1211
 
1241
- def test_search
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
- Use a trait and an after `create` hook for each indexed model:
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, _evaluator|
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
- # use it
1335
- FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
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", routing: params[:city_id])
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(body: {query: {match: {name: "milk"}}})
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", body_options: {min_score: 1})
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", models: [Product, Category])
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: {Category => 2, Product => 1}
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("*", scroll: "1m").scroll do |batch|
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("*", scroll: "1m")
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", body_options: {track_total_hits: true})
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", fields: ["store.city"], where: {"store.zip_code" => 12345})
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(knn: {field: :embedding, vector: [1, 2, 3]}, limit: 10)
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(knn: {field: :embedding, vector: [1, 2, 3], ef_search: 40}, limit: 10)
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(knn: {field: :embedding, vector: query_embedding}, limit: 20)
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, limit: 20)
1938
- semantic_search = Product.search(knn: {field: :embedding, vector: query_embedding}, limit: 20)
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", conversions_term: "organic banana")
2002
+ Product.search("banana").conversions_v2(term: "organic banana")
2027
2003
  ```
2028
2004
 
2029
- Multiple conversion fields
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
- # searchkick also supports multiple "conversions" fields
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
- unique_user_conversions: searches.group(:query).distinct.count(:user_id),
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
- and during query time:
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", conversions: "total_conversions") # only boost by total_conversions
2055
- Product.search("banana", conversions: false) # no conversion boosting
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 search queue name
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.queue_name = :search_reindex
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", includes: [:brand, :stores])
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("*", models: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
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", scope_results: ->(r) { r.with_attached_images })
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", request_params: {search_type: "dfs_query_then_fetch"})
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 callbacks: false
2178
+ searchkick callback_options: {if: :search_data_changed?}
2178
2179
 
2179
- # add the callbacks manually
2180
- after_commit :reindex, if: -> (model) { model.previous_changes.key?("name") } # use your own condition
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", 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
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