searchkick 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -10,7 +10,7 @@ Searchkick handles:
10
10
  - special characters - `jalapeno` matches `jalapeño`
11
11
  - extra whitespace - `dishwasher` matches `dish washer`
12
12
  - misspellings - `zuchini` matches `zucchini`
13
- - custom synonyms - `qtip` matches `cotton swab`
13
+ - custom synonyms - `pop` matches `soda`
14
14
 
15
15
  Plus:
16
16
 
@@ -20,40 +20,53 @@ Plus:
20
20
  - autocomplete
21
21
  - “Did you mean” suggestions
22
22
  - supports many languages
23
- - works with ActiveRecord, Mongoid, and NoBrainer
23
+ - works with Active Record and Mongoid
24
+
25
+ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Autosuggest](https://github.com/ankane/autosuggest) for query suggestions
24
26
 
25
27
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
26
28
 
27
- [![Build Status](https://travis-ci.org/ankane/searchkick.svg?branch=master)](https://travis-ci.org/ankane/searchkick)
29
+ [![Build Status](https://github.com/ankane/searchkick/workflows/build/badge.svg?branch=master)](https://github.com/ankane/searchkick/actions)
28
30
 
29
31
  ## Contents
30
32
 
31
33
  - [Getting Started](#getting-started)
32
34
  - [Querying](#querying)
33
35
  - [Indexing](#indexing)
36
+ - [Intelligent Search](#intelligent-search)
34
37
  - [Instant Search / Autocomplete](#instant-search--autocomplete)
35
38
  - [Aggregations](#aggregations)
39
+ - [Testing](#testing)
36
40
  - [Deployment](#deployment)
37
41
  - [Performance](#performance)
38
- - [Elasticsearch DSL](#advanced)
42
+ - [Advanced Search](#advanced)
39
43
  - [Reference](#reference)
44
+ - [Contributing](#contributing)
45
+
46
+ Searchkick 5.0 was recently released! See [how to upgrade](#upgrading)
40
47
 
41
48
  ## Getting Started
42
49
 
43
- [Install Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html). For Homebrew, use:
50
+ Install [Elasticsearch](https://www.elastic.co/downloads/elasticsearch) or [OpenSearch](https://opensearch.org/downloads.html). For Homebrew, use:
44
51
 
45
52
  ```sh
46
53
  brew install elasticsearch
47
54
  brew services start elasticsearch
55
+ # or
56
+ brew install opensearch
57
+ brew services start opensearch
48
58
  ```
49
59
 
50
- Add this line to your application’s Gemfile:
60
+ Add these lines to your application’s Gemfile:
51
61
 
52
62
  ```ruby
53
- gem 'searchkick'
63
+ gem "searchkick"
64
+
65
+ gem "elasticsearch" # select one
66
+ gem "opensearch-ruby" # select one
54
67
  ```
55
68
 
56
- The latest version works with Elasticsearch 6 and 7. For Elasticsearch 5, use version 3.1.3 and [this readme](https://github.com/ankane/searchkick/blob/v3.1.3/README.md).
69
+ The latest version works with Elasticsearch 7 and 8 and OpenSearch 1. For Elasticsearch 6, use version 4.6.3 and [this readme](https://github.com/ankane/searchkick/blob/v4.6.3/README.md).
57
70
 
58
71
  Add searchkick to models you want to search.
59
72
 
@@ -78,7 +91,7 @@ products.each do |product|
78
91
  end
79
92
  ```
80
93
 
81
- 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.
94
+ 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.
82
95
 
83
96
  ## Querying
84
97
 
@@ -98,14 +111,20 @@ Where
98
111
 
99
112
  ```ruby
100
113
  where: {
101
- expires_at: {gt: Time.now}, # lt, gte, lte also available
102
- orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
103
- aisle_id: [25, 30], # in
104
- store_id: {not: 2}, # not
105
- aisle_id: {not: [25, 30]}, # not in
106
- user_ids: {all: [1, 3]}, # all elements in array
107
- category: /frozen .+/, # regexp
108
- _or: [{in_stock: true}, {backordered: true}]
114
+ expires_at: {gt: Time.now}, # lt, gte, lte also available
115
+ orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
116
+ aisle_id: [25, 30], # in
117
+ store_id: {not: 2}, # not
118
+ aisle_id: {not: [25, 30]}, # not in
119
+ user_ids: {all: [1, 3]}, # all elements in array
120
+ category: {like: "%frozen%"}, # like
121
+ category: {ilike: "%frozen%"}, # ilike
122
+ category: /frozen .+/, # regexp
123
+ category: {prefix: "frozen"}, # prefix
124
+ store_id: {exists: true}, # exists
125
+ _or: [{in_stock: true}, {backordered: true}],
126
+ _and: [{in_stock: true}, {backordered: true}],
127
+ _not: {store_id: 1} # negate a condition
109
128
  }
110
129
  ```
111
130
 
@@ -115,7 +134,7 @@ Order
115
134
  order: {_score: :desc} # most relevant first - default
116
135
  ```
117
136
 
118
- [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html)
137
+ [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
119
138
 
120
139
  Limit / offset
121
140
 
@@ -129,11 +148,11 @@ Select
129
148
  select: [:name]
130
149
  ```
131
150
 
132
- [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html)
151
+ [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering)
133
152
 
134
153
  ### Results
135
154
 
136
- Searches return a `Searchkick::Results` object. This responds like an array to most methods.
155
+ Searches return a `Searchkick::Relation` object. This responds like an array to most methods.
137
156
 
138
157
  ```ruby
139
158
  results = Product.search("milk")
@@ -142,7 +161,7 @@ results.any?
142
161
  results.each { |result| ... }
143
162
  ```
144
163
 
145
- By default, ids are fetched from Elasticsearch and records are fetched from your database. To fetch everything from Elasticsearch, use:
164
+ By default, ids are fetched from the search server and records are fetched from your database. To fetch everything from the search server, use:
146
165
 
147
166
  ```ruby
148
167
  Product.search("apples", load: false)
@@ -160,12 +179,14 @@ Get the time the search took (in milliseconds)
160
179
  results.took
161
180
  ```
162
181
 
163
- Get the full response from Elasticsearch
182
+ Get the full response from the search server
164
183
 
165
184
  ```ruby
166
185
  results.response
167
186
  ```
168
187
 
188
+ **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.
189
+
169
190
  ### Boosting
170
191
 
171
192
  Boost important fields
@@ -197,7 +218,7 @@ boost_by_recency: {created_at: {scale: "7d", decay: 0.5}}
197
218
 
198
219
  You can also boost by:
199
220
 
200
- - [Conversions](#keep-getting-better)
221
+ - [Conversions](#intelligent-search)
201
222
  - [Distance](#boost-by-distance)
202
223
 
203
224
  ### Get Everything
@@ -287,7 +308,9 @@ To only match the exact order, use:
287
308
  User.search "fresh honey", match: :phrase
288
309
  ```
289
310
 
290
- ### Language
311
+ ### Stemming and Language
312
+
313
+ Searchkick stems words by default for better matching. `apple` and `apples` both stem to `appl`, so searches for either term will have the same matches.
291
314
 
292
315
  Searchkick defaults to English for stemming. To change this, use:
293
316
 
@@ -297,44 +320,95 @@ class Product < ApplicationRecord
297
320
  end
298
321
  ```
299
322
 
300
- [See the list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html)
301
-
302
- A few languages require plugins:
323
+ 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:
303
324
 
304
325
  - `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
305
- - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-kuromoji.html)
326
+ - `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-smartcn.html)
327
+ - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-kuromoji.html)
306
328
  - `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
307
- - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-stempel.html)
308
- - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-ukrainian.html)
329
+ - `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-nori.html)
330
+ - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-stempel.html)
331
+ - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
309
332
  - `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
310
333
 
311
- ### Synonyms
334
+ You can also use a Hunspell dictionary for stemming.
312
335
 
313
336
  ```ruby
314
337
  class Product < ApplicationRecord
315
- searchkick synonyms: [["burger", "hamburger"], ["sneakers", "shoes"]]
338
+ searchkick stemmer: {type: "hunspell", locale: "en_US"}
316
339
  end
317
340
  ```
318
341
 
319
- Call `Product.reindex` after changing synonyms.
342
+ Disable stemming with:
320
343
 
321
- Synonyms cannot be multiple words at the moment.
344
+ ```ruby
345
+ class Image < ApplicationRecord
346
+ searchkick stem: false
347
+ end
348
+ ```
349
+
350
+ Exclude certain words from stemming with:
351
+
352
+ ```ruby
353
+ class Image < ApplicationRecord
354
+ searchkick stem_exclusion: ["apples"]
355
+ end
356
+ ```
357
+
358
+ Or change how words are stemmed:
359
+
360
+ ```ruby
361
+ class Image < ApplicationRecord
362
+ searchkick stemmer_override: ["apples => other"]
363
+ end
364
+ ```
322
365
 
323
- To read synonyms from a file, use:
366
+ ### Synonyms
324
367
 
325
368
  ```ruby
326
- synonyms: -> { CSV.read("/some/path/synonyms.csv") }
369
+ class Product < ApplicationRecord
370
+ searchkick search_synonyms: [["pop", "soda"], ["burger", "hamburger"]]
371
+ end
327
372
  ```
328
373
 
374
+ Call `Product.reindex` after changing synonyms. Synonyms are applied at search time before stemming, and can be a single word or multiple words.
375
+
329
376
  For directional synonyms, use:
330
377
 
331
378
  ```ruby
332
- synonyms: ["lightbulb => halogenlamp"]
379
+ search_synonyms: ["lightbulb => halogenlamp"]
333
380
  ```
334
381
 
335
- ### Tags and Dynamic Synonyms
382
+ ### Dynamic Synonyms
336
383
 
337
- 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 or tags without a full reindex. You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
384
+ 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.
385
+
386
+ #### Elasticsearch 7.3+ and OpenSearch
387
+
388
+ 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.
389
+
390
+ ```txt
391
+ pop, soda
392
+ burger, hamburger
393
+ ```
394
+
395
+ Then use:
396
+
397
+ ```ruby
398
+ class Product < ApplicationRecord
399
+ searchkick search_synonyms: "synonyms.txt"
400
+ end
401
+ ```
402
+
403
+ And reload with:
404
+
405
+ ```ruby
406
+ Product.search_index.reload_synonyms
407
+ ```
408
+
409
+ #### Elasticsearch < 7.3
410
+
411
+ You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
338
412
 
339
413
  ```ruby
340
414
  class Product < ApplicationRecord
@@ -419,7 +493,7 @@ Search :ice_cream::cake: and get `ice cream cake`!
419
493
  Add this line to your application’s Gemfile:
420
494
 
421
495
  ```ruby
422
- gem 'gemoji-parser'
496
+ gem "gemoji-parser"
423
497
  ```
424
498
 
425
499
  And use:
@@ -454,12 +528,10 @@ class Product < ApplicationRecord
454
528
  end
455
529
  ```
456
530
 
457
- By default, all records are indexed. To control which records are indexed, use the `should_index?` method together with the `search_import` scope.
531
+ By default, all records are indexed. To control which records are indexed, use the `should_index?` method.
458
532
 
459
533
  ```ruby
460
534
  class Product < ApplicationRecord
461
- scope :search_import, -> { where(active: true) }
462
-
463
535
  def should_index?
464
536
  active # only index active records
465
537
  end
@@ -486,7 +558,7 @@ For large data sets, try [parallel reindexing](#parallel-reindexing).
486
558
 
487
559
  - app starts
488
560
 
489
- ### Stay Synced
561
+ ### Strategies
490
562
 
491
563
  There are four strategies for keeping the index synced with your database.
492
564
 
@@ -536,7 +608,7 @@ Searchkick.callbacks(false) do
536
608
  end
537
609
  ```
538
610
 
539
- #### Associations
611
+ ### Associations
540
612
 
541
613
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
542
614
 
@@ -552,11 +624,31 @@ class Image < ApplicationRecord
552
624
  end
553
625
  ```
554
626
 
555
- ### Analytics
627
+ ### Default Scopes
628
+
629
+ If you have a default scope that filters records, use the `should_index?` method to exclude them from indexing:
630
+
631
+ ```ruby
632
+ class Product < ApplicationRecord
633
+ default_scope { where(deleted_at: nil) }
634
+
635
+ def should_index?
636
+ deleted_at.nil?
637
+ end
638
+ end
639
+ ```
640
+
641
+ If you want to index and search filtered records, set:
556
642
 
557
- The best starting point to improve your search **by far** is to track searches and conversions.
643
+ ```ruby
644
+ class Product < ApplicationRecord
645
+ searchkick unscope: true
646
+ end
647
+ ```
558
648
 
559
- [Searchjoy](https://github.com/ankane/searchjoy) makes it easy.
649
+ ## Intelligent Search
650
+
651
+ 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.
560
652
 
561
653
  ```ruby
562
654
  Product.search "apple", track: {user_id: current_user.id}
@@ -569,15 +661,9 @@ Focus on:
569
661
  - top searches with low conversions
570
662
  - top searches with no results
571
663
 
572
- ### Keep Getting Better
573
-
574
- Searchkick can use 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.
575
-
576
- The first step is to define your conversion metric and start tracking conversions. The database works well for low volume, but feel free to use Redis or another datastore.
664
+ 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.
577
665
 
578
- Searchkick automatically treats `apple` and `APPLE` the same.
579
-
580
- Next, add conversions to the index.
666
+ Add conversion data with:
581
667
 
582
668
  ```ruby
583
669
  class Product < ApplicationRecord
@@ -588,7 +674,7 @@ class Product < ApplicationRecord
588
674
  def search_data
589
675
  {
590
676
  name: name,
591
- conversions: searches.group(:query).uniq.count(:user_id)
677
+ conversions: searches.group(:query).distinct.count(:user_id)
592
678
  # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
593
679
  }
594
680
  end
@@ -601,9 +687,11 @@ Reindex and set up a cron job to add new conversions daily.
601
687
  rake searchkick:reindex CLASS=Product
602
688
  ```
603
689
 
604
- **Note:** For a more performant (but more advanced) approach, check out [performant conversions](#performant-conversions).
690
+ This can make a huge difference on the quality of your search.
691
+
692
+ For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
605
693
 
606
- ### Personalized Results
694
+ ## Personalized Results
607
695
 
608
696
  Order results differently for each user. For example, show a user’s previously purchased products before other results.
609
697
 
@@ -624,13 +712,13 @@ Reindex and search with:
624
712
  Product.search "milk", boost_where: {orderer_ids: current_user.id}
625
713
  ```
626
714
 
627
- ### Instant Search / Autocomplete
715
+ ## Instant Search / Autocomplete
628
716
 
629
717
  Autocomplete predicts what a user will type, making the search experience faster and easier.
630
718
 
631
719
  ![Autocomplete](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/autocomplete.png)
632
720
 
633
- **Note:** To autocomplete on general categories (like `cereal` rather than product names), check out [Autosuggest](https://github.com/ankane/autosuggest).
721
+ **Note:** To autocomplete on search terms rather than results, check out [Autosuggest](https://github.com/ankane/autosuggest).
634
722
 
635
723
  **Note 2:** If you only have a few thousand records, don’t use Searchkick for autocomplete. It’s *much* faster to load all records into JavaScript and autocomplete there (eliminates network requests).
636
724
 
@@ -692,7 +780,7 @@ Then add the search box and JavaScript code to a view.
692
780
  </script>
693
781
  ```
694
782
 
695
- ### Suggestions
783
+ ## Suggestions
696
784
 
697
785
  ![Suggest](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/recursion.png)
698
786
 
@@ -709,7 +797,7 @@ products = Product.search "peantu butta", suggest: true
709
797
  products.suggestions # ["peanut butter"]
710
798
  ```
711
799
 
712
- ### Aggregations
800
+ ## Aggregations
713
801
 
714
802
  [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) provide aggregated search data.
715
803
 
@@ -752,8 +840,6 @@ Order
752
840
  Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
753
841
  ```
754
842
 
755
- **Note:** Use `_term` instead of `_key` in Elasticsearch 5
756
-
757
843
  [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)
758
844
 
759
845
  Ranges
@@ -784,10 +870,10 @@ Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :creat
784
870
  For other aggregation types, including sub-aggregations, use `body_options`:
785
871
 
786
872
  ```ruby
787
- Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}
873
+ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}}
788
874
  ```
789
875
 
790
- ### Highlight
876
+ ## Highlight
791
877
 
792
878
  Specify which fields to index with highlighting.
793
879
 
@@ -838,9 +924,9 @@ Additional options can be specified for each field:
838
924
  Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
839
925
  ```
840
926
 
841
- You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_highlighted_fragments).
927
+ You can find available highlight options in the [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html).
842
928
 
843
- ### Similar Items
929
+ ## Similar Items
844
930
 
845
931
  Find similar items.
846
932
 
@@ -849,7 +935,7 @@ product = Product.first
849
935
  product.similar(fields: [:name], where: {size: "12 oz"})
850
936
  ```
851
937
 
852
- ### Geospatial Searches
938
+ ## Geospatial Searches
853
939
 
854
940
  ```ruby
855
941
  class Restaurant < ApplicationRecord
@@ -889,7 +975,7 @@ Boost results by distance - closer results are boosted more
889
975
  Restaurant.search "noodles", boost_by_distance: {location: {origin: {lat: 37, lon: -122}}}
890
976
  ```
891
977
 
892
- Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#_decay_functions)
978
+ Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay)
893
979
 
894
980
  ```ruby
895
981
  Restaurant.search "wings", boost_by_distance: {location: {origin: {lat: 37, lon: -122}, function: "linear", scale: "30mi", decay: 0.5}}
@@ -985,13 +1071,13 @@ Product.search("soap", debug: true)
985
1071
 
986
1072
  This prints useful info to `stdout`.
987
1073
 
988
- See how Elasticsearch scores your queries with:
1074
+ See how the search server scores your queries with:
989
1075
 
990
1076
  ```ruby
991
1077
  Product.search("soap", explain: true).response
992
1078
  ```
993
1079
 
994
- See how Elasticsearch tokenizes your queries with:
1080
+ See how the search server tokenizes your queries with:
995
1081
 
996
1082
  ```ruby
997
1083
  Product.search_index.tokens("Dish Washer Soap", analyzer: "searchkick_index")
@@ -1016,21 +1102,201 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search")
1016
1102
 
1017
1103
  See the [complete list of analyzers](https://github.com/ankane/searchkick/blob/31780ddac7a89eab1e0552a32b403f2040a37931/lib/searchkick/index_options.rb#L32).
1018
1104
 
1105
+ ## Testing
1106
+
1107
+ As you iterate on your search, it’s a good idea to add tests.
1108
+
1109
+ For performance, only enable Searchkick callbacks for the tests that need it.
1110
+
1111
+ ### Parallel Tests
1112
+
1113
+ Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
1114
+
1115
+ ```ruby
1116
+ class ActiveSupport::TestCase
1117
+ parallelize_setup do |worker|
1118
+ Searchkick.index_suffix = worker
1119
+
1120
+ # reindex models
1121
+ Product.reindex
1122
+
1123
+ # and disable callbacks
1124
+ Searchkick.disable_callbacks
1125
+ end
1126
+ end
1127
+ ```
1128
+
1129
+ And use:
1130
+
1131
+ ```ruby
1132
+ class ProductTest < ActiveSupport::TestCase
1133
+ def setup
1134
+ Searchkick.enable_callbacks
1135
+ end
1136
+
1137
+ def teardown
1138
+ Searchkick.disable_callbacks
1139
+ end
1140
+
1141
+ def test_search
1142
+ Product.create!(name: "Apple")
1143
+ Product.search_index.refresh
1144
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1145
+ end
1146
+ end
1147
+ ```
1148
+
1149
+ ### Minitest
1150
+
1151
+ Add to your `test/test_helper.rb`:
1152
+
1153
+ ```ruby
1154
+ # reindex models
1155
+ Product.reindex
1156
+
1157
+ # and disable callbacks
1158
+ Searchkick.disable_callbacks
1159
+ ```
1160
+
1161
+ And use:
1162
+
1163
+ ```ruby
1164
+ class ProductTest < Minitest::Test
1165
+ def setup
1166
+ Searchkick.enable_callbacks
1167
+ end
1168
+
1169
+ def teardown
1170
+ Searchkick.disable_callbacks
1171
+ end
1172
+
1173
+ def test_search
1174
+ Product.create!(name: "Apple")
1175
+ Product.search_index.refresh
1176
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1177
+ end
1178
+ end
1179
+ ```
1180
+
1181
+ ### RSpec
1182
+
1183
+ Add to your `spec/spec_helper.rb`:
1184
+
1185
+ ```ruby
1186
+ RSpec.configure do |config|
1187
+ config.before(:suite) do
1188
+ # reindex models
1189
+ Product.reindex
1190
+
1191
+ # and disable callbacks
1192
+ Searchkick.disable_callbacks
1193
+ end
1194
+
1195
+ config.around(:each, search: true) do |example|
1196
+ Searchkick.callbacks(nil) do
1197
+ example.run
1198
+ end
1199
+ end
1200
+ end
1201
+ ```
1202
+
1203
+ And use:
1204
+
1205
+ ```ruby
1206
+ describe Product, search: true do
1207
+ it "searches" do
1208
+ Product.create!(name: "Apple")
1209
+ Product.search_index.refresh
1210
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1211
+ end
1212
+ end
1213
+ ```
1214
+
1215
+ ### Factory Bot
1216
+
1217
+ Use a trait and an after `create` hook for each indexed model:
1218
+
1219
+ ```ruby
1220
+ FactoryBot.define do
1221
+ factory :product do
1222
+ # ...
1223
+
1224
+ # Note: This should be the last trait in the list so `reindex` is called
1225
+ # after all the other callbacks complete.
1226
+ trait :reindex do
1227
+ after(:create) do |product, _evaluator|
1228
+ product.reindex(refresh: true)
1229
+ end
1230
+ end
1231
+ end
1232
+ end
1233
+
1234
+ # use it
1235
+ FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1236
+ ```
1237
+
1238
+ ### GitHub Actions
1239
+
1240
+ Check out [setup-elasticsearch](https://github.com/ankane/setup-elasticsearch) for an easy way to install Elasticsearch:
1241
+
1242
+ ```yml
1243
+ - uses: ankane/setup-elasticsearch@v1
1244
+ ```
1245
+
1246
+ And [setup-opensearch](https://github.com/ankane/setup-opensearch) for an easy way to install OpenSearch:
1247
+
1248
+ ```yml
1249
+ - uses: ankane/setup-opensearch@v1
1250
+ ```
1251
+
1019
1252
  ## Deployment
1020
1253
 
1021
- Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
1254
+ For the search server, Searchkick uses `ENV["ELASTICSEARCH_URL"]` for Elasticsearch and `ENV["OPENSEARCH_URL"]` for OpenSearch. This defaults to `http://localhost:9200`.
1255
+
1256
+ - [Elastic Cloud](#elastic-cloud)
1257
+ - [Heroku](#heroku)
1258
+ - [Amazon OpenSearch Service](#amazon-opensearch-service)
1259
+ - [Self-Hosted and Other](#self-hosted-and-other)
1260
+
1261
+ ### Elastic Cloud
1262
+
1263
+ Create an initializer `config/initializers/elasticsearch.rb` with:
1264
+
1265
+ ```ruby
1266
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1267
+ ```
1268
+
1269
+ Then deploy and reindex:
1270
+
1271
+ ```sh
1272
+ rake searchkick:reindex:all
1273
+ ```
1022
1274
 
1023
1275
  ### Heroku
1024
1276
 
1025
- Choose an add-on: [Bonsai](https://elements.heroku.com/addons/bonsai) or [Elastic Cloud](https://elements.heroku.com/addons/foundelasticsearch). [SearchBox](https://elements.heroku.com/addons/searchbox) does not work at the moment.
1277
+ 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).
1026
1278
 
1027
- For Bonsai:
1279
+ For Elasticsearch on Bonsai:
1028
1280
 
1029
1281
  ```sh
1030
1282
  heroku addons:create bonsai
1031
1283
  heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1032
1284
  ```
1033
1285
 
1286
+ For OpenSearch on Bonsai:
1287
+
1288
+ ```sh
1289
+ heroku addons:create bonsai --engine=opensearch
1290
+ heroku config:set OPENSEARCH_URL=`heroku config:get BONSAI_URL`
1291
+ ```
1292
+
1293
+ For SearchBox:
1294
+
1295
+ ```sh
1296
+ heroku addons:create searchbox:starter
1297
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
1298
+ ```
1299
+
1034
1300
  For Elastic Cloud (previously Found):
1035
1301
 
1036
1302
  ```sh
@@ -1044,30 +1310,30 @@ Visit the Shield page and reset your password. You’ll need to add the username
1044
1310
  heroku config:get FOUNDELASTICSEARCH_URL
1045
1311
  ```
1046
1312
 
1047
- And add `elastic:password@` right after `https://`:
1313
+ And add `elastic:password@` right after `https://` and add port `9243` at the end:
1048
1314
 
1049
1315
  ```sh
1050
- heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io
1316
+ heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io:9243
1051
1317
  ```
1052
1318
 
1053
1319
  Then deploy and reindex:
1054
1320
 
1055
1321
  ```sh
1056
- heroku run rake searchkick:reindex CLASS=Product
1322
+ heroku run rake searchkick:reindex:all
1057
1323
  ```
1058
1324
 
1059
- ### Amazon Elasticsearch Service
1325
+ ### Amazon OpenSearch Service
1060
1326
 
1061
- Create an initializer `config/initializers/elasticsearch.rb` with:
1327
+ Create an initializer `config/initializers/opensearch.rb` with:
1062
1328
 
1063
1329
  ```ruby
1064
- ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com"
1330
+ ENV["OPENSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
1065
1331
  ```
1066
1332
 
1067
1333
  To use signed requests, include in your Gemfile:
1068
1334
 
1069
1335
  ```ruby
1070
- gem 'faraday_middleware-aws-sigv4'
1336
+ gem "faraday_middleware-aws-sigv4"
1071
1337
  ```
1072
1338
 
1073
1339
  and add to your initializer:
@@ -1083,28 +1349,30 @@ Searchkick.aws_credentials = {
1083
1349
  Then deploy and reindex:
1084
1350
 
1085
1351
  ```sh
1086
- rake searchkick:reindex CLASS=Product
1352
+ rake searchkick:reindex:all
1087
1353
  ```
1088
1354
 
1089
- ### Other
1355
+ ### Self-Hosted and Other
1090
1356
 
1091
- Create an initializer `config/initializers/elasticsearch.rb` with:
1357
+ Create an initializer with:
1092
1358
 
1093
1359
  ```ruby
1094
- ENV["ELASTICSEARCH_URL"] = "https://user:password@host"
1360
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1361
+ # or
1362
+ ENV["OPENSEARCH_URL"] = "https://user:password@host:port"
1095
1363
  ```
1096
1364
 
1097
1365
  Then deploy and reindex:
1098
1366
 
1099
1367
  ```sh
1100
- rake searchkick:reindex CLASS=Product
1368
+ rake searchkick:reindex:all
1101
1369
  ```
1102
1370
 
1103
1371
  ### Data Protection
1104
1372
 
1105
- 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.
1373
+ 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.
1106
1374
 
1107
- Bonsai, Elastic Cloud, and Amazon Elasticsearch all support encryption at rest and HTTPS.
1375
+ Bonsai, Elastic Cloud, and Amazon OpenSearch Service all support encryption at rest and HTTPS.
1108
1376
 
1109
1377
  ### Automatic Failover
1110
1378
 
@@ -1137,7 +1405,7 @@ See [Production Rails](https://github.com/ankane/production_rails) for other goo
1137
1405
  Significantly increase performance with faster JSON generation. Add [Oj](https://github.com/ohler55/oj) to your Gemfile.
1138
1406
 
1139
1407
  ```ruby
1140
- gem 'oj'
1408
+ gem "oj"
1141
1409
  ```
1142
1410
 
1143
1411
  This speeds up all JSON generation and parsing in your application (automatically!)
@@ -1147,7 +1415,7 @@ This speeds up all JSON generation and parsing in your application (automaticall
1147
1415
  Significantly increase performance with persistent HTTP connections. Add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile and it’ll automatically be used.
1148
1416
 
1149
1417
  ```ruby
1150
- gem 'typhoeus'
1418
+ gem "typhoeus"
1151
1419
  ```
1152
1420
 
1153
1421
  To reduce log noise, create an initializer with:
@@ -1160,7 +1428,7 @@ If you run into issues on Windows, check out [this post](https://www.rastating.c
1160
1428
 
1161
1429
  ### Searchable Fields
1162
1430
 
1163
- By default, all string fields are searchable (can be used in `fields` option). Speed up indexing and reduce index size by only making some fields searchable. This disables the `_all` field unless it’s listed.
1431
+ By default, all string fields are searchable (can be used in `fields` option). Speed up indexing and reduce index size by only making some fields searchable.
1164
1432
 
1165
1433
  ```ruby
1166
1434
  class Product < ApplicationRecord
@@ -1216,7 +1484,7 @@ Product.reindex(async: {wait: true})
1216
1484
  You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
1217
1485
 
1218
1486
  ```ruby
1219
- gem 'activejob-traffic_control', '>= 0.1.3'
1487
+ gem "activejob-traffic_control", ">= 0.1.3"
1220
1488
  ```
1221
1489
 
1222
1490
  And create an initializer with:
@@ -1249,7 +1517,7 @@ Product.search_index.promote(index_name, update_refresh_interval: true)
1249
1517
 
1250
1518
  ### Queuing
1251
1519
 
1252
- Push ids of records needing reindexed to a queue and reindex in bulk for better performance. First, set up Redis in an initializer. We recommend using [connection_pool](https://github.com/mperham/connection_pool).
1520
+ Push ids of records needing reindexing to a queue and reindex in bulk for better performance. First, set up Redis in an initializer. We recommend using [connection_pool](https://github.com/mperham/connection_pool).
1253
1521
 
1254
1522
  ```ruby
1255
1523
  Searchkick.redis = ConnectionPool.new { Redis.new }
@@ -1279,7 +1547,7 @@ For more tips, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/
1279
1547
 
1280
1548
  ### Routing
1281
1549
 
1282
- Searchkick supports [Elasticsearch’s routing feature](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
1550
+ Searchkick supports [routing](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
1283
1551
 
1284
1552
  ```ruby
1285
1553
  class Business < ApplicationRecord
@@ -1352,14 +1620,14 @@ class ReindexConversionsJob < ApplicationJob
1352
1620
  # get records that have a recent conversion
1353
1621
  recently_converted_ids =
1354
1622
  Searchjoy::Search.where("convertable_type = ? AND converted_at > ?", class_name, 1.day.ago)
1355
- .order(:convertable_id).uniq.pluck(:convertable_id)
1623
+ .order(:convertable_id).distinct.pluck(:convertable_id)
1356
1624
 
1357
1625
  # split into groups
1358
1626
  recently_converted_ids.in_groups_of(1000, false) do |ids|
1359
1627
  # fetch conversions
1360
1628
  conversions =
1361
1629
  Searchjoy::Search.where(convertable_id: ids, convertable_type: class_name)
1362
- .group(:convertable_id, :query).uniq.count(:user_id)
1630
+ .group(:convertable_id, :query).distinct.count(:user_id)
1363
1631
 
1364
1632
  # group conversions by record
1365
1633
  conversions_by_record = {}
@@ -1387,7 +1655,7 @@ ReindexConversionsJob.perform_later("Product")
1387
1655
 
1388
1656
  ## Advanced
1389
1657
 
1390
- Searchkick makes it easy to use the Elasticsearch DSL on its own.
1658
+ Searchkick makes it easy to use the Elasticsearch or OpenSearch DSL on its own.
1391
1659
 
1392
1660
  ### Advanced Mapping
1393
1661
 
@@ -1396,10 +1664,8 @@ Create a custom mapping:
1396
1664
  ```ruby
1397
1665
  class Product < ApplicationRecord
1398
1666
  searchkick mappings: {
1399
- product: {
1400
- properties: {
1401
- name: {type: "keyword"}
1402
- }
1667
+ properties: {
1668
+ name: {type: "keyword"}
1403
1669
  }
1404
1670
  }
1405
1671
  end
@@ -1443,7 +1709,7 @@ products =
1443
1709
  end
1444
1710
  ```
1445
1711
 
1446
- ### Elasticsearch Gem
1712
+ ### Client
1447
1713
 
1448
1714
  Searchkick is built on top of the [elasticsearch](https://github.com/elastic/elasticsearch-ruby) gem. To access the client directly, use:
1449
1715
 
@@ -1456,8 +1722,8 @@ Searchkick.client
1456
1722
  To batch search requests for performance, use:
1457
1723
 
1458
1724
  ```ruby
1459
- products = Product.search("snacks", execute: false)
1460
- coupons = Coupon.search("snacks", execute: false)
1725
+ products = Product.search("snacks")
1726
+ coupons = Coupon.search("snacks")
1461
1727
  Searchkick.multi_search([products, coupons])
1462
1728
  ```
1463
1729
 
@@ -1479,6 +1745,49 @@ Boost specific models with:
1479
1745
  indices_boost: {Category => 2, Product => 1}
1480
1746
  ```
1481
1747
 
1748
+ ## Multi-Tenancy
1749
+
1750
+ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenancy-with-searchkick/) on the [Apartment](https://github.com/influitive/apartment) gem. Follow a similar pattern if you use another gem.
1751
+
1752
+ ## Scroll API
1753
+
1754
+ 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.
1755
+
1756
+ ```ruby
1757
+ Product.search("*", scroll: "1m").scroll do |batch|
1758
+ # process batch ...
1759
+ end
1760
+ ```
1761
+
1762
+ You can also scroll batches manually.
1763
+
1764
+ ```ruby
1765
+ products = Product.search "*", scroll: "1m"
1766
+ while products.any?
1767
+ # process batch ...
1768
+
1769
+ products = products.scroll
1770
+ end
1771
+
1772
+ products.clear_scroll
1773
+ ```
1774
+
1775
+ ## Deep Paging
1776
+
1777
+ 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:
1778
+
1779
+ ```ruby
1780
+ class Product < ApplicationRecord
1781
+ searchkick deep_paging: true
1782
+ end
1783
+ ```
1784
+
1785
+ If you just need an accurate total count, you can instead use:
1786
+
1787
+ ```ruby
1788
+ Product.search("pears", body_options: {track_total_hits: true})
1789
+ ```
1790
+
1482
1791
  ## Nested Data
1483
1792
 
1484
1793
  To query nested data, use dot notation.
@@ -1570,7 +1879,7 @@ class Product < ApplicationRecord
1570
1879
  def search_data
1571
1880
  {
1572
1881
  name: name,
1573
- unique_user_conversions: searches.group(:query).uniq.count(:user_id),
1882
+ unique_user_conversions: searches.group(:query).distinct.count(:user_id),
1574
1883
  # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
1575
1884
  total_conversions: searches.group(:query).count
1576
1885
  # {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
@@ -1646,14 +1955,6 @@ class Product < ApplicationRecord
1646
1955
  end
1647
1956
  ```
1648
1957
 
1649
- Turn off stemming
1650
-
1651
- ```ruby
1652
- class Product < ApplicationRecord
1653
- searchkick stem: false
1654
- end
1655
- ```
1656
-
1657
1958
  Turn on stemming for conversions
1658
1959
 
1659
1960
  ```ruby
@@ -1662,14 +1963,6 @@ class Product < ApplicationRecord
1662
1963
  end
1663
1964
  ```
1664
1965
 
1665
- Use a different [similarity algorithm](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html) for scoring
1666
-
1667
- ```ruby
1668
- class Product < ApplicationRecord
1669
- searchkick similarity: "classic"
1670
- end
1671
- ```
1672
-
1673
1966
  Make search case-sensitive
1674
1967
 
1675
1968
  ```ruby
@@ -1704,14 +1997,7 @@ class Product < ApplicationRecord
1704
1997
  end
1705
1998
  ```
1706
1999
 
1707
- Lazy searching
1708
-
1709
- ```ruby
1710
- products = Product.search("carrots", execute: false)
1711
- products.each { ... } # search not executed until here
1712
- ```
1713
-
1714
- Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html), like `search_type` and `query_cache`
2000
+ Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-search-api-query-params) like `search_type`
1715
2001
 
1716
2002
  ```ruby
1717
2003
  Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
@@ -1748,170 +2034,62 @@ Turn on misspellings after a certain number of characters
1748
2034
  Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
1749
2035
  ```
1750
2036
 
1751
- **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off
2037
+ **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off with Elasticsearch 7 and OpenSearch
1752
2038
 
1753
2039
  ```ruby
1754
2040
  Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1755
2041
  ```
1756
2042
 
1757
- ## Testing
1758
-
1759
- For performance, only enable Searchkick callbacks for the tests that need it.
1760
-
1761
- ### Minitest
1762
-
1763
- Add to your `test/test_helper.rb`:
1764
-
1765
- ```ruby
1766
- # reindex models
1767
- Product.reindex
1768
-
1769
- # and disable callbacks
1770
- Searchkick.disable_callbacks
1771
- ```
1772
-
1773
- And use:
1774
-
1775
- ```ruby
1776
- class ProductTest < Minitest::Test
1777
- def setup
1778
- Searchkick.enable_callbacks
1779
- end
1780
-
1781
- def teardown
1782
- Searchkick.disable_callbacks
1783
- end
2043
+ ## Gotchas
1784
2044
 
1785
- def test_search
1786
- Product.create!(name: "Apple")
1787
- Product.search_index.refresh
1788
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1789
- end
1790
- end
1791
- ```
1792
-
1793
- ### RSpec
1794
-
1795
- Add to your `spec/spec_helper.rb`:
1796
-
1797
- ```ruby
1798
- RSpec.configure do |config|
1799
- config.before(:suite) do
1800
- # reindex models
1801
- Product.reindex
1802
-
1803
- # and disable callbacks
1804
- Searchkick.disable_callbacks
1805
- end
1806
-
1807
- config.around(:each, search: true) do |example|
1808
- Searchkick.callbacks(true) do
1809
- example.run
1810
- end
1811
- end
1812
- end
1813
- ```
2045
+ ### Consistency
1814
2046
 
1815
- And use:
2047
+ 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.
1816
2048
 
1817
2049
  ```ruby
1818
- describe Product, search: true do
1819
- it "searches" do
1820
- Product.create!(name: "Apple")
1821
- Product.search_index.refresh
1822
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1823
- end
1824
- end
2050
+ product.save!
2051
+ Product.search_index.refresh
1825
2052
  ```
1826
2053
 
1827
- ### Factory Bot
2054
+ ### Inconsistent Scores
1828
2055
 
1829
- Use a trait and an after `create` hook for each indexed model:
2056
+ 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:
1830
2057
 
1831
2058
  ```ruby
1832
- FactoryBot.define do
1833
- factory :product do
1834
- # ...
1835
-
1836
- # Note: This should be the last trait in the list so `reindex` is called
1837
- # after all the other callbacks complete.
1838
- trait :reindex do
1839
- after(:create) do |product, _evaluator|
1840
- product.reindex(refresh: true)
1841
- end
1842
- end
1843
- end
2059
+ class Product < ApplicationRecord
2060
+ searchkick settings: {number_of_shards: 1}
1844
2061
  end
1845
-
1846
- # use it
1847
- FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1848
2062
  ```
1849
2063
 
1850
- ### Parallel Tests
1851
-
1852
- Set:
1853
-
1854
- ```ruby
1855
- Searchkick.index_suffix = ENV["TEST_ENV_NUMBER"]
1856
- ```
1857
-
1858
- ## Multi-Tenancy
1859
-
1860
- Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenancy-with-searchkick/) on the [Apartment](https://github.com/influitive/apartment) gem. Follow a similar pattern if you use another gem.
2064
+ For convenience, this is set by default in the test environment.
1861
2065
 
1862
2066
  ## Upgrading
1863
2067
 
1864
- See [how to upgrade to Searchkick 3](docs/Searchkick-3-Upgrade.md)
1865
-
1866
- ## Elasticsearch 6 to 7 Upgrade
2068
+ ### 5.0
1867
2069
 
1868
- 1. Install Searchkick 4
1869
- 2. Upgrade your Elasticsearch cluster
1870
-
1871
- ## Elasticsearch 5 to 6 Upgrade
1872
-
1873
- Elasticsearch 6 removes the ability to reindex with the `_all` field. Before you upgrade, we recommend disabling this field manually and specifying default fields on your models.
2070
+ Searchkick 5 supports both the `elasticsearch` and `opensearch-ruby` gems. Add the one you want to use to your Gemfile:
1874
2071
 
1875
2072
  ```ruby
1876
- class Product < ApplicationRecord
1877
- searchkick _all: false, default_fields: [:name]
1878
- end
2073
+ gem "elasticsearch"
2074
+ # or
2075
+ gem "opensearch-ruby"
1879
2076
  ```
1880
2077
 
1881
- If you need search across multiple fields, we recommend creating a similar field in your search data.
2078
+ If using the deprecated `faraday_middleware-aws-signers-v4` gem, switch to `faraday_middleware-aws-sigv4`.
1882
2079
 
1883
- ```ruby
1884
- class Product < ApplicationRecord
1885
- def search_data
1886
- {
1887
- all: [name, size, quantity].join(" ")
1888
- }
1889
- end
1890
- end
1891
- ```
1892
-
1893
- ## Elasticsearch Gotchas
1894
-
1895
- ### Consistency
1896
-
1897
- 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.
2080
+ Also, searches now use lazy loading:
1898
2081
 
1899
2082
  ```ruby
1900
- product.save!
1901
- Product.search_index.refresh
1902
- ```
1903
-
1904
- ### Inconsistent Scores
2083
+ # search not executed
2084
+ Product.search("milk")
1905
2085
 
1906
- 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:
1907
-
1908
- ```ruby
1909
- class Product < ApplicationRecord
1910
- searchkick settings: {number_of_shards: 1}
1911
- end
2086
+ # search executed
2087
+ Product.search("milk").to_a
1912
2088
  ```
1913
2089
 
1914
- For convenience, this is set by default in the test environment.
2090
+ And there’s a [new option](#default-scopes) for models with default scopes.
2091
+
2092
+ Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md) for the full list of changes.
1915
2093
 
1916
2094
  ## History
1917
2095
 
@@ -1930,13 +2108,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
1930
2108
  - Write, clarify, or fix documentation
1931
2109
  - Suggest or add new features
1932
2110
 
1933
- If you’re looking for ideas, [try here](https://github.com/ankane/searchkick/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
1934
-
1935
- To get started with development and testing:
2111
+ To get started with development:
1936
2112
 
1937
2113
  ```sh
1938
2114
  git clone https://github.com/ankane/searchkick.git
1939
2115
  cd searchkick
1940
2116
  bundle install
1941
- rake test
2117
+ bundle exec rake test
1942
2118
  ```
2119
+
2120
+ Feel free to open an issue to get feedback on your idea before spending too much time on it.