searchkick 4.6.3 → 5.2.2

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