searchkick 2.3.2 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +251 -84
  3. data/LICENSE.txt +1 -1
  4. data/README.md +552 -432
  5. data/lib/searchkick/bulk_indexer.rb +173 -0
  6. data/lib/searchkick/bulk_reindex_job.rb +2 -2
  7. data/lib/searchkick/hash_wrapper.rb +12 -0
  8. data/lib/searchkick/index.rb +187 -348
  9. data/lib/searchkick/index_options.rb +494 -282
  10. data/lib/searchkick/logging.rb +17 -13
  11. data/lib/searchkick/model.rb +52 -97
  12. data/lib/searchkick/multi_search.rb +9 -10
  13. data/lib/searchkick/process_batch_job.rb +17 -4
  14. data/lib/searchkick/process_queue_job.rb +20 -12
  15. data/lib/searchkick/query.rb +415 -199
  16. data/lib/searchkick/railtie.rb +7 -0
  17. data/lib/searchkick/record_data.rb +128 -0
  18. data/lib/searchkick/record_indexer.rb +79 -0
  19. data/lib/searchkick/reindex_queue.rb +1 -1
  20. data/lib/searchkick/reindex_v2_job.rb +14 -12
  21. data/lib/searchkick/results.rb +135 -41
  22. data/lib/searchkick/version.rb +1 -1
  23. data/lib/searchkick.rb +130 -61
  24. data/lib/tasks/searchkick.rake +34 -0
  25. metadata +18 -162
  26. data/.gitignore +0 -22
  27. data/.travis.yml +0 -39
  28. data/Gemfile +0 -16
  29. data/Rakefile +0 -20
  30. data/benchmark/Gemfile +0 -23
  31. data/benchmark/benchmark.rb +0 -97
  32. data/lib/searchkick/tasks.rb +0 -33
  33. data/searchkick.gemspec +0 -28
  34. data/test/aggs_test.rb +0 -197
  35. data/test/autocomplete_test.rb +0 -75
  36. data/test/boost_test.rb +0 -202
  37. data/test/callbacks_test.rb +0 -59
  38. data/test/ci/before_install.sh +0 -17
  39. data/test/errors_test.rb +0 -19
  40. data/test/gemfiles/activerecord31.gemfile +0 -7
  41. data/test/gemfiles/activerecord32.gemfile +0 -7
  42. data/test/gemfiles/activerecord40.gemfile +0 -8
  43. data/test/gemfiles/activerecord41.gemfile +0 -8
  44. data/test/gemfiles/activerecord42.gemfile +0 -7
  45. data/test/gemfiles/activerecord50.gemfile +0 -7
  46. data/test/gemfiles/apartment.gemfile +0 -8
  47. data/test/gemfiles/cequel.gemfile +0 -8
  48. data/test/gemfiles/mongoid2.gemfile +0 -7
  49. data/test/gemfiles/mongoid3.gemfile +0 -6
  50. data/test/gemfiles/mongoid4.gemfile +0 -7
  51. data/test/gemfiles/mongoid5.gemfile +0 -7
  52. data/test/gemfiles/mongoid6.gemfile +0 -12
  53. data/test/gemfiles/nobrainer.gemfile +0 -8
  54. data/test/gemfiles/parallel_tests.gemfile +0 -8
  55. data/test/geo_shape_test.rb +0 -175
  56. data/test/highlight_test.rb +0 -78
  57. data/test/index_test.rb +0 -166
  58. data/test/inheritance_test.rb +0 -83
  59. data/test/marshal_test.rb +0 -8
  60. data/test/match_test.rb +0 -276
  61. data/test/misspellings_test.rb +0 -56
  62. data/test/model_test.rb +0 -42
  63. data/test/multi_search_test.rb +0 -36
  64. data/test/multi_tenancy_test.rb +0 -22
  65. data/test/order_test.rb +0 -46
  66. data/test/pagination_test.rb +0 -70
  67. data/test/partial_reindex_test.rb +0 -58
  68. data/test/query_test.rb +0 -35
  69. data/test/records_test.rb +0 -10
  70. data/test/reindex_test.rb +0 -64
  71. data/test/reindex_v2_job_test.rb +0 -32
  72. data/test/routing_test.rb +0 -23
  73. data/test/should_index_test.rb +0 -32
  74. data/test/similar_test.rb +0 -28
  75. data/test/sql_test.rb +0 -214
  76. data/test/suggest_test.rb +0 -95
  77. data/test/support/kaminari.yml +0 -21
  78. data/test/synonyms_test.rb +0 -67
  79. data/test/test_helper.rb +0 -567
  80. data/test/where_test.rb +0 -223
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  :rocket: Intelligent search made easy
4
4
 
5
- Searchkick learns what **your users** are looking for. As more people search, it gets smarter and the results get better. It’s friendly for developers - and magical for your users.
5
+ **Searchkick learns what your users are looking for.** As more people search, it gets smarter and the results get better. It’s friendly for developers - and magical for your users.
6
6
 
7
7
  Searchkick handles:
8
8
 
@@ -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
 
@@ -19,12 +19,13 @@ Plus:
19
19
  - easily personalize results for each user
20
20
  - autocomplete
21
21
  - “Did you mean” suggestions
22
+ - supports many languages
22
23
  - works with ActiveRecord, Mongoid, and NoBrainer
23
24
 
24
- :speech_balloon: Get [handcrafted updates](http://chartkick.us7.list-manage.com/subscribe?u=952c861f99eb43084e0a49f98&id=6ea6541e8e&group[0][4]=true) for new features
25
-
26
25
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
27
26
 
27
+ :speech_balloon: Get [handcrafted updates](https://chartkick.us7.list-manage.com/subscribe?u=952c861f99eb43084e0a49f98&id=6ea6541e8e&group[0][4]=true) for new features
28
+
28
29
  [![Build Status](https://travis-ci.org/ankane/searchkick.svg?branch=master)](https://travis-ci.org/ankane/searchkick)
29
30
 
30
31
  ## Contents
@@ -32,16 +33,19 @@ Plus:
32
33
  - [Getting Started](#getting-started)
33
34
  - [Querying](#querying)
34
35
  - [Indexing](#indexing)
36
+ - [Intelligent Search](#intelligent-search)
35
37
  - [Instant Search / Autocomplete](#instant-search--autocomplete)
36
38
  - [Aggregations](#aggregations)
39
+ - [Testing](#testing)
37
40
  - [Deployment](#deployment)
38
41
  - [Performance](#performance)
39
42
  - [Elasticsearch DSL](#advanced)
40
43
  - [Reference](#reference)
44
+ - [Contributing](#contributing)
41
45
 
42
46
  ## Getting Started
43
47
 
44
- [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:
45
49
 
46
50
  ```sh
47
51
  brew install elasticsearch
@@ -54,12 +58,12 @@ Add this line to your application’s Gemfile:
54
58
  gem 'searchkick'
55
59
  ```
56
60
 
57
- The latest version works with Elasticsearch 2 and 5. For Elasticsearch 1, use version 1.5.1 and [this readme](https://github.com/ankane/searchkick/blob/v1.5.1/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).
58
62
 
59
63
  Add searchkick to models you want to search.
60
64
 
61
65
  ```ruby
62
- class Product < ActiveRecord::Base
66
+ class Product < ApplicationRecord
63
67
  searchkick
64
68
  end
65
69
  ```
@@ -73,7 +77,7 @@ Product.reindex
73
77
  And to query, use:
74
78
 
75
79
  ```ruby
76
- products = Product.search "apples"
80
+ products = Product.search("apples")
77
81
  products.each do |product|
78
82
  puts product.name
79
83
  end
@@ -99,14 +103,19 @@ Where
99
103
 
100
104
  ```ruby
101
105
  where: {
102
- expires_at: {gt: Time.now}, # lt, gte, lte also available
103
- orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
104
- aisle_id: [25, 30], # in
105
- store_id: {not: 2}, # not
106
- aisle_id: {not: [25, 30]}, # not in
107
- user_ids: {all: [1, 3]}, # all elements in array
108
- category: /frozen .+/, # regexp
109
- _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
110
119
  }
111
120
  ```
112
121
 
@@ -130,6 +139,8 @@ Select
130
139
  select: [:name]
131
140
  ```
132
141
 
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)
143
+
133
144
  ### Results
134
145
 
135
146
  Searches return a `Searchkick::Results` object. This responds like an array to most methods.
@@ -165,6 +176,8 @@ Get the full response from Elasticsearch
165
176
  results.response
166
177
  ```
167
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
+
168
181
  ### Boosting
169
182
 
170
183
  Boost important fields
@@ -188,7 +201,16 @@ boost_where: {user_id: {value: 1, factor: 100}} # default factor is 1000
188
201
  boost_where: {user_id: [{value: 1, factor: 100}, {value: 2, factor: 200}]}
189
202
  ```
190
203
 
191
- [Conversions](#keep-getting-better) are also a great way to boost.
204
+ Boost by recency
205
+
206
+ ```ruby
207
+ boost_by_recency: {created_at: {scale: "7d", decay: 0.5}}
208
+ ```
209
+
210
+ You can also boost by:
211
+
212
+ - [Conversions](#keep-getting-better)
213
+ - [Distance](#boost-by-distance)
192
214
 
193
215
  ### Get Everything
194
216
 
@@ -236,7 +258,7 @@ Product.search "fresh honey", operator: "or" # fresh OR honey
236
258
  By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
237
259
 
238
260
  ```ruby
239
- class Product < ActiveRecord::Base
261
+ class Product < ApplicationRecord
240
262
  searchkick word_start: [:name]
241
263
  end
242
264
  ```
@@ -249,19 +271,21 @@ Product.search "back", fields: [:name], match: :word_start
249
271
 
250
272
  Available options are:
251
273
 
252
- ```ruby
253
- :word # default
254
- :word_start
255
- :word_middle
256
- :word_end
257
- :text_start
258
- :text_middle
259
- :text_end
260
- ```
274
+ Option | Matches | Example
275
+ --- | --- | ---
276
+ `:word` | entire word | `apple` matches `apple`
277
+ `:word_start` | start of word | `app` matches `apple`
278
+ `:word_middle` | any part of word | `ppl` matches `apple`
279
+ `:word_end` | end of word | `ple` matches `apple`
280
+ `:text_start` | start of text | `gre` matches `green apple`, `app` does not match
281
+ `:text_middle` | any part of text | `een app` matches `green apple`
282
+ `:text_end` | end of text | `ple` matches `green apple`, `een` does not match
283
+
284
+ The default is `:word`. The most matches will happen with `:word_middle`.
261
285
 
262
286
  ### Exact Matches
263
287
 
264
- To match a field exactly (case-insensitive), use:
288
+ To match a field exactly (case-sensitive), use:
265
289
 
266
290
  ```ruby
267
291
  User.search query, fields: [{email: :exact}, :name]
@@ -275,46 +299,106 @@ To only match the exact order, use:
275
299
  User.search "fresh honey", match: :phrase
276
300
  ```
277
301
 
278
- ### 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.
279
305
 
280
306
  Searchkick defaults to English for stemming. To change this, use:
281
307
 
282
308
  ```ruby
283
- class Product < ActiveRecord::Base
309
+ class Product < ApplicationRecord
284
310
  searchkick language: "german"
285
311
  end
286
312
  ```
287
313
 
288
- [See the list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html)
314
+ See the [list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html). A few languages require plugins:
289
315
 
290
- ### Synonyms
316
+ - `chinese` - [analysis-ik plugin](https://github.com/medcl/elasticsearch-analysis-ik)
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)
319
+ - `korean` - [analysis-openkoreantext plugin](https://github.com/open-korean-text/elasticsearch-analysis-openkoreantext)
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)
323
+ - `vietnamese` - [analysis-vietnamese plugin](https://github.com/duydo/elasticsearch-analysis-vietnamese)
324
+
325
+ Disable stemming with:
291
326
 
292
327
  ```ruby
293
- class Product < ActiveRecord::Base
294
- searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]]
328
+ class Image < ApplicationRecord
329
+ searchkick stem: false
295
330
  end
296
331
  ```
297
332
 
298
- Call `Product.reindex` after changing synonyms.
333
+ Exclude certain words from stemming with:
299
334
 
300
- To read synonyms from a file, use:
335
+ ```ruby
336
+ class Image < ApplicationRecord
337
+ searchkick stem_exclusion: ["apples"]
338
+ end
339
+ ```
340
+
341
+ Or change how words are stemmed:
301
342
 
302
343
  ```ruby
303
- synonyms: -> { CSV.read("/some/path/synonyms.csv") }
344
+ class Image < ApplicationRecord
345
+ searchkick stemmer_override: ["apples => other"]
346
+ end
304
347
  ```
305
348
 
349
+ ### Synonyms
350
+
351
+ ```ruby
352
+ class Product < ApplicationRecord
353
+ searchkick search_synonyms: [["pop", "soda"], ["burger", "hamburger"]]
354
+ end
355
+ ```
356
+
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
+
306
359
  For directional synonyms, use:
307
360
 
308
361
  ```ruby
309
- synonyms: ["lightbulb => halogenlamp"]
362
+ search_synonyms: ["lightbulb => halogenlamp"]
310
363
  ```
311
364
 
312
- ### Tags and Dynamic Synonyms
365
+ ### Dynamic Synonyms
313
366
 
314
- 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:
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+
370
+
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.
372
+
373
+ ```txt
374
+ pop, soda
375
+ burger, hamburger
376
+ ```
377
+
378
+ Then use:
379
+
380
+ ```ruby
381
+ search_synonyms: "synonyms.txt"
382
+ ```
383
+
384
+ Add [elasticsearch-xpack](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch-xpack) to your Gemfile:
315
385
 
316
386
  ```ruby
317
- class Product < ActiveRecord::Base
387
+ gem 'elasticsearch-xpack', '>= 7.8.0'
388
+ ```
389
+
390
+ And use:
391
+
392
+ ```ruby
393
+ Product.search_index.reload_synonyms
394
+ ```
395
+
396
+ #### Elasticsearch < 7.3
397
+
398
+ You can use a library like [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on) and do:
399
+
400
+ ```ruby
401
+ class Product < ApplicationRecord
318
402
  acts_as_taggable
319
403
  scope :search_import, -> { includes(:tags) }
320
404
 
@@ -332,27 +416,6 @@ Search with:
332
416
  Product.search query, fields: [:name_tagged]
333
417
  ```
334
418
 
335
- ### WordNet
336
-
337
- Prepopulate English synonyms with the [WordNet database](https://en.wikipedia.org/wiki/WordNet).
338
-
339
- 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.
340
-
341
- ```sh
342
- cd /tmp
343
- curl -o wordnet.tar.gz http://wordnetcode.princeton.edu/3.0/WNprolog-3.0.tar.gz
344
- tar -zxvf wordnet.tar.gz
345
- mv prolog/wn_s.pl /var/lib
346
- ```
347
-
348
- Tell each model to use it:
349
-
350
- ```ruby
351
- class Product < ActiveRecord::Base
352
- searchkick wordnet: true
353
- end
354
- ```
355
-
356
419
  ### Misspellings
357
420
 
358
421
  By default, Searchkick handles misspelled queries by returning results with an [edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance) of one.
@@ -377,6 +440,14 @@ Turn off misspellings with:
377
440
  Product.search "zuchini", misspellings: false # no zucchini
378
441
  ```
379
442
 
443
+ Specify which fields can include misspellings with:
444
+
445
+ ```ruby
446
+ Product.search "zucini", fields: [:name, :color], misspellings: {fields: [:name]}
447
+ ```
448
+
449
+ > When doing this, you must also specify fields to search
450
+
380
451
  ### Bad Matches
381
452
 
382
453
  If a user searches `butter`, they may also get results for `peanut butter`. To prevent this, use:
@@ -396,6 +467,12 @@ exclude_queries = {
396
467
  Product.search query, exclude: exclude_queries[query]
397
468
  ```
398
469
 
470
+ You can demote results by boosting by a factor less than one:
471
+
472
+ ```ruby
473
+ Product.search("butter", boost_where: {category: {value: "pantry", factor: 0.5}})
474
+ ```
475
+
399
476
  ### Emoji
400
477
 
401
478
  Search :ice_cream::cake: and get `ice cream cake`!
@@ -417,7 +494,7 @@ Product.search "🍨🍰", emoji: true
417
494
  Control what data is indexed with the `search_data` method. Call `Product.reindex` after changing this method.
418
495
 
419
496
  ```ruby
420
- class Product < ActiveRecord::Base
497
+ class Product < ApplicationRecord
421
498
  belongs_to :department
422
499
 
423
500
  def search_data
@@ -433,7 +510,7 @@ end
433
510
  Searchkick uses `find_in_batches` to import documents. To eager load associations, use the `search_import` scope.
434
511
 
435
512
  ```ruby
436
- class Product < ActiveRecord::Base
513
+ class Product < ApplicationRecord
437
514
  scope :search_import, -> { includes(:department) }
438
515
  end
439
516
  ```
@@ -441,7 +518,7 @@ end
441
518
  By default, all records are indexed. To control which records are indexed, use the `should_index?` method together with the `search_import` scope.
442
519
 
443
520
  ```ruby
444
- class Product < ActiveRecord::Base
521
+ class Product < ApplicationRecord
445
522
  scope :search_import, -> { where(active: true) }
446
523
 
447
524
  def should_index?
@@ -470,11 +547,11 @@ For large data sets, try [parallel reindexing](#parallel-reindexing).
470
547
 
471
548
  - app starts
472
549
 
473
- ### Stay Synced
550
+ ### Strategies
474
551
 
475
552
  There are four strategies for keeping the index synced with your database.
476
553
 
477
- 1. Immediate (default)
554
+ 1. Inline (default)
478
555
 
479
556
  Anytime a record is inserted, updated, or deleted
480
557
 
@@ -483,12 +560,12 @@ There are four strategies for keeping the index synced with your database.
483
560
  Use background jobs for better performance
484
561
 
485
562
  ```ruby
486
- class Product < ActiveRecord::Base
563
+ class Product < ApplicationRecord
487
564
  searchkick callbacks: :async
488
565
  end
489
566
  ```
490
567
 
491
- And [install Active Job](https://github.com/ankane/activejob_backport) for Rails 4.1 and below. Jobs are added to a queue named `searchkick`.
568
+ Jobs are added to a queue named `searchkick`.
492
569
 
493
570
  3. Queuing
494
571
 
@@ -499,7 +576,7 @@ There are four strategies for keeping the index synced with your database.
499
576
  Turn off automatic syncing
500
577
 
501
578
  ```ruby
502
- class Product < ActiveRecord::Base
579
+ class Product < ApplicationRecord
503
580
  searchkick callbacks: false
504
581
  end
505
582
  ```
@@ -520,27 +597,25 @@ Searchkick.callbacks(false) do
520
597
  end
521
598
  ```
522
599
 
523
- #### Associations
600
+ ### Associations
524
601
 
525
602
  Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
526
603
 
527
604
  ```ruby
528
- class Image < ActiveRecord::Base
605
+ class Image < ApplicationRecord
529
606
  belongs_to :product
530
607
 
531
608
  after_commit :reindex_product
532
609
 
533
610
  def reindex_product
534
- product.reindex # or reindex_async
611
+ product.reindex
535
612
  end
536
613
  end
537
614
  ```
538
615
 
539
- ### Analytics
540
-
541
- The best starting point to improve your search **by far** is to track searches and conversions.
616
+ ## Intelligent Search
542
617
 
543
- [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.
544
619
 
545
620
  ```ruby
546
621
  Product.search "apple", track: {user_id: current_user.id}
@@ -553,21 +628,15 @@ Focus on:
553
628
  - top searches with low conversions
554
629
  - top searches with no results
555
630
 
556
- ### 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.
557
632
 
558
- 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.
559
-
560
- 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.
561
-
562
- You do **not** need to clean up the search queries. Searchkick automatically treats `apple` and `APPLES` the same.
563
-
564
- Next, add conversions to the index.
633
+ Add conversion data with:
565
634
 
566
635
  ```ruby
567
- class Product < ActiveRecord::Base
636
+ class Product < ApplicationRecord
568
637
  has_many :searches, class_name: "Searchjoy::Search", as: :convertable
569
638
 
570
- searchkick conversions: ["conversions"] # name of field
639
+ searchkick conversions: [:conversions] # name of field
571
640
 
572
641
  def search_data
573
642
  {
@@ -585,14 +654,16 @@ Reindex and set up a cron job to add new conversions daily.
585
654
  rake searchkick:reindex CLASS=Product
586
655
  ```
587
656
 
588
- **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.
658
+
659
+ For a more performant way to reindex conversion data, check out [performant conversions](#performant-conversions).
589
660
 
590
- ### Personalized Results
661
+ ## Personalized Results
591
662
 
592
663
  Order results differently for each user. For example, show a user’s previously purchased products before other results.
593
664
 
594
665
  ```ruby
595
- class Product < ActiveRecord::Base
666
+ class Product < ApplicationRecord
596
667
  def search_data
597
668
  {
598
669
  name: name,
@@ -608,20 +679,20 @@ Reindex and search with:
608
679
  Product.search "milk", boost_where: {orderer_ids: current_user.id}
609
680
  ```
610
681
 
611
- ### Instant Search / Autocomplete
682
+ ## Instant Search / Autocomplete
612
683
 
613
684
  Autocomplete predicts what a user will type, making the search experience faster and easier.
614
685
 
615
- ![Autocomplete](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/autocomplete.png)
686
+ ![Autocomplete](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/autocomplete.png)
616
687
 
617
- **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).
618
689
 
619
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).
620
691
 
621
692
  First, specify which fields use this feature. This is necessary since autocomplete can increase the index size significantly, but don’t worry - this gives you blazing faster queries.
622
693
 
623
694
  ```ruby
624
- class Movie < ActiveRecord::Base
695
+ class Movie < ApplicationRecord
625
696
  searchkick word_start: [:title, :director]
626
697
  end
627
698
  ```
@@ -632,7 +703,7 @@ Reindex and search with:
632
703
  Movie.search "jurassic pa", fields: [:title], match: :word_start
633
704
  ```
634
705
 
635
- Typically, you want to use a JavaScript library like [typeahead.js](http://twitter.github.io/typeahead.js/) or [jQuery UI](http://jqueryui.com/autocomplete/).
706
+ Typically, you want to use a JavaScript library like [typeahead.js](https://twitter.github.io/typeahead.js/) or [jQuery UI](https://jqueryui.com/autocomplete/).
636
707
 
637
708
  #### Here’s how to make it work with Rails
638
709
 
@@ -676,12 +747,12 @@ Then add the search box and JavaScript code to a view.
676
747
  </script>
677
748
  ```
678
749
 
679
- ### Suggestions
750
+ ## Suggestions
680
751
 
681
- ![Suggest](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/recursion.png)
752
+ ![Suggest](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/recursion.png)
682
753
 
683
754
  ```ruby
684
- class Product < ActiveRecord::Base
755
+ class Product < ApplicationRecord
685
756
  searchkick suggest: [:name] # fields to generate suggestions
686
757
  end
687
758
  ```
@@ -693,11 +764,11 @@ products = Product.search "peantu butta", suggest: true
693
764
  products.suggestions # ["peanut butter"]
694
765
  ```
695
766
 
696
- ### Aggregations
767
+ ## Aggregations
697
768
 
698
769
  [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) provide aggregated search data.
699
770
 
700
- ![Aggregations](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/facets.png)
771
+ ![Aggregations](https://gist.github.com/ankane/b6988db2802aca68a589b31e41b44195/raw/40febe948427e5bc53ec4e5dc248822855fef76f/facets.png)
701
772
 
702
773
  ```ruby
703
774
  products = Product.search "chuck taylor", aggs: [:product_type, :gender, :brand]
@@ -733,7 +804,7 @@ Product.search "apples", aggs: {store_id: {limit: 10}}
733
804
  Order
734
805
 
735
806
  ```ruby
736
- Product.search "wingtips", aggs: {color: {order: {"_term" => "asc"}}} # alphabetically
807
+ Product.search "wingtips", aggs: {color: {order: {"_key" => "asc"}}} # alphabetically
737
808
  ```
738
809
 
739
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)
@@ -751,75 +822,30 @@ Minimum document count
751
822
  Product.search "apples", aggs: {store_id: {min_doc_count: 2}}
752
823
  ```
753
824
 
754
- Date histogram
825
+ Script support
755
826
 
756
827
  ```ruby
757
- Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}}
828
+ Product.search "*", aggs: {color: {script: {source: "'Color: ' + _value"}}}
758
829
  ```
759
830
 
760
- #### Moving From Facets
761
-
762
- 1. Replace `facets` with `aggs` in searches. **Note:** Stats facets are not supported at this time.
763
-
764
- ```ruby
765
- products = Product.search "chuck taylor", facets: [:brand]
766
- # to
767
- products = Product.search "chuck taylor", aggs: [:brand]
768
- ```
769
-
770
- 2. Replace the `facets` method with `aggs` for results.
771
-
772
- ```ruby
773
- products.facets
774
- # to
775
- products.aggs
776
- ```
777
-
778
- The keys in results differ slightly. Instead of:
779
-
780
- ```json
781
- {
782
- "_type":"terms",
783
- "missing":0,
784
- "total":45,
785
- "other":34,
786
- "terms":[
787
- {"term":14.0,"count":11}
788
- ]
789
- }
790
- ```
791
-
792
- You get:
793
-
794
- ```json
795
- {
796
- "doc_count":45,
797
- "doc_count_error_upper_bound":0,
798
- "sum_other_doc_count":34,
799
- "buckets":[
800
- {"key":14.0,"doc_count":11}
801
- ]
802
- }
803
- ```
804
-
805
- Update your application to handle this.
831
+ Date histogram
806
832
 
807
- 3. By default, `where` conditions apply to aggregations. This is equivalent to `smart_facets: true`. If you have `smart_facets: true`, you can remove it. If this is not desired, set `smart_aggs: false`.
833
+ ```ruby
834
+ Product.search "pear", aggs: {products_per_year: {date_histogram: {field: :created_at, interval: :year}}}
835
+ ```
808
836
 
809
- 4. If you have any range facets with dates, change the key from `ranges` to `date_ranges`.
837
+ For other aggregation types, including sub-aggregations, use `body_options`:
810
838
 
811
- ```ruby
812
- facets: {date_field: {ranges: date_ranges}}
813
- # to
814
- aggs: {date_field: {date_ranges: date_ranges}}
815
- ```
839
+ ```ruby
840
+ Product.search "orange", body_options: {aggs: {price: {histogram: {field: :price, interval: 10}}}}
841
+ ```
816
842
 
817
- ### Highlight
843
+ ## Highlight
818
844
 
819
845
  Specify which fields to index with highlighting.
820
846
 
821
847
  ```ruby
822
- class Product < ActiveRecord::Base
848
+ class Product < ApplicationRecord
823
849
  searchkick highlight: [:name]
824
850
  end
825
851
  ```
@@ -827,23 +853,21 @@ end
827
853
  Highlight the search query in the results.
828
854
 
829
855
  ```ruby
830
- bands = Band.search "cinema", fields: [:name], highlight: true
856
+ bands = Band.search "cinema", highlight: true
831
857
  ```
832
858
 
833
- **Note:** The `fields` option is required, unless highlight options are given - see below.
834
-
835
859
  View the highlighted fields with:
836
860
 
837
861
  ```ruby
838
- bands.each do |band|
839
- band.search_highlights[:name] # "Two Door <em>Cinema</em> Club"
862
+ bands.with_highlights.each do |band, highlights|
863
+ highlights[:name] # "Two Door <em>Cinema</em> Club"
840
864
  end
841
865
  ```
842
866
 
843
867
  To change the tag, use:
844
868
 
845
869
  ```ruby
846
- Band.search "cinema", fields: [:name], highlight: {tag: "<strong>"}
870
+ Band.search "cinema", highlight: {tag: "<strong>"}
847
871
  ```
848
872
 
849
873
  To highlight and search different fields, use:
@@ -852,7 +876,16 @@ To highlight and search different fields, use:
852
876
  Band.search "cinema", fields: [:name], highlight: {fields: [:description]}
853
877
  ```
854
878
 
855
- Additional options, including fragment size, can be specified for each field:
879
+ By default, the entire field is highlighted. To get small snippets instead, use:
880
+
881
+ ```ruby
882
+ bands = Band.search "cinema", highlight: {fragment_size: 20}
883
+ bands.with_highlights(multiple: true).each do |band, highlights|
884
+ highlights[:name].join(" and ")
885
+ end
886
+ ```
887
+
888
+ Additional options can be specified for each field:
856
889
 
857
890
  ```ruby
858
891
  Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
@@ -860,7 +893,7 @@ Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size
860
893
 
861
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).
862
895
 
863
- ### Similar Items
896
+ ## Similar Items
864
897
 
865
898
  Find similar items.
866
899
 
@@ -869,14 +902,14 @@ product = Product.first
869
902
  product.similar(fields: [:name], where: {size: "12 oz"})
870
903
  ```
871
904
 
872
- ### Geospatial Searches
905
+ ## Geospatial Searches
873
906
 
874
907
  ```ruby
875
- class Restaurant < ActiveRecord::Base
908
+ class Restaurant < ApplicationRecord
876
909
  searchkick locations: [:location]
877
910
 
878
911
  def search_data
879
- attributes.merge location: {lat: latitude, lon: longitude}
912
+ attributes.merge(location: {lat: latitude, lon: longitude})
880
913
  end
881
914
  end
882
915
  ```
@@ -893,6 +926,8 @@ Bounded by a box
893
926
  Restaurant.search "sushi", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}}
894
927
  ```
895
928
 
929
+ **Note:** `top_right` and `bottom_left` also work
930
+
896
931
  Bounded by a polygon
897
932
 
898
933
  ```ruby
@@ -918,10 +953,8 @@ Restaurant.search "wings", boost_by_distance: {location: {origin: {lat: 37, lon:
918
953
  You can also index and search geo shapes.
919
954
 
920
955
  ```ruby
921
- class Restaurant < ActiveRecord::Base
922
- searchkick geo_shape: {
923
- bounds: {tree: "geohash", precision: "1km"}
924
- }
956
+ class Restaurant < ApplicationRecord
957
+ searchkick geo_shape: [:bounds]
925
958
 
926
959
  def search_data
927
960
  attributes.merge(
@@ -954,12 +987,6 @@ Not touching the query shape
954
987
  Restaurant.search "burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
955
988
  ```
956
989
 
957
- Containing the query shape (Elasticsearch 2.2+)
958
-
959
- ```ruby
960
- Restaurant.search "fries", where: {bounds: {geo_shape: {type: "envelope", relation: "contains", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
961
- ```
962
-
963
990
  ## Inheritance
964
991
 
965
992
  Searchkick supports single table inheritance.
@@ -969,11 +996,19 @@ class Dog < Animal
969
996
  end
970
997
  ```
971
998
 
999
+ In your parent model, set:
1000
+
1001
+ ```ruby
1002
+ class Animal < ApplicationRecord
1003
+ searchkick inheritance: true
1004
+ end
1005
+ ```
1006
+
972
1007
  The parent and child model can both reindex.
973
1008
 
974
1009
  ```ruby
975
1010
  Animal.reindex
976
- Dog.reindex # equivalent
1011
+ Dog.reindex # equivalent, all animals reindexed
977
1012
  ```
978
1013
 
979
1014
  And to search, use:
@@ -984,11 +1019,14 @@ Dog.search "*" # just dogs
984
1019
  Animal.search "*", type: [Dog, Cat] # just cats and dogs
985
1020
  ```
986
1021
 
987
- **Note:** The `suggest` option retrieves suggestions from the parent at the moment.
1022
+ **Notes:**
988
1023
 
989
- ```ruby
990
- Dog.search "airbudd", suggest: true # suggestions for all animals
991
- ```
1024
+ 1. The `suggest` option retrieves suggestions from the parent at the moment.
1025
+
1026
+ ```ruby
1027
+ Dog.search "airbudd", suggest: true # suggestions for all animals
1028
+ ```
1029
+ 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.
992
1030
 
993
1031
  ## Debugging Queries
994
1032
 
@@ -1031,52 +1069,217 @@ Product.search_index.tokens("dieg", analyzer: "searchkick_word_search")
1031
1069
 
1032
1070
  See the [complete list of analyzers](https://github.com/ankane/searchkick/blob/31780ddac7a89eab1e0552a32b403f2040a37931/lib/searchkick/index_options.rb#L32).
1033
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
+
1034
1205
  ## Deployment
1035
1206
 
1036
1207
  Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
1037
1208
 
1209
+ - [Elastic Cloud](#elastic-cloud)
1210
+ - [Heroku](#heroku)
1211
+ - [Amazon Elasticsearch Service](#amazon-elasticsearch-service)
1212
+ - [Self-Hosted and Other](#other)
1213
+
1214
+ ### Elastic Cloud
1215
+
1216
+ Create an initializer `config/initializers/elasticsearch.rb` with:
1217
+
1218
+ ```ruby
1219
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1220
+ ```
1221
+
1222
+ Then deploy and reindex:
1223
+
1224
+ ```sh
1225
+ rake searchkick:reindex:all
1226
+ ```
1227
+
1038
1228
  ### Heroku
1039
1229
 
1040
- Choose an add-on: [SearchBox](https://elements.heroku.com/addons/searchbox), [Bonsai](https://elements.heroku.com/addons/bonsai), or [Elastic Cloud](https://elements.heroku.com/addons/foundelasticsearch).
1230
+ 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).
1231
+
1232
+ For Bonsai:
1233
+
1234
+ ```sh
1235
+ heroku addons:create bonsai
1236
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1237
+ ```
1238
+
1239
+ For SearchBox:
1041
1240
 
1042
1241
  ```sh
1043
- # SearchBox
1044
1242
  heroku addons:create searchbox:starter
1045
1243
  heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
1244
+ ```
1046
1245
 
1047
- # Bonsai
1048
- heroku addons:create bonsai
1049
- heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
1246
+ For Elastic Cloud (previously Found):
1050
1247
 
1051
- # Found
1248
+ ```sh
1052
1249
  heroku addons:create foundelasticsearch
1053
- heroku config:set ELASTICSEARCH_URL=`heroku config:get FOUNDELASTICSEARCH_URL`
1250
+ heroku addons:open foundelasticsearch
1054
1251
  ```
1055
1252
 
1056
- Then deploy and reindex:
1253
+ Visit the Shield page and reset your password. You’ll need to add the username and password to your url. Get the existing url with:
1057
1254
 
1058
1255
  ```sh
1059
- heroku run rake searchkick:reindex CLASS=Product
1256
+ heroku config:get FOUNDELASTICSEARCH_URL
1060
1257
  ```
1061
1258
 
1062
- ### Amazon Elasticsearch Service
1259
+ And add `elastic:password@` right after `https://` and add port `9243` at the end:
1063
1260
 
1064
- Include `elasticsearch 1.0.15` or greater in your Gemfile.
1261
+ ```sh
1262
+ heroku config:set ELASTICSEARCH_URL=https://elastic:password@12345.us-east-1.aws.found.io:9243
1263
+ ```
1065
1264
 
1066
- ```ruby
1067
- gem 'elasticsearch', '>= 1.0.15'
1265
+ Then deploy and reindex:
1266
+
1267
+ ```sh
1268
+ heroku run rake searchkick:reindex:all
1068
1269
  ```
1069
1270
 
1271
+ ### Amazon Elasticsearch Service
1272
+
1070
1273
  Create an initializer `config/initializers/elasticsearch.rb` with:
1071
1274
 
1072
1275
  ```ruby
1073
- ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com"
1276
+ ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com:443"
1074
1277
  ```
1075
1278
 
1076
- To use signed request, include in your Gemfile:
1279
+ To use signed requests, include in your Gemfile:
1077
1280
 
1078
1281
  ```ruby
1079
- gem 'faraday_middleware-aws-signers-v4'
1282
+ gem 'faraday_middleware-aws-sigv4'
1080
1283
  ```
1081
1284
 
1082
1285
  and add to your initializer:
@@ -1092,33 +1295,35 @@ Searchkick.aws_credentials = {
1092
1295
  Then deploy and reindex:
1093
1296
 
1094
1297
  ```sh
1095
- rake searchkick:reindex CLASS=Product
1298
+ rake searchkick:reindex:all
1096
1299
  ```
1097
1300
 
1098
- ### Other
1301
+ ### Self-Hosted and Other
1099
1302
 
1100
1303
  Create an initializer `config/initializers/elasticsearch.rb` with:
1101
1304
 
1102
1305
  ```ruby
1103
- ENV["ELASTICSEARCH_URL"] = "http://username:password@api.searchbox.io"
1306
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host:port"
1104
1307
  ```
1105
1308
 
1106
1309
  Then deploy and reindex:
1107
1310
 
1108
1311
  ```sh
1109
- rake searchkick:reindex CLASS=Product
1312
+ rake searchkick:reindex:all
1110
1313
  ```
1111
1314
 
1315
+ ### Data Protection
1316
+
1317
+ 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.
1318
+
1319
+ Bonsai, Elastic Cloud, and Amazon Elasticsearch all support encryption at rest and HTTPS.
1320
+
1112
1321
  ### Automatic Failover
1113
1322
 
1114
1323
  Create an initializer `config/initializers/elasticsearch.rb` with multiple hosts:
1115
1324
 
1116
1325
  ```ruby
1117
- ENV["ELASTICSEARCH_URL"] = "http://localhost:9200,http://localhost:9201"
1118
-
1119
- Searchkick.client_options = {
1120
- retry_on_failure: true
1121
- }
1326
+ ENV["ELASTICSEARCH_URL"] = "https://user:password@host1,https://user:password@host2"
1122
1327
  ```
1123
1328
 
1124
1329
  See [elasticsearch-transport](https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-transport) for a complete list of options.
@@ -1160,17 +1365,17 @@ gem 'typhoeus'
1160
1365
  To reduce log noise, create an initializer with:
1161
1366
 
1162
1367
  ```ruby
1163
- Ethon.logger = Logger.new("/dev/null")
1368
+ Ethon.logger = Logger.new(nil)
1164
1369
  ```
1165
1370
 
1166
1371
  If you run into issues on Windows, check out [this post](https://www.rastating.com/fixing-issues-in-typhoeus-and-httparty-on-windows/).
1167
1372
 
1168
1373
  ### Searchable Fields
1169
1374
 
1170
- 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.
1375
+ 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.
1171
1376
 
1172
1377
  ```ruby
1173
- class Product < ActiveRecord::Base
1378
+ class Product < ApplicationRecord
1174
1379
  searchkick searchable: [:name]
1175
1380
  end
1176
1381
  ```
@@ -1180,12 +1385,12 @@ end
1180
1385
  By default, all string fields are filterable (can be used in `where` option). Speed up indexing and reduce index size by only making some fields filterable.
1181
1386
 
1182
1387
  ```ruby
1183
- class Product < ActiveRecord::Base
1388
+ class Product < ApplicationRecord
1184
1389
  searchkick filterable: [:brand]
1185
1390
  end
1186
1391
  ```
1187
1392
 
1188
- **Note:** Non-string fields will always be filterable and should not be passed to this option.
1393
+ **Note:** Non-string fields are always filterable and should not be passed to this option.
1189
1394
 
1190
1395
  ### Parallel Reindexing
1191
1396
 
@@ -1214,10 +1419,10 @@ And use:
1214
1419
  Searchkick.reindex_status(index_name)
1215
1420
  ```
1216
1421
 
1217
- You can also have Searchkick wait for reindexing to complete [master]
1422
+ You can also have Searchkick wait for reindexing to complete
1218
1423
 
1219
1424
  ```ruby
1220
- Searchkick.reindex(async: {wait: true})
1425
+ Product.reindex(async: {wait: true})
1221
1426
  ```
1222
1427
 
1223
1428
  You can use [ActiveJob::TrafficControl](https://github.com/nickelser/activejob-traffic_control) to control concurrency. Install the gem:
@@ -1265,7 +1470,7 @@ Searchkick.redis = ConnectionPool.new { Redis.new }
1265
1470
  And ask your models to queue updates.
1266
1471
 
1267
1472
  ```ruby
1268
- class Product < ActiveRecord::Base
1473
+ class Product < ApplicationRecord
1269
1474
  searchkick callbacks: :queue
1270
1475
  end
1271
1476
  ```
@@ -1289,7 +1494,7 @@ For more tips, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/
1289
1494
  Searchkick supports [Elasticsearch’s routing feature](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
1290
1495
 
1291
1496
  ```ruby
1292
- class Business < ActiveRecord::Base
1497
+ class Business < ApplicationRecord
1293
1498
  searchkick routing: true
1294
1499
 
1295
1500
  def search_routing
@@ -1309,7 +1514,7 @@ Business.search "ice cream", routing: params[:city_id]
1309
1514
  Reindex a subset of attributes to reduce time spent generating search data and cut down on network traffic.
1310
1515
 
1311
1516
  ```ruby
1312
- class Product < ActiveRecord::Base
1517
+ class Product < ApplicationRecord
1313
1518
  def search_data
1314
1519
  {
1315
1520
  name: name
@@ -1336,7 +1541,7 @@ Product.reindex(:search_prices)
1336
1541
  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.
1337
1542
 
1338
1543
  ```ruby
1339
- class Product < ActiveRecord::Base
1544
+ class Product < ApplicationRecord
1340
1545
  def search_data
1341
1546
  {
1342
1547
  name: name
@@ -1354,7 +1559,7 @@ end
1354
1559
  Create a job to update the cache and reindex records with new conversions.
1355
1560
 
1356
1561
  ```ruby
1357
- class ReindexConversionsJob < ActiveJob::Base
1562
+ class ReindexConversionsJob < ApplicationJob
1358
1563
  def perform(class_name)
1359
1564
  # get records that have a recent conversion
1360
1565
  recently_converted_ids =
@@ -1401,12 +1606,10 @@ Searchkick makes it easy to use the Elasticsearch DSL on its own.
1401
1606
  Create a custom mapping:
1402
1607
 
1403
1608
  ```ruby
1404
- class Product < ActiveRecord::Base
1609
+ class Product < ApplicationRecord
1405
1610
  searchkick mappings: {
1406
- product: {
1407
- properties: {
1408
- name: {type: "string", analyzer: "keyword"}
1409
- }
1611
+ properties: {
1612
+ name: {type: "keyword"}
1410
1613
  }
1411
1614
  }
1412
1615
  end
@@ -1416,7 +1619,7 @@ end
1416
1619
  To keep the mappings and settings generated by Searchkick, use:
1417
1620
 
1418
1621
  ```ruby
1419
- class Product < ActiveRecord::Base
1622
+ class Product < ApplicationRecord
1420
1623
  searchkick merge_mappings: true, mappings: {...}
1421
1624
  end
1422
1625
  ```
@@ -1426,11 +1629,9 @@ end
1426
1629
  And use the `body` option to search:
1427
1630
 
1428
1631
  ```ruby
1429
- products = Product.search body: {match: {name: "milk"}}
1632
+ products = Product.search body: {query: {match: {name: "milk"}}}
1430
1633
  ```
1431
1634
 
1432
- **Note:** This replaces the entire body, so other options are ignored.
1433
-
1434
1635
  View the response with:
1435
1636
 
1436
1637
  ```ruby
@@ -1465,50 +1666,79 @@ Searchkick.client
1465
1666
  To batch search requests for performance, use:
1466
1667
 
1467
1668
  ```ruby
1468
- fresh_products = Product.search("fresh", execute: false)
1469
- frozen_products = Product.search("frozen", execute: false)
1470
- Searchkick.multi_search([fresh_products, frozen_products])
1669
+ products = Product.search("snacks", execute: false)
1670
+ coupons = Coupon.search("snacks", execute: false)
1671
+ Searchkick.multi_search([products, coupons])
1471
1672
  ```
1472
1673
 
1473
- Then use `fresh_products` and `frozen_products` as typical results.
1674
+ Then use `products` and `coupons` as typical results.
1474
1675
 
1475
- **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors. Also, if you use the `below` option for misspellings, misspellings will be disabled.
1676
+ **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors.
1476
1677
 
1477
- ## Multiple Indices
1678
+ ## Multiple Models
1478
1679
 
1479
- Search across multiple indices with:
1680
+ Search across multiple models with:
1480
1681
 
1481
1682
  ```ruby
1482
- Searchkick.search "milk", index_name: [Product, Category]
1683
+ Searchkick.search "milk", models: [Product, Category]
1483
1684
  ```
1484
1685
 
1485
- Boost specific indices with:
1686
+ Boost specific models with:
1486
1687
 
1487
1688
  ```ruby
1488
1689
  indices_boost: {Category => 2, Product => 1}
1489
1690
  ```
1490
1691
 
1491
- ## Nested Data
1692
+ ## Multi-Tenancy
1492
1693
 
1493
- To query nested data, use dot notation.
1694
+ 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.
1695
+
1696
+ ## Scroll API
1697
+
1698
+ 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.
1494
1699
 
1495
1700
  ```ruby
1496
- User.search "san", fields: ["address.city"], where: {"address.zip_code" => 12345}
1701
+ Product.search("*", scroll: "1m").scroll do |batch|
1702
+ # process batch ...
1703
+ end
1497
1704
  ```
1498
1705
 
1499
- ## Search Concepts
1706
+ You can also scroll batches manually.
1500
1707
 
1501
- ### Precision and Recall
1708
+ ```ruby
1709
+ products = Product.search "*", scroll: "1m"
1710
+ while products.any?
1711
+ # process batch ...
1502
1712
 
1503
- [Precision and recall](https://en.wikipedia.org/wiki/Precision_and_recall) are two key concepts in search (also known as *information retrieval*). To help illustrate, let’s walk through an example.
1713
+ products = products.scroll
1714
+ end
1715
+
1716
+ products.clear_scroll
1717
+ ```
1504
1718
 
1505
- You have a store with 16 types of apples. A user searches for `apples` gets 10 results. 8 of the results are for apples, and 2 are for apple juice.
1719
+ ## Deep Paging
1506
1720
 
1507
- **Precision** is the fraction of documents in the results that are relevant. There are 10 results and 8 are relevant, so precision is 80%.
1721
+ 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:
1508
1722
 
1509
- **Recall** is the fraction of relevant documents in the results out of all relevant documents. There are 16 apples and only 8 in the results, so recall is 50%.
1723
+ ```ruby
1724
+ class Product < ApplicationRecord
1725
+ searchkick deep_paging: true
1726
+ end
1727
+ ```
1510
1728
 
1511
- There’s typically a trade-off between the two. As you tweak your search to increase precision (not return irrelevant documents), there’s are greater chance a relevant document also isn’t returned, which decreases recall. The opposite also applies. As you try to increase recall (return a higher number of relevent documents), there’s a greater chance you also return an irrelevant document, decreasing precision.
1729
+ If you just need an accurate total count with Elasticsearch 7, you can instead use:
1730
+
1731
+ ```ruby
1732
+ Product.search("pears", body_options: {track_total_hits: true})
1733
+ ```
1734
+
1735
+ ## Nested Data
1736
+
1737
+ To query nested data, use dot notation.
1738
+
1739
+ ```ruby
1740
+ User.search "san", fields: ["address.city"], where: {"address.zip_code" => 12345}
1741
+ ```
1512
1742
 
1513
1743
  ## Reference
1514
1744
 
@@ -1517,8 +1747,6 @@ Reindex one record
1517
1747
  ```ruby
1518
1748
  product = Product.find(1)
1519
1749
  product.reindex
1520
- # or to reindex in the background
1521
- product.reindex_async
1522
1750
  ```
1523
1751
 
1524
1752
  Reindex multiple records
@@ -1542,7 +1770,7 @@ Product.search_index.clean_indices
1542
1770
  Use custom settings
1543
1771
 
1544
1772
  ```ruby
1545
- class Product < ActiveRecord::Base
1773
+ class Product < ApplicationRecord
1546
1774
  searchkick settings: {number_of_shards: 3}
1547
1775
  end
1548
1776
  ```
@@ -1550,7 +1778,7 @@ end
1550
1778
  Use a different index name
1551
1779
 
1552
1780
  ```ruby
1553
- class Product < ActiveRecord::Base
1781
+ class Product < ApplicationRecord
1554
1782
  searchkick index_name: "products_v2"
1555
1783
  end
1556
1784
  ```
@@ -1558,7 +1786,7 @@ end
1558
1786
  Use a dynamic index name
1559
1787
 
1560
1788
  ```ruby
1561
- class Product < ActiveRecord::Base
1789
+ class Product < ApplicationRecord
1562
1790
  searchkick index_name: -> { "#{name.tableize}-#{I18n.locale}" }
1563
1791
  end
1564
1792
  ```
@@ -1566,11 +1794,17 @@ end
1566
1794
  Prefix the index name
1567
1795
 
1568
1796
  ```ruby
1569
- class Product < ActiveRecord::Base
1797
+ class Product < ApplicationRecord
1570
1798
  searchkick index_prefix: "datakick"
1571
1799
  end
1572
1800
  ```
1573
1801
 
1802
+ For all models
1803
+
1804
+ ```ruby
1805
+ Searchkick.index_prefix = "datakick"
1806
+ ```
1807
+
1574
1808
  Use a different term for boosting by conversions
1575
1809
 
1576
1810
  ```ruby
@@ -1580,7 +1814,7 @@ Product.search("banana", conversions_term: "organic banana")
1580
1814
  Multiple conversion fields
1581
1815
 
1582
1816
  ```ruby
1583
- class Product < ActiveRecord::Base
1817
+ class Product < ApplicationRecord
1584
1818
  has_many :searches, class_name: "Searchjoy::Search"
1585
1819
 
1586
1820
  # searchkick also supports multiple "conversions" fields
@@ -1636,33 +1870,57 @@ Eager load associations
1636
1870
  Product.search "milk", includes: [:brand, :stores]
1637
1871
  ```
1638
1872
 
1639
- Eager load different associations by model [master]
1873
+ Eager load different associations by model
1874
+
1875
+ ```ruby
1876
+ Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
1877
+ ```
1878
+
1879
+ Run additional scopes on results
1880
+
1881
+ ```ruby
1882
+ Product.search "milk", scope_results: ->(r) { r.with_attached_images }
1883
+ ```
1884
+
1885
+ Specify default fields to search
1640
1886
 
1641
1887
  ```ruby
1642
- Searchkick.search("*", index_name: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
1888
+ class Product < ApplicationRecord
1889
+ searchkick default_fields: [:name]
1890
+ end
1643
1891
  ```
1644
1892
 
1645
1893
  Turn off special characters
1646
1894
 
1647
1895
  ```ruby
1648
- class Product < ActiveRecord::Base
1896
+ class Product < ApplicationRecord
1649
1897
  # A will not match Ä
1650
1898
  searchkick special_characters: false
1651
1899
  end
1652
1900
  ```
1653
1901
 
1654
- Use a different [similarity algorithm](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-similarity.html) for scoring
1902
+ Turn on stemming for conversions
1903
+
1904
+ ```ruby
1905
+ class Product < ApplicationRecord
1906
+ searchkick stem_conversions: true
1907
+ end
1908
+ ```
1909
+
1910
+ Make search case-sensitive
1655
1911
 
1656
1912
  ```ruby
1657
- class Product < ActiveRecord::Base
1658
- searchkick similarity: "classic"
1913
+ class Product < ApplicationRecord
1914
+ searchkick case_sensitive: true
1659
1915
  end
1660
1916
  ```
1661
1917
 
1918
+ **Note:** If misspellings are enabled (default), results with a single character case difference will match. Turn off misspellings if this is not desired.
1919
+
1662
1920
  Change import batch size
1663
1921
 
1664
1922
  ```ruby
1665
- class Product < ActiveRecord::Base
1923
+ class Product < ApplicationRecord
1666
1924
  searchkick batch_size: 200 # defaults to 1000
1667
1925
  end
1668
1926
  ```
@@ -1673,6 +1931,16 @@ Create index without importing
1673
1931
  Product.reindex(import: false)
1674
1932
  ```
1675
1933
 
1934
+ Use a different id
1935
+
1936
+ ```ruby
1937
+ class Product < ApplicationRecord
1938
+ def search_document_id
1939
+ custom_id
1940
+ end
1941
+ end
1942
+ ```
1943
+
1676
1944
  Lazy searching
1677
1945
 
1678
1946
  ```ruby
@@ -1686,10 +1954,18 @@ Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference
1686
1954
  Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
1687
1955
  ```
1688
1956
 
1957
+ Set options across all models
1958
+
1959
+ ```ruby
1960
+ Searchkick.model_options = {
1961
+ batch_size: 200
1962
+ }
1963
+ ```
1964
+
1689
1965
  Reindex conditionally
1690
1966
 
1691
1967
  ```ruby
1692
- class Product < ActiveRecord::Base
1968
+ class Product < ApplicationRecord
1693
1969
  searchkick callbacks: false
1694
1970
 
1695
1971
  # add the callbacks manually
@@ -1715,165 +1991,10 @@ Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
1715
1991
  Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1716
1992
  ```
1717
1993
 
1718
- ## Testing
1719
-
1720
- For performance, only enable Searchkick callbacks for the tests that need it.
1721
-
1722
- ### Minitest
1723
-
1724
- Add to your `test/test_helper.rb`:
1725
-
1726
- ```ruby
1727
- # reindex models
1728
- Product.reindex
1729
-
1730
- # and disable callbacks
1731
- Searchkick.disable_callbacks
1732
- ```
1733
-
1734
- And use:
1735
-
1736
- ```ruby
1737
- class ProductTest < Minitest::Test
1738
- def setup
1739
- Searchkick.enable_callbacks
1740
- end
1741
-
1742
- def teardown
1743
- Searchkick.disable_callbacks
1744
- end
1745
-
1746
- def test_search
1747
- Product.create!(name: "Apple")
1748
- Product.search_index.refresh
1749
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1750
- end
1751
- end
1752
- ```
1753
-
1754
- ### RSpec
1994
+ ## Elasticsearch 6 to 7 Upgrade
1755
1995
 
1756
- Add to your `spec/spec_helper.rb`:
1757
-
1758
- ```ruby
1759
- RSpec.configure do |config|
1760
- config.before(:suite) do
1761
- # reindex models
1762
- Product.reindex
1763
-
1764
- # and disable callbacks
1765
- Searchkick.disable_callbacks
1766
- end
1767
-
1768
- config.around(:each, search: true) do |example|
1769
- Searchkick.enable_callbacks
1770
- example.run
1771
- Searchkick.disable_callbacks
1772
- end
1773
- end
1774
- ```
1775
-
1776
- And use:
1777
-
1778
- ```ruby
1779
- describe Product, search: true do
1780
- it "searches" do
1781
- Product.create!(name: "Apple")
1782
- Product.search_index.refresh
1783
- assert_equal ["Apple"], Product.search("apple").map(&:name)
1784
- end
1785
- end
1786
- ```
1787
-
1788
- ### Factory Girl
1789
-
1790
- Use a trait and an after `create` hook for each indexed model:
1791
-
1792
- ```ruby
1793
- FactoryGirl.define do
1794
- factory :product do
1795
- # ...
1796
-
1797
- # Note: This should be the last trait in the list so `reindex` is called
1798
- # after all the other callbacks complete.
1799
- trait :reindex do
1800
- after(:create) do |product, _evaluator|
1801
- product.reindex(refresh: true)
1802
- end
1803
- end
1804
- end
1805
- end
1806
-
1807
- # use it
1808
- FactoryGirl.create(:product, :some_trait, :reindex, some_attribute: "foo")
1809
- ```
1810
-
1811
- ### Parallel Tests
1812
-
1813
- Set:
1814
-
1815
- ```ruby
1816
- Searchkick.index_suffix = ENV["TEST_ENV_NUMBER"]
1817
- ```
1818
-
1819
- ## Multi-Tenancy
1820
-
1821
- 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.
1822
-
1823
- ## Upgrading
1824
-
1825
- View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
1826
-
1827
- Important notes are listed below.
1828
-
1829
- ### 2.0.0
1830
-
1831
- - Added support for `reindex` on associations
1832
-
1833
- #### Breaking Changes
1834
-
1835
- - Removed support for Elasticsearch 1 as it reaches [end of life](https://www.elastic.co/support/eol)
1836
- - Removed facets, legacy options, and legacy methods
1837
- - Invalid options now throw an `ArgumentError`
1838
- - The `query` and `json` options have been removed in favor of `body`
1839
- - The `include` option has been removed in favor of `includes`
1840
- - The `personalize` option has been removed in favor of `boost_where`
1841
- - The `partial` option has been removed in favor of `operator`
1842
- - Renamed `select_v2` to `select` (legacy `select` no longer available)
1843
- - The `_all` field is disabled if `searchable` option is used (for performance)
1844
- - The `partial_reindex(:method_name)` method has been replaced with `reindex(:method_name)`
1845
- - The `unsearchable` and `only_analyzed` options have been removed in favor of `searchable` and `filterable`
1846
- - `load: false` no longer returns an array in Elasticsearch 2
1847
-
1848
- ### 1.0.0
1849
-
1850
- - Added support for Elasticsearch 2.0
1851
- - Facets are deprecated in favor of [aggregations](#aggregations) - see [how to upgrade](#moving-from-facets)
1852
-
1853
- #### Breaking Changes
1854
-
1855
- - **ActiveRecord 4.1+ and Mongoid 3+:** Attempting to reindex with a scope now throws a `Searchkick::DangerousOperation` error to keep your from accidentally recreating your index with only a few records.
1856
-
1857
- ```ruby
1858
- Product.where(color: "brandy").reindex # error!
1859
- ```
1860
-
1861
- If this is what you intend to do, use:
1862
-
1863
- ```ruby
1864
- Product.where(color: "brandy").reindex(accept_danger: true)
1865
- ```
1866
-
1867
- - Misspellings are enabled by default for [partial matches](#partial-matches). Use `misspellings: false` to disable.
1868
- - [Transpositions](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance) are enabled by default for misspellings. Use `misspellings: {transpositions: false}` to disable.
1869
-
1870
- ### 0.6.0 and 0.7.0
1871
-
1872
- If running Searchkick `0.6.0` or `0.7.0` and Elasticsearch `0.90`, we recommend upgrading to Searchkick `0.6.1` or `0.7.1` to fix an issue that causes downtime when reindexing.
1873
-
1874
- ### 0.3.0
1875
-
1876
- Before `0.3.0`, locations were indexed incorrectly. When upgrading, be sure to reindex immediately.
1996
+ 1. Install Searchkick 4
1997
+ 2. Upgrade your Elasticsearch cluster
1877
1998
 
1878
1999
  ## Elasticsearch Gotchas
1879
2000
 
@@ -1891,21 +2012,20 @@ Product.search_index.refresh
1891
2012
  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:
1892
2013
 
1893
2014
  ```ruby
1894
- class Product < ActiveRecord::Base
2015
+ class Product < ApplicationRecord
1895
2016
  searchkick settings: {number_of_shards: 1}
1896
2017
  end
1897
2018
  ```
1898
2019
 
1899
2020
  For convenience, this is set by default in the test environment.
1900
2021
 
1901
- ## Thanks
2022
+ ## History
1902
2023
 
1903
- 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).
2024
+ View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
1904
2025
 
1905
- ## Roadmap
2026
+ ## Thanks
1906
2027
 
1907
- - Reindex API
1908
- - Incorporate human eval
2028
+ 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).
1909
2029
 
1910
2030
  ## Contributing
1911
2031
 
@@ -1916,13 +2036,13 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
1916
2036
  - Write, clarify, or fix documentation
1917
2037
  - Suggest or add new features
1918
2038
 
1919
- If you’re looking for ideas, [try here](https://github.com/ankane/searchkick/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22).
1920
-
1921
- To get started with development and testing:
2039
+ To get started with development:
1922
2040
 
1923
2041
  ```sh
1924
2042
  git clone https://github.com/ankane/searchkick.git
1925
2043
  cd searchkick
1926
2044
  bundle install
1927
- rake test
2045
+ bundle exec rake test
1928
2046
  ```
2047
+
2048
+ Feel free to open an issue to get feedback on your idea before spending too much time on it.