searchkick 3.1.2 → 4.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2018 Andrew Kane
1
+ Copyright (c) 2013-2020 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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
 
@@ -22,33 +22,30 @@ Plus:
22
22
  - supports many languages
23
23
  - works with ActiveRecord, Mongoid, and NoBrainer
24
24
 
25
- :speech_balloon: Get [handcrafted updates](https://chartkick.us7.list-manage.com/subscribe?u=952c861f99eb43084e0a49f98&id=6ea6541e8e&group[0][4]=true) for new features
25
+ Check out [Searchjoy](https://github.com/ankane/searchjoy) for analytics and [Autosuggest](https://github.com/ankane/autosuggest) for query suggestions
26
26
 
27
27
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
28
28
 
29
- [![Build Status](https://travis-ci.org/ankane/searchkick.svg?branch=master)](https://travis-ci.org/ankane/searchkick)
30
-
31
- ---
32
-
33
- Does your company use Searchkick? Want free advising? Fill out [this application](https://goo.gl/forms/Or1HQTRb2rgQCNtd2)
34
-
35
- ---
29
+ [![Build Status](https://github.com/ankane/searchkick/workflows/build/badge.svg?branch=master)](https://github.com/ankane/searchkick/actions)
36
30
 
37
31
  ## Contents
38
32
 
39
33
  - [Getting Started](#getting-started)
40
34
  - [Querying](#querying)
41
35
  - [Indexing](#indexing)
36
+ - [Intelligent Search](#intelligent-search)
42
37
  - [Instant Search / Autocomplete](#instant-search--autocomplete)
43
38
  - [Aggregations](#aggregations)
39
+ - [Testing](#testing)
44
40
  - [Deployment](#deployment)
45
41
  - [Performance](#performance)
46
42
  - [Elasticsearch DSL](#advanced)
47
43
  - [Reference](#reference)
44
+ - [Contributing](#contributing)
48
45
 
49
46
  ## Getting Started
50
47
 
51
- [Install Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html). For Homebrew, use:
48
+ [Install Elasticsearch](https://www.elastic.co/downloads/elasticsearch). For Homebrew, use:
52
49
 
53
50
  ```sh
54
51
  brew install elasticsearch
@@ -61,7 +58,7 @@ Add this line to your application’s Gemfile:
61
58
  gem 'searchkick'
62
59
  ```
63
60
 
64
- The latest version works with Elasticsearch 5 and 6. For Elasticsearch 2, use version 2.5.0 and [this readme](https://github.com/ankane/searchkick/blob/v2.5.0/README.md).
61
+ 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).
65
62
 
66
63
  Add searchkick to models you want to search.
67
64
 
@@ -106,14 +103,19 @@ Where
106
103
 
107
104
  ```ruby
108
105
  where: {
109
- expires_at: {gt: Time.now}, # lt, gte, lte also available
110
- orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
111
- aisle_id: [25, 30], # in
112
- store_id: {not: 2}, # not
113
- aisle_id: {not: [25, 30]}, # not in
114
- user_ids: {all: [1, 3]}, # all elements in array
115
- category: /frozen .+/, # regexp
116
- _or: [{in_stock: true}, {backordered: true}]
106
+ expires_at: {gt: Time.now}, # lt, gte, lte also available
107
+ orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
108
+ aisle_id: [25, 30], # in
109
+ store_id: {not: 2}, # not
110
+ aisle_id: {not: [25, 30]}, # not in
111
+ user_ids: {all: [1, 3]}, # all elements in array
112
+ category: {like: "%frozen%"}, # like
113
+ category: /frozen .+/, # regexp
114
+ category: {prefix: "frozen"}, # prefix
115
+ store_id: {exists: true}, # exists
116
+ _or: [{in_stock: true}, {backordered: true}],
117
+ _and: [{in_stock: true}, {backordered: true}],
118
+ _not: {store_id: 1} # negate a condition
117
119
  }
118
120
  ```
119
121
 
@@ -137,7 +139,7 @@ Select
137
139
  select: [:name]
138
140
  ```
139
141
 
140
- [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html)
142
+ [These source filtering options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-source-filtering)
141
143
 
142
144
  ### Results
143
145
 
@@ -174,6 +176,8 @@ Get the full response from Elasticsearch
174
176
  results.response
175
177
  ```
176
178
 
179
+ **Note:** By default, Elasticsearch [limits paging](#deep-paging-master) to the first 10,000 results for performance. With Elasticsearch 7, this applies to the total count as well.
180
+
177
181
  ### Boosting
178
182
 
179
183
  Boost important fields
@@ -295,7 +299,9 @@ To only match the exact order, use:
295
299
  User.search "fresh honey", match: :phrase
296
300
  ```
297
301
 
298
- ### Language
302
+ ### Stemming and Language
303
+
304
+ 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.
299
305
 
300
306
  Searchkick defaults to English for stemming. To change this, use:
301
307
 
@@ -305,44 +311,91 @@ class Product < ApplicationRecord
305
311
  end
306
312
  ```
307
313
 
308
- [See the list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html)
309
-
310
- A few languages require plugins:
314
+ See the [list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html). A few languages require plugins:
311
315
 
312
316
  - `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
313
- - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-kuromoji.html)
317
+ - `chinese2` - [analysis-smartcn plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-smartcn.html)
318
+ - `japanese` - [analysis-kuromoji plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-kuromoji.html)
314
319
  - `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
315
- - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-stempel.html)
316
- - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/6.2/analysis-ukrainian.html)
320
+ - `korean2` - [analysis-nori plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-nori.html)
321
+ - `polish` - [analysis-stempel plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-stempel.html)
322
+ - `ukrainian` - [analysis-ukrainian plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/7.4/analysis-ukrainian.html)
317
323
  - `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
318
324
 
325
+ Disable stemming with:
326
+
327
+ ```ruby
328
+ class Image < ApplicationRecord
329
+ searchkick stem: false
330
+ end
331
+ ```
332
+
333
+ Exclude certain words from stemming with:
334
+
335
+ ```ruby
336
+ class Image < ApplicationRecord
337
+ searchkick stem_exclusion: ["apples"]
338
+ end
339
+ ```
340
+
341
+ Or change how words are stemmed:
342
+
343
+ ```ruby
344
+ class Image < ApplicationRecord
345
+ searchkick stemmer_override: ["apples => other"]
346
+ end
347
+ ```
348
+
319
349
  ### Synonyms
320
350
 
321
351
  ```ruby
322
352
  class Product < ApplicationRecord
323
- searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]]
353
+ searchkick search_synonyms: [["pop", "soda"], ["burger", "hamburger"]]
324
354
  end
325
355
  ```
326
356
 
327
- Call `Product.reindex` after changing synonyms.
357
+ Call `Product.reindex` after changing synonyms. Synonyms are applied at search time before stemming, and can be a single word or multiple words.
358
+
359
+ For directional synonyms, use:
360
+
361
+ ```ruby
362
+ search_synonyms: ["lightbulb => halogenlamp"]
363
+ ```
364
+
365
+ ### Dynamic Synonyms
366
+
367
+ 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.
368
+
369
+ #### Elasticsearch 7.3+
328
370
 
329
- Synonyms cannot be more than two words at the moment.
371
+ For Elasticsearch 7.3+, we recommend placing synonyms in a file on the Elasticsearch server (in the `config` directory). This allows you to reload synonyms without reindexing.
330
372
 
331
- To read synonyms from a file, use:
373
+ ```txt
374
+ pop, soda
375
+ burger, hamburger
376
+ ```
377
+
378
+ Then use:
332
379
 
333
380
  ```ruby
334
- synonyms: -> { CSV.read("/some/path/synonyms.csv") }
381
+ search_synonyms: "synonyms.txt"
335
382
  ```
336
383
 
337
- For directional synonyms, use:
384
+ Add [elasticsearch-xpack](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-xpack) to your Gemfile:
338
385
 
339
386
  ```ruby
340
- synonyms: ["lightbulb => halogenlamp"]
387
+ gem 'elasticsearch-xpack', '>= 7.8.0'
341
388
  ```
342
389
 
343
- ### Tags and Dynamic Synonyms
390
+ And use:
391
+
392
+ ```ruby
393
+ Product.search_index.reload_synonyms
394
+ ```
395
+
396
+ #### Elasticsearch < 7.3
344
397
 
345
- 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:
398
+ You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
346
399
 
347
400
  ```ruby
348
401
  class Product < ApplicationRecord
@@ -363,27 +416,6 @@ Search with:
363
416
  Product.search query, fields: [:name_tagged]
364
417
  ```
365
418
 
366
- ### WordNet
367
-
368
- Prepopulate English synonyms with the [WordNet database](https://en.wikipedia.org/wiki/WordNet).
369
-
370
- Download [WordNet 3.0](http://wordnetcode.princeton.edu/3.0/WNprolog-3.0.tar.gz) to each Elasticsearch server and move `wn_s.pl` to the `/var/lib` directory.
371
-
372
- ```sh
373
- cd /tmp
374
- curl -o wordnet.tar.gz http://wordnetcode.princeton.edu/3.0/WNprolog-3.0.tar.gz
375
- tar -zxvf wordnet.tar.gz
376
- mv prolog/wn_s.pl /var/lib
377
- ```
378
-
379
- Tell each model to use it:
380
-
381
- ```ruby
382
- class Product < ApplicationRecord
383
- searchkick wordnet: true
384
- end
385
- ```
386
-
387
419
  ### Misspellings
388
420
 
389
421
  By default, Searchkick handles misspelled queries by returning results with an [edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance) of one.
@@ -515,7 +547,7 @@ For large data sets, try [parallel reindexing](#parallel-reindexing).
515
547
 
516
548
  - app starts
517
549
 
518
- ### Stay Synced
550
+ ### Strategies
519
551
 
520
552
  There are four strategies for keeping the index synced with your database.
521
553
 
@@ -565,7 +597,7 @@ Searchkick.callbacks(false) do
565
597
  end
566
598
  ```
567
599
 
568
- #### Associations
600
+ ### Associations
569
601
 
570
602
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
571
603
 
@@ -581,11 +613,9 @@ class Image < ApplicationRecord
581
613
  end
582
614
  ```
583
615
 
584
- ### Analytics
585
-
586
- The best starting point to improve your search **by far** is to track searches and conversions.
616
+ ## Intelligent Search
587
617
 
588
- [Searchjoy](https://github.com/ankane/searchjoy) makes it easy.
618
+ 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.
589
619
 
590
620
  ```ruby
591
621
  Product.search "apple", track: {user_id: current_user.id}
@@ -598,15 +628,9 @@ Focus on:
598
628
  - top searches with low conversions
599
629
  - top searches with no results
600
630
 
601
- ### Keep Getting Better
631
+ 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.
602
632
 
603
- 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.
604
-
605
- 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.
606
-
607
- Searchkick automatically treats `apple` and `APPLE` the same.
608
-
609
- Next, add conversions to the index.
633
+ Add conversion data with:
610
634
 
611
635
  ```ruby
612
636
  class Product < ApplicationRecord
@@ -630,9 +654,11 @@ Reindex and set up a cron job to add new conversions daily.
630
654
  rake searchkick:reindex CLASS=Product
631
655
  ```
632
656
 
633
- **Note:** For a more performant (but more advanced) approach, check out [performant conversions](#performant-conversions).
657
+ This can make a huge difference on the quality of your search.
634
658
 
635
- ### Personalized Results
659
+ For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
660
+
661
+ ## Personalized Results
636
662
 
637
663
  Order results differently for each user. For example, show a user’s previously purchased products before other results.
638
664
 
@@ -653,13 +679,13 @@ Reindex and search with:
653
679
  Product.search "milk", boost_where: {orderer_ids: current_user.id}
654
680
  ```
655
681
 
656
- ### Instant Search / Autocomplete
682
+ ## Instant Search / Autocomplete
657
683
 
658
684
  Autocomplete predicts what a user will type, making the search experience faster and easier.
659
685
 
660
686
  ![Autocomplete](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/autocomplete.png)
661
687
 
662
- **Note:** To autocomplete on general categories (like `cereal` rather than product names), check out [Autosuggest](https://github.com/ankane/autosuggest).
688
+ **Note:** To autocomplete on search terms rather than results, check out [Autosuggest](https://github.com/ankane/autosuggest).
663
689
 
664
690
  **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).
665
691
 
@@ -721,7 +747,7 @@ Then add the search box and JavaScript code to a view.
721
747
  </script>
722
748
  ```
723
749
 
724
- ### Suggestions
750
+ ## Suggestions
725
751
 
726
752
  ![Suggest](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/recursion.png)
727
753
 
@@ -738,7 +764,7 @@ products = Product.search "peantu butta", suggest: true
738
764
  products.suggestions # ["peanut butter"]
739
765
  ```
740
766
 
741
- ### Aggregations
767
+ ## Aggregations
742
768
 
743
769
  [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) provide aggregated search data.
744
770
 
@@ -781,8 +807,6 @@ Order
781
807
  Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
782
808
  ```
783
809
 
784
- **Note:** Use `_term` instead of `_key` in Elasticsearch 5
785
-
786
810
  [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)
787
811
 
788
812
  Ranges
@@ -804,8 +828,6 @@ Script support
804
828
  Product.search "*", aggs: {color: {script: {source: "'Color: ' + _value"}}}
805
829
  ```
806
830
 
807
- **Note:** Use `inline` instead of `source` before Elasticsearch 5.6
808
-
809
831
  Date histogram
810
832
 
811
833
  ```ruby
@@ -815,10 +837,10 @@ Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :creat
815
837
  For other aggregation types, including sub-aggregations, use `body_options`:
816
838
 
817
839
  ```ruby
818
- Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}
840
+ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}}
819
841
  ```
820
842
 
821
- ### Highlight
843
+ ## Highlight
822
844
 
823
845
  Specify which fields to index with highlighting.
824
846
 
@@ -871,7 +893,7 @@ Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size
871
893
 
872
894
  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).
873
895
 
874
- ### Similar Items
896
+ ## Similar Items
875
897
 
876
898
  Find similar items.
877
899
 
@@ -880,7 +902,7 @@ product = Product.first
880
902
  product.similar(fields: [:name], where: {size: "12 oz"})
881
903
  ```
882
904
 
883
- ### Geospatial Searches
905
+ ## Geospatial Searches
884
906
 
885
907
  ```ruby
886
908
  class Restaurant < ApplicationRecord
@@ -932,9 +954,7 @@ You can also index and search geo shapes.
932
954
 
933
955
  ```ruby
934
956
  class Restaurant < ApplicationRecord
935
- searchkick geo_shape: {
936
- bounds: {tree: "geohash", precision: "1km"}
937
- }
957
+ searchkick geo_shape: [:bounds]
938
958
 
939
959
  def search_data
940
960
  attributes.merge(
@@ -967,12 +987,6 @@ Not touching the query shape
967
987
  Restaurant.search "burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
968
988
  ```
969
989
 
970
- Containing the query shape
971
-
972
- ```ruby
973
- Restaurant.search "fries", where: {bounds: {geo_shape: {type: "envelope", relation: "contains", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
974
- ```
975
-
976
990
  ## Inheritance
977
991
 
978
992
  Searchkick supports single table inheritance.
@@ -1055,13 +1069,173 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search")
1055
1069
 
1056
1070
  See the [complete list of analyzers](https://github.com/ankane/searchkick/blob/31780ddac7a89eab1e0552a32b403f2040a37931/lib/searchkick/index_options.rb#L32).
1057
1071
 
1072
+ ## Testing
1073
+
1074
+ As you iterate on your search, it’s a good idea to add tests.
1075
+
1076
+ For performance, only enable Searchkick callbacks for the tests that need it.
1077
+
1078
+ ### Parallel Tests
1079
+
1080
+ Rails 6 enables parallel tests by default. Add to your `test/test_helper.rb`:
1081
+
1082
+ ```ruby
1083
+ class ActiveSupport::TestCase
1084
+ parallelize_setup do |worker|
1085
+ Searchkick.index_suffix = worker
1086
+
1087
+ # reindex models
1088
+ Product.reindex
1089
+
1090
+ # and disable callbacks
1091
+ Searchkick.disable_callbacks
1092
+ end
1093
+ end
1094
+ ```
1095
+
1096
+ And use:
1097
+
1098
+ ```ruby
1099
+ class ProductTest < ActiveSupport::TestCase
1100
+ def setup
1101
+ Searchkick.enable_callbacks
1102
+ end
1103
+
1104
+ def teardown
1105
+ Searchkick.disable_callbacks
1106
+ end
1107
+
1108
+ def test_search
1109
+ Product.create!(name: "Apple")
1110
+ Product.search_index.refresh
1111
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1112
+ end
1113
+ end
1114
+ ```
1115
+
1116
+ ### Minitest
1117
+
1118
+ Add to your `test/test_helper.rb`:
1119
+
1120
+ ```ruby
1121
+ # reindex models
1122
+ Product.reindex
1123
+
1124
+ # and disable callbacks
1125
+ Searchkick.disable_callbacks
1126
+ ```
1127
+
1128
+ And use:
1129
+
1130
+ ```ruby
1131
+ class ProductTest < Minitest::Test
1132
+ def setup
1133
+ Searchkick.enable_callbacks
1134
+ end
1135
+
1136
+ def teardown
1137
+ Searchkick.disable_callbacks
1138
+ end
1139
+
1140
+ def test_search
1141
+ Product.create!(name: "Apple")
1142
+ Product.search_index.refresh
1143
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1144
+ end
1145
+ end
1146
+ ```
1147
+
1148
+ ### RSpec
1149
+
1150
+ Add to your `spec/spec_helper.rb`:
1151
+
1152
+ ```ruby
1153
+ RSpec.configure do |config|
1154
+ config.before(:suite) do
1155
+ # reindex models
1156
+ Product.reindex
1157
+
1158
+ # and disable callbacks
1159
+ Searchkick.disable_callbacks
1160
+ end
1161
+
1162
+ config.around(:each, search: true) do |example|
1163
+ Searchkick.callbacks(nil) do
1164
+ example.run
1165
+ end
1166
+ end
1167
+ end
1168
+ ```
1169
+
1170
+ And use:
1171
+
1172
+ ```ruby
1173
+ describe Product, search: true do
1174
+ it "searches" do
1175
+ Product.create!(name: "Apple")
1176
+ Product.search_index.refresh
1177
+ assert_equal ["Apple"], Product.search("apple").map(&:name)
1178
+ end
1179
+ end
1180
+ ```
1181
+
1182
+ ### Factory Bot
1183
+
1184
+ Use a trait and an after `create` hook for each indexed model:
1185
+
1186
+ ```ruby
1187
+ FactoryBot.define do
1188
+ factory :product do
1189
+ # ...
1190
+
1191
+ # Note: This should be the last trait in the list so `reindex` is called
1192
+ # after all the other callbacks complete.
1193
+ trait :reindex do
1194
+ after(:create) do |product, _evaluator|
1195
+ product.reindex(refresh: true)
1196
+ end
1197
+ end
1198
+ end
1199
+ end
1200
+
1201
+ # use it
1202
+ FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1203
+ ```
1204
+
1205
+ ### GitHub Actions
1206
+
1207
+ Check out [setup-elasticsearch](https://github.com/ankane/setup-elasticsearch) for an easy way to install Elasticsearch.
1208
+
1209
+ ```yml
1210
+ - uses: ankane/setup-elasticsearch@v1
1211
+ ```
1212
+
1058
1213
  ## Deployment
1059
1214
 
1060
1215
  Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
1061
1216
 
1217
+ - [Elastic Cloud](#elastic-cloud)
1218
+ - [Heroku](#heroku)
1219
+ - [Amazon Elasticsearch Service](#amazon-elasticsearch-service)
1220
+ - [Self-Hosted and Other](#other)
1221
+
1222
+ ### Elastic Cloud
1223
+
1224
+ Create an initializer `config/initializers/elasticsearch.rb` with:
1225
+
1226
+ ```ruby
1227
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1228
+ ```
1229
+
1230
+ Then deploy and reindex:
1231
+
1232
+ ```sh
1233
+ rake searchkick:reindex:all
1234
+ ```
1235
+
1062
1236
  ### Heroku
1063
1237
 
1064
- 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.
1238
+ 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).
1065
1239
 
1066
1240
  For Bonsai:
1067
1241
 
@@ -1070,6 +1244,13 @@ heroku addons:create bonsai
1070
1244
  heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1071
1245
  ```
1072
1246
 
1247
+ For SearchBox:
1248
+
1249
+ ```sh
1250
+ heroku addons:create searchbox:starter
1251
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
1252
+ ```
1253
+
1073
1254
  For Elastic Cloud (previously Found):
1074
1255
 
1075
1256
  ```sh
@@ -1083,16 +1264,16 @@ Visit the Shield page and reset your password. You’ll need to add the username
1083
1264
  heroku config:get FOUNDELASTICSEARCH_URL
1084
1265
  ```
1085
1266
 
1086
- And add `elastic:password@` right after `https://`:
1267
+ And add `elastic:password@` right after `https://` and add port `9243` at the end:
1087
1268
 
1088
1269
  ```sh
1089
- heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io
1270
+ heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io:9243
1090
1271
  ```
1091
1272
 
1092
1273
  Then deploy and reindex:
1093
1274
 
1094
1275
  ```sh
1095
- heroku run rake searchkick:reindex CLASS=Product
1276
+ heroku run rake searchkick:reindex:all
1096
1277
  ```
1097
1278
 
1098
1279
  ### Amazon Elasticsearch Service
@@ -1100,7 +1281,7 @@ heroku run rake searchkick:reindex CLASS=Product
1100
1281
  Create an initializer `config/initializers/elasticsearch.rb` with:
1101
1282
 
1102
1283
  ```ruby
1103
- ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com"
1284
+ ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
1104
1285
  ```
1105
1286
 
1106
1287
  To use signed requests, include in your Gemfile:
@@ -1122,21 +1303,21 @@ Searchkick.aws_credentials = {
1122
1303
  Then deploy and reindex:
1123
1304
 
1124
1305
  ```sh
1125
- rake searchkick:reindex CLASS=Product
1306
+ rake searchkick:reindex:all
1126
1307
  ```
1127
1308
 
1128
- ### Other
1309
+ ### Self-Hosted and Other
1129
1310
 
1130
1311
  Create an initializer `config/initializers/elasticsearch.rb` with:
1131
1312
 
1132
1313
  ```ruby
1133
- ENV["ELASTICSEARCH_URL"] = "https://user:password@host"
1314
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1134
1315
  ```
1135
1316
 
1136
1317
  Then deploy and reindex:
1137
1318
 
1138
1319
  ```sh
1139
- rake searchkick:reindex CLASS=Product
1320
+ rake searchkick:reindex:all
1140
1321
  ```
1141
1322
 
1142
1323
  ### Data Protection
@@ -1199,7 +1380,7 @@ If you run into issues on Windows, check out [this post](https://www.rastating.c
1199
1380
 
1200
1381
  ### Searchable Fields
1201
1382
 
1202
- 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.
1383
+ 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.
1203
1384
 
1204
1385
  ```ruby
1205
1386
  class Product < ApplicationRecord
@@ -1435,10 +1616,8 @@ Create a custom mapping:
1435
1616
  ```ruby
1436
1617
  class Product < ApplicationRecord
1437
1618
  searchkick mappings: {
1438
- product: {
1439
- properties: {
1440
- name: {type: "keyword"}
1441
- }
1619
+ properties: {
1620
+ name: {type: "keyword"}
1442
1621
  }
1443
1622
  }
1444
1623
  end
@@ -1461,8 +1640,6 @@ And use the `body` option to search:
1461
1640
  products = Product.search body: {query: {match: {name: "milk"}}}
1462
1641
  ```
1463
1642
 
1464
- **Note:** This replaces the entire body, so other options are ignored.
1465
-
1466
1643
  View the response with:
1467
1644
 
1468
1645
  ```ruby
@@ -1506,24 +1683,61 @@ Then use `products` and `coupons` as typical results.
1506
1683
 
1507
1684
  **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors.
1508
1685
 
1509
- ## Multiple Indices
1686
+ ## Multiple Models
1510
1687
 
1511
- Search across multiple models/indices with:
1688
+ Search across multiple models with:
1512
1689
 
1513
1690
  ```ruby
1514
- Searchkick.search "milk", index_name: [Product, Category]
1691
+ Searchkick.search "milk", models: [Product, Category]
1515
1692
  ```
1516
1693
 
1517
- Specify conditions for different indices
1694
+ Boost specific models with:
1518
1695
 
1519
1696
  ```ruby
1520
- where: {_or: [{_type: "product", in_stock: true}, {_type: "category", active: true}]}
1697
+ indices_boost: {Category => 2, Product => 1}
1521
1698
  ```
1522
1699
 
1523
- Boost specific indices with:
1700
+ ## Multi-Tenancy
1701
+
1702
+ 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.
1703
+
1704
+ ## Scroll API
1705
+
1706
+ Searchkick also supports the [scroll API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html). Scrolling is not intended for real time user requests, but rather for processing large amounts of data.
1524
1707
 
1525
1708
  ```ruby
1526
- indices_boost: {Category => 2, Product => 1}
1709
+ Product.search("*", scroll: "1m").scroll do |batch|
1710
+ # process batch ...
1711
+ end
1712
+ ```
1713
+
1714
+ You can also scroll batches manually.
1715
+
1716
+ ```ruby
1717
+ products = Product.search "*", scroll: "1m"
1718
+ while products.any?
1719
+ # process batch ...
1720
+
1721
+ products = products.scroll
1722
+ end
1723
+
1724
+ products.clear_scroll
1725
+ ```
1726
+
1727
+ ## Deep Paging
1728
+
1729
+ 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:
1730
+
1731
+ ```ruby
1732
+ class Product < ApplicationRecord
1733
+ searchkick deep_paging: true
1734
+ end
1735
+ ```
1736
+
1737
+ If you just need an accurate total count with Elasticsearch 7, you can instead use:
1738
+
1739
+ ```ruby
1740
+ Product.search("pears", body_options: {track_total_hits: true})
1527
1741
  ```
1528
1742
 
1529
1743
  ## Nested Data
@@ -1667,7 +1881,7 @@ Product.search "milk", includes: [:brand, :stores]
1667
1881
  Eager load different associations by model
1668
1882
 
1669
1883
  ```ruby
1670
- Searchkick.search("*", index_name: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
1884
+ Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
1671
1885
  ```
1672
1886
 
1673
1887
  Run additional scopes on results
@@ -1693,14 +1907,6 @@ class Product < ApplicationRecord
1693
1907
  end
1694
1908
  ```
1695
1909
 
1696
- Turn off stemming
1697
-
1698
- ```ruby
1699
- class Product < ApplicationRecord
1700
- searchkick stem: false
1701
- end
1702
- ```
1703
-
1704
1910
  Turn on stemming for conversions
1705
1911
 
1706
1912
  ```ruby
@@ -1709,14 +1915,6 @@ class Product < ApplicationRecord
1709
1915
  end
1710
1916
  ```
1711
1917
 
1712
- Use a different [similarity algorithm](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html) for scoring
1713
-
1714
- ```ruby
1715
- class Product < ApplicationRecord
1716
- searchkick similarity: "classic"
1717
- end
1718
- ```
1719
-
1720
1918
  Make search case-sensitive
1721
1919
 
1722
1920
  ```ruby
@@ -1801,136 +1999,10 @@ Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
1801
1999
  Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1802
2000
  ```
1803
2001
 
1804
- ## Testing
1805
-
1806
- For performance, only enable Searchkick callbacks for the tests that need it.
1807
-
1808
- ### Minitest
1809
-
1810
- Add to your `test/test_helper.rb`:
1811
-
1812
- ```ruby
1813
- # reindex models
1814
- Product.reindex
1815
-
1816
- # and disable callbacks
1817
- Searchkick.disable_callbacks
1818
- ```
1819
-
1820
- And use:
1821
-
1822
- ```ruby
1823
- class ProductTest < Minitest::Test
1824
- def setup
1825
- Searchkick.enable_callbacks
1826
- end
1827
-
1828
- def teardown
1829
- Searchkick.disable_callbacks
1830
- end
1831
-
1832
- def test_search
1833
- Product.create!(name: "Apple")
1834
- Product.search_index.refresh
1835
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1836
- end
1837
- end
1838
- ```
1839
-
1840
- ### RSpec
1841
-
1842
- Add to your `spec/spec_helper.rb`:
1843
-
1844
- ```ruby
1845
- RSpec.configure do |config|
1846
- config.before(:suite) do
1847
- # reindex models
1848
- Product.reindex
1849
-
1850
- # and disable callbacks
1851
- Searchkick.disable_callbacks
1852
- end
1853
-
1854
- config.around(:each, search: true) do |example|
1855
- Searchkick.callbacks(true) do
1856
- example.run
1857
- end
1858
- end
1859
- end
1860
- ```
1861
-
1862
- And use:
1863
-
1864
- ```ruby
1865
- describe Product, search: true do
1866
- it "searches" do
1867
- Product.create!(name: "Apple")
1868
- Product.search_index.refresh
1869
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1870
- end
1871
- end
1872
- ```
1873
-
1874
- ### Factory Bot
1875
-
1876
- Use a trait and an after `create` hook for each indexed model:
2002
+ ## Elasticsearch 6 to 7 Upgrade
1877
2003
 
1878
- ```ruby
1879
- FactoryBot.define do
1880
- factory :product do
1881
- # ...
1882
-
1883
- # Note: This should be the last trait in the list so `reindex` is called
1884
- # after all the other callbacks complete.
1885
- trait :reindex do
1886
- after(:create) do |product, _evaluator|
1887
- product.reindex(refresh: true)
1888
- end
1889
- end
1890
- end
1891
- end
1892
-
1893
- # use it
1894
- FactoryBot.create(:product, :some_trait, :reindex, some_attribute: "foo")
1895
- ```
1896
-
1897
- ### Parallel Tests
1898
-
1899
- Set:
1900
-
1901
- ```ruby
1902
- Searchkick.index_suffix = ENV["TEST_ENV_NUMBER"]
1903
- ```
1904
-
1905
- ## Multi-Tenancy
1906
-
1907
- 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.
1908
-
1909
- ## Upgrading
1910
-
1911
- See [how to upgrade to Searchkick 3](docs/Searchkick-3-Upgrade.md)
1912
-
1913
- ## Elasticsearch 5 to 6 Upgrade
1914
-
1915
- 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.
1916
-
1917
- ```ruby
1918
- class Product < ApplicationRecord
1919
- searchkick _all: false, default_fields: [:name]
1920
- end
1921
- ```
1922
-
1923
- If you need search across multiple fields, we recommend creating a similar field in your search data.
1924
-
1925
- ```ruby
1926
- class Product < ApplicationRecord
1927
- def search_data
1928
- {
1929
- all: [name, size, quantity].join(" ")
1930
- }
1931
- end
1932
- end
1933
- ```
2004
+ 1. Install Searchkick 4
2005
+ 2. Upgrade your Elasticsearch cluster
1934
2006
 
1935
2007
  ## Elasticsearch Gotchas
1936
2008
 
@@ -1963,11 +2035,6 @@ View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.
1963
2035
 
1964
2036
  Thanks to Karel Minarik for [Elasticsearch Ruby](https://github.com/elasticsearch/elasticsearch-ruby) and [Tire](https://github.com/karmi/retire), Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884), and Alex Leschenko for [Elasticsearch autocomplete](https://github.com/leschenko/elasticsearch_autocomplete).
1965
2037
 
1966
- ## Roadmap
1967
-
1968
- - Reindex API
1969
- - Incorporate human eval
1970
-
1971
2038
  ## Contributing
1972
2039
 
1973
2040
  Everyone is encouraged to help improve this project. Here are a few ways you can help:
@@ -1977,13 +2044,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
1977
2044
  - Write, clarify, or fix documentation
1978
2045
  - Suggest or add new features
1979
2046
 
1980
- If you’re looking for ideas, [try here](https://github.com/ankane/searchkick/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
1981
-
1982
- To get started with development and testing:
2047
+ To get started with development:
1983
2048
 
1984
2049
  ```sh
1985
2050
  git clone https://github.com/ankane/searchkick.git
1986
2051
  cd searchkick
1987
2052
  bundle install
1988
- rake test
2053
+ bundle exec rake test
1989
2054
  ```
2055
+
2056
+ Feel free to open an issue to get feedback on your idea before spending too much time on it.