searchkick_bharthur 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +44 -0
  4. data/CHANGELOG.md +360 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +1443 -0
  8. data/Rakefile +8 -0
  9. data/lib/searchkick/index.rb +662 -0
  10. data/lib/searchkick/logging.rb +185 -0
  11. data/lib/searchkick/middleware.rb +12 -0
  12. data/lib/searchkick/model.rb +105 -0
  13. data/lib/searchkick/query.rb +845 -0
  14. data/lib/searchkick/reindex_job.rb +26 -0
  15. data/lib/searchkick/reindex_v2_job.rb +23 -0
  16. data/lib/searchkick/results.rb +211 -0
  17. data/lib/searchkick/tasks.rb +33 -0
  18. data/lib/searchkick/version.rb +3 -0
  19. data/lib/searchkick.rb +159 -0
  20. data/searchkick.gemspec +28 -0
  21. data/test/aggs_test.rb +115 -0
  22. data/test/autocomplete_test.rb +65 -0
  23. data/test/boost_test.rb +144 -0
  24. data/test/callbacks_test.rb +27 -0
  25. data/test/ci/before_install.sh +21 -0
  26. data/test/dangerous_reindex_test.rb +27 -0
  27. data/test/facets_test.rb +90 -0
  28. data/test/gemfiles/activerecord31.gemfile +7 -0
  29. data/test/gemfiles/activerecord32.gemfile +7 -0
  30. data/test/gemfiles/activerecord40.gemfile +8 -0
  31. data/test/gemfiles/activerecord41.gemfile +8 -0
  32. data/test/gemfiles/activerecord50.gemfile +7 -0
  33. data/test/gemfiles/apartment.gemfile +8 -0
  34. data/test/gemfiles/mongoid2.gemfile +7 -0
  35. data/test/gemfiles/mongoid3.gemfile +6 -0
  36. data/test/gemfiles/mongoid4.gemfile +7 -0
  37. data/test/gemfiles/mongoid5.gemfile +7 -0
  38. data/test/gemfiles/nobrainer.gemfile +6 -0
  39. data/test/highlight_test.rb +63 -0
  40. data/test/index_test.rb +120 -0
  41. data/test/inheritance_test.rb +78 -0
  42. data/test/match_test.rb +227 -0
  43. data/test/misspellings_test.rb +46 -0
  44. data/test/model_test.rb +42 -0
  45. data/test/multi_search_test.rb +22 -0
  46. data/test/multi_tenancy_test.rb +22 -0
  47. data/test/order_test.rb +44 -0
  48. data/test/pagination_test.rb +53 -0
  49. data/test/query_test.rb +13 -0
  50. data/test/records_test.rb +8 -0
  51. data/test/reindex_job_test.rb +31 -0
  52. data/test/reindex_v2_job_test.rb +32 -0
  53. data/test/routing_test.rb +13 -0
  54. data/test/should_index_test.rb +32 -0
  55. data/test/similar_test.rb +28 -0
  56. data/test/sql_test.rb +196 -0
  57. data/test/suggest_test.rb +80 -0
  58. data/test/synonyms_test.rb +54 -0
  59. data/test/test_helper.rb +361 -0
  60. data/test/where_test.rb +171 -0
  61. metadata +231 -0
data/README.md ADDED
@@ -0,0 +1,1443 @@
1
+ # Searchkick
2
+
3
+ :rocket: Intelligent search made easy
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.
6
+
7
+ Searchkick handles:
8
+
9
+ - stemming - `tomatoes` matches `tomato`
10
+ - special characters - `jalapeno` matches `jalapeño`
11
+ - extra whitespace - `dishwasher` matches `dish washer`
12
+ - misspellings - `zuchini` matches `zucchini`
13
+ - custom synonyms - `qtip` matches `cotton swab`
14
+
15
+ Plus:
16
+
17
+ - query like SQL - no need to learn a new query language
18
+ - reindex without downtime
19
+ - easily personalize results for each user
20
+ - autocomplete
21
+ - “Did you mean” suggestions
22
+ - works with ActiveRecord, Mongoid, and NoBrainer
23
+
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
+ :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
27
+
28
+ [![Build Status](https://travis-ci.org/ankane/searchkick.svg?branch=master)](https://travis-ci.org/ankane/searchkick)
29
+
30
+ ## Get Started
31
+
32
+ [Install Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html). For Homebrew, use:
33
+
34
+ ```sh
35
+ brew install elasticsearch
36
+
37
+ # start the server
38
+ elasticsearch
39
+ ```
40
+
41
+ Add this line to your application’s Gemfile:
42
+
43
+ ```ruby
44
+ gem 'searchkick'
45
+ ```
46
+
47
+ For Elasticsearch 2.0, use the version `1.0` and above. For Elasticsearch 0.90, use version `0.6.3` and [this readme](https://github.com/ankane/searchkick/blob/v0.6.3/README.md).
48
+
49
+ Add searchkick to models you want to search.
50
+
51
+ ```ruby
52
+ class Product < ActiveRecord::Base
53
+ searchkick
54
+ end
55
+ ```
56
+
57
+ Add data to the search index.
58
+
59
+ ```ruby
60
+ Product.reindex
61
+ ```
62
+
63
+ And to query, use:
64
+
65
+ ```ruby
66
+ products = Product.search "apples"
67
+ products.each do |product|
68
+ puts product.name
69
+ end
70
+ ```
71
+
72
+ Searchkick supports the complete [Elasticsearch Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html). As your search becomes more advanced, we recommend you use the [Elasticsearch DSL](#advanced) for maximum flexibility.
73
+
74
+ ### Queries
75
+
76
+ Query like SQL
77
+
78
+ ```ruby
79
+ Product.search "apples", where: {in_stock: true}, limit: 10, offset: 50
80
+ ```
81
+
82
+ Search specific fields
83
+
84
+ ```ruby
85
+ fields: [:name, :brand]
86
+ ```
87
+
88
+ Where
89
+
90
+ ```ruby
91
+ where: {
92
+ expires_at: {gt: Time.now}, # lt, gte, lte also available
93
+ orders_count: 1..10, # equivalent to {gte: 1, lte: 10}
94
+ aisle_id: [25, 30], # in
95
+ store_id: {not: 2}, # not
96
+ aisle_id: {not: [25, 30]}, # not in
97
+ user_ids: {all: [1, 3]}, # all elements in array
98
+ category: /frozen .+/, # regexp
99
+ or: [
100
+ [{in_stock: true}, {backordered: true}]
101
+ ]
102
+ }
103
+ ```
104
+
105
+ Order
106
+
107
+ ```ruby
108
+ order: {_score: :desc} # most relevant first - default
109
+ ```
110
+
111
+ [All of these sort options are supported](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html)
112
+
113
+ Limit / offset
114
+
115
+ ```ruby
116
+ limit: 20, offset: 40
117
+ ```
118
+
119
+ ### Results
120
+
121
+ Searches return a `Searchkick::Results` object. This responds like an array to most methods.
122
+
123
+ ```ruby
124
+ results = Product.search("milk")
125
+ results.size
126
+ results.any?
127
+ results.each { |result| ... }
128
+ ```
129
+
130
+ Get total results
131
+
132
+ ```ruby
133
+ results.total_count
134
+ ```
135
+
136
+ Get the time the search took (in milliseconds)
137
+
138
+ ```ruby
139
+ results.took
140
+ ```
141
+
142
+ Get the full response from Elasticsearch
143
+
144
+ ```ruby
145
+ results.response
146
+ ```
147
+
148
+ ### Boosting
149
+
150
+ Boost important fields
151
+
152
+ ```ruby
153
+ fields: ["title^10", "description"]
154
+ ```
155
+
156
+ Boost by the value of a field (field must be numeric)
157
+
158
+ ```ruby
159
+ boost_by: [:orders_count] # give popular documents a little boost
160
+ boost_by: {orders_count: {factor: 10}} # default factor is 1
161
+ ```
162
+
163
+ Boost matching documents
164
+
165
+ ```ruby
166
+ boost_where: {user_id: 1}
167
+ boost_where: {user_id: {value: 1, factor: 100}} # default factor is 1000
168
+ boost_where: {user_id: [{value: 1, factor: 100}, {value: 2, factor: 200}]}
169
+ ```
170
+
171
+ [Conversions](#keep-getting-better) are also a great way to boost.
172
+
173
+ ### Get Everything
174
+
175
+ Use a `*` for the query.
176
+
177
+ ```ruby
178
+ Product.search "*"
179
+ ```
180
+
181
+ ### Pagination
182
+
183
+ Plays nicely with kaminari and will_paginate.
184
+
185
+ ```ruby
186
+ # controller
187
+ @products = Product.search "milk", page: params[:page], per_page: 20
188
+ ```
189
+
190
+ View with kaminari
191
+
192
+ ```erb
193
+ <%= paginate @products %>
194
+ ```
195
+
196
+ View with will_paginate
197
+
198
+ ```erb
199
+ <%= will_paginate @products %>
200
+ ```
201
+
202
+ ### Partial Matches
203
+
204
+ By default, results must match all words in the query.
205
+
206
+ ```ruby
207
+ Product.search "fresh honey" # fresh AND honey
208
+ ```
209
+
210
+ To change this, use:
211
+
212
+ ```ruby
213
+ Product.search "fresh honey", operator: "or" # fresh OR honey
214
+ ```
215
+
216
+ By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
217
+
218
+ ```ruby
219
+ class Product < ActiveRecord::Base
220
+ searchkick word_start: [:name]
221
+ end
222
+ ```
223
+
224
+ And to search (after you reindex):
225
+
226
+ ```ruby
227
+ Product.search "back", fields: [:name], match: :word_start
228
+ ```
229
+
230
+ Available options are:
231
+
232
+ ```ruby
233
+ :word # default
234
+ :word_start
235
+ :word_middle
236
+ :word_end
237
+ :text_start
238
+ :text_middle
239
+ :text_end
240
+ ```
241
+
242
+ ### Exact Matches
243
+
244
+ ```ruby
245
+ User.search params[:q], fields: [{email: :exact}, :name]
246
+ ```
247
+
248
+ ### Phrase Matches
249
+
250
+ ```ruby
251
+ User.search "fresh honey", match: :phrase
252
+ ```
253
+
254
+ ### Language
255
+
256
+ Searchkick defaults to English for stemming. To change this, use:
257
+
258
+ ```ruby
259
+ class Product < ActiveRecord::Base
260
+ searchkick language: "german"
261
+ end
262
+ ```
263
+
264
+ [See the list of stemmers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stemmer-tokenfilter.html)
265
+
266
+ ### Synonyms
267
+
268
+ ```ruby
269
+ class Product < ActiveRecord::Base
270
+ searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]]
271
+ # or
272
+ # searchkick synonyms: -> { CSV.read("/some/path/synonyms.csv") }
273
+ end
274
+ ```
275
+
276
+ Call `Product.reindex` after changing synonyms.
277
+
278
+ ### WordNet
279
+
280
+ Prepopulate English synonyms with the [WordNet database](https://en.wikipedia.org/wiki/WordNet).
281
+
282
+ 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.
283
+
284
+ ```sh
285
+ cd /tmp
286
+ curl -o wordnet.tar.gz http://wordnetcode.princeton.edu/3.0/WNprolog-3.0.tar.gz
287
+ tar -zxvf wordnet.tar.gz
288
+ mv prolog/wn_s.pl /var/lib
289
+ ```
290
+
291
+ Tell each model to use it:
292
+
293
+ ```ruby
294
+ class Product < ActiveRecord::Base
295
+ searchkick wordnet: true
296
+ end
297
+ ```
298
+
299
+ ### Misspellings
300
+
301
+ By default, Searchkick handles misspelled queries by returning results with an [edit distance](https://en.wikipedia.org/wiki/Levenshtein_distance) of one.
302
+
303
+ You can change this with:
304
+
305
+ ```ruby
306
+ Product.search "zucini", misspellings: {edit_distance: 2} # zucchini
307
+ ```
308
+
309
+ To improve performance for correctly spelled queries (which should be a majority for most applications), Searchkick can first perform a search without misspellings, and if there are too few results, perform another with them.
310
+
311
+ ```ruby
312
+ Product.search "zuchini", misspellings: {below: 5}
313
+ ```
314
+
315
+ If there are fewer than 5 results, a 2nd search is performed with misspellings enabled. The result of this query is returned.
316
+
317
+ Turn off misspellings with:
318
+
319
+ ```ruby
320
+ Product.search "zuchini", misspellings: false # no zucchini
321
+ ```
322
+
323
+ ### Emoji
324
+
325
+ Search :ice_cream::cake: and get `ice cream cake`!
326
+
327
+ Add this line to your application’s Gemfile:
328
+
329
+ ```ruby
330
+ gem 'gemoji-parser'
331
+ ```
332
+
333
+ And use:
334
+
335
+ ```ruby
336
+ Product.search "🍨🍰", emoji: true
337
+ ```
338
+
339
+ ### Indexing
340
+
341
+ Control what data is indexed with the `search_data` method. Call `Product.reindex` after changing this method.
342
+
343
+ ```ruby
344
+ class Product < ActiveRecord::Base
345
+ belongs_to :department
346
+
347
+ def search_data
348
+ {
349
+ name: name,
350
+ department_name: department.name,
351
+ on_sale: sale_price.present?
352
+ }
353
+ end
354
+ end
355
+ ```
356
+
357
+ Searchkick uses `find_in_batches` to import documents. To eager load associations, use the `search_import` scope.
358
+
359
+ ```ruby
360
+ class Product < ActiveRecord::Base
361
+ scope :search_import, -> { includes(:department) }
362
+ end
363
+ ```
364
+
365
+ By default, all records are indexed. To control which records are indexed, use the `should_index?` method.
366
+
367
+ ```ruby
368
+ class Product < ActiveRecord::Base
369
+ def should_index?
370
+ active # only index active records
371
+ end
372
+ end
373
+ ```
374
+
375
+ ### To Reindex, or Not to Reindex
376
+
377
+ #### Reindex
378
+
379
+ - when you install or upgrade searchkick
380
+ - change the `search_data` method
381
+ - change the `searchkick` method
382
+
383
+ #### No need to reindex
384
+
385
+ - App starts
386
+
387
+ ### Stay Synced
388
+
389
+ There are three strategies for keeping the index synced with your database.
390
+
391
+ 1. Immediate (default)
392
+
393
+ Anytime a record is inserted, updated, or deleted
394
+
395
+ 2. Asynchronous
396
+
397
+ Use background jobs for better performance
398
+
399
+ ```ruby
400
+ class Product < ActiveRecord::Base
401
+ searchkick callbacks: :async
402
+ end
403
+ ```
404
+
405
+ And [install Active Job](https://github.com/ankane/activejob_backport) for Rails 4.1 and below. Jobs are added to a queue named `searchkick`.
406
+
407
+ 3. Manual
408
+
409
+ Turn off automatic syncing
410
+
411
+ ```ruby
412
+ class Product < ActiveRecord::Base
413
+ searchkick callbacks: false
414
+ end
415
+ ```
416
+
417
+ You can also do bulk updates.
418
+
419
+ ```ruby
420
+ Searchkick.callbacks(:bulk) do
421
+ User.find_each(&:update_fields)
422
+ end
423
+ ```
424
+
425
+ Or temporarily skip updates.
426
+
427
+ ```ruby
428
+ Searchkick.callbacks(false) do
429
+ User.find_each(&:update_fields)
430
+ end
431
+ ```
432
+
433
+ #### Associations
434
+
435
+ Data is **not** automatically synced when an association is updated. If this is desired, add a callback to reindex:
436
+
437
+ ```ruby
438
+ class Image < ActiveRecord::Base
439
+ belongs_to :product
440
+
441
+ after_commit :reindex_product
442
+
443
+ def reindex_product
444
+ product.reindex # or reindex_async
445
+ end
446
+ end
447
+ ```
448
+
449
+ ### Analytics
450
+
451
+ We highly recommend tracking searches and conversions.
452
+
453
+ [Searchjoy](https://github.com/ankane/searchjoy) makes it easy.
454
+
455
+ ```ruby
456
+ Product.search "apple", track: {user_id: current_user.id}
457
+ ```
458
+
459
+ [See the docs](https://github.com/ankane/searchjoy) for how to install and use.
460
+
461
+ ### Keep Getting Better
462
+
463
+ 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.
464
+
465
+ 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.
466
+
467
+ You do **not** need to clean up the search queries. Searchkick automatically treats `apple` and `APPLES` the same.
468
+
469
+ Next, add conversions to the index.
470
+
471
+ ```ruby
472
+ class Product < ActiveRecord::Base
473
+ has_many :searches, class_name: "Searchjoy::Search"
474
+
475
+ searchkick conversions: "conversions" # name of field
476
+
477
+ def search_data
478
+ {
479
+ name: name,
480
+ conversions: searches.group(:query).uniq.count(:user_id)
481
+ # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
482
+ }
483
+ end
484
+ end
485
+ ```
486
+
487
+ Reindex and set up a cron job to add new conversions daily.
488
+
489
+ ```ruby
490
+ rake searchkick:reindex CLASS=Product
491
+ ```
492
+
493
+ ### Personalized Results
494
+
495
+ Order results differently for each user. For example, show a user’s previously purchased products before other results.
496
+
497
+ ```ruby
498
+ class Product < ActiveRecord::Base
499
+ def search_data
500
+ {
501
+ name: name,
502
+ orderer_ids: orders.pluck(:user_id) # boost this product for these users
503
+ }
504
+ end
505
+ end
506
+ ```
507
+
508
+ Reindex and search with:
509
+
510
+ ```ruby
511
+ Product.search "milk", boost_where: {orderer_ids: current_user.id}
512
+ ```
513
+
514
+ ### Instant Search / Autocomplete
515
+
516
+ Autocomplete predicts what a user will type, making the search experience faster and easier.
517
+
518
+ ![Autocomplete](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/autocomplete.png)
519
+
520
+ **Note:** 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).
521
+
522
+ 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.
523
+
524
+ ```ruby
525
+ class Book < ActiveRecord::Base
526
+ searchkick match: :word_start, searchable: [:title, :author]
527
+ end
528
+ ```
529
+
530
+ Reindex and search with:
531
+
532
+ ```ruby
533
+ Book.search "tipping poi"
534
+ ```
535
+
536
+ Typically, you want to use a JavaScript library like [typeahead.js](http://twitter.github.io/typeahead.js/) or [jQuery UI](http://jqueryui.com/autocomplete/).
537
+
538
+ #### Here’s how to make it work with Rails
539
+
540
+ First, add a route and controller action.
541
+
542
+ ```ruby
543
+ # app/controllers/books_controller.rb
544
+ class BooksController < ApplicationController
545
+ def autocomplete
546
+ render json: Book.search(params[:query], {
547
+ fields: ["title^5", "author"],
548
+ limit: 10,
549
+ load: false,
550
+ misspellings: {below: 5}
551
+ }).map(&:title)
552
+ end
553
+ end
554
+ ```
555
+
556
+ Then add the search box and JavaScript code to a view.
557
+
558
+ ```html
559
+ <input type="text" id="query" name="query" />
560
+
561
+ <script src="jquery.js"></script>
562
+ <script src="typeahead.js"></script>
563
+ <script>
564
+ $("#query").typeahead({
565
+ name: "book",
566
+ remote: "/books/autocomplete?query=%QUERY"
567
+ });
568
+ </script>
569
+ ```
570
+
571
+ ### Suggestions
572
+
573
+ ![Suggest](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/recursion.png)
574
+
575
+ ```ruby
576
+ class Product < ActiveRecord::Base
577
+ searchkick suggest: [:name] # fields to generate suggestions
578
+ end
579
+ ```
580
+
581
+ Reindex and search with:
582
+
583
+ ```ruby
584
+ products = Product.search "peantu butta", suggest: true
585
+ products.suggestions # ["peanut butter"]
586
+ ```
587
+
588
+ ### Aggregations
589
+
590
+ [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-facets.html) provide aggregated search data.
591
+
592
+ ![Aggregations](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/facets.png)
593
+
594
+ ```ruby
595
+ products = Product.search "chuck taylor", aggs: [:product_type, :gender, :brand]
596
+ products.aggs
597
+ ```
598
+
599
+ By default, `where` conditions apply to aggregations.
600
+
601
+ ```ruby
602
+ Product.search "wingtips", where: {color: "brandy"}, aggs: [:size]
603
+ # aggregations for brandy wingtips are returned
604
+ ```
605
+
606
+ Change this with:
607
+
608
+ ```ruby
609
+ Product.search "wingtips", where: {color: "brandy"}, aggs: [:size], smart_aggs: false
610
+ # aggregations for all wingtips are returned
611
+ ```
612
+
613
+ Set `where` conditions for each aggregation separately with:
614
+
615
+ ```ruby
616
+ Product.search "wingtips", aggs: {size: {where: {color: "brandy"}}}
617
+ ```
618
+
619
+ Limit
620
+
621
+ ```ruby
622
+ Product.search "apples", aggs: {store_id: {limit: 10}}
623
+ ```
624
+
625
+ Order
626
+
627
+ ```ruby
628
+ Product.search "wingtips", aggs: {color: {order: {"_term" => "asc"}}} # alphabetically
629
+ ```
630
+
631
+ [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)
632
+
633
+ Ranges
634
+
635
+ ```ruby
636
+ price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
637
+ Product.search "*", aggs: {price: {ranges: price_ranges}}
638
+ ```
639
+
640
+ Minimum document count
641
+
642
+ ```ruby
643
+ Product.search "apples", aggs: {store_id: {min_doc_count: 2}}
644
+ ```
645
+
646
+ #### Moving From Facets
647
+
648
+ 1. Replace `facets` with `aggs` in searches. **Note:** Stats facets are not supported at this time.
649
+
650
+ ```ruby
651
+ products = Product.search "chuck taylor", facets: [:brand]
652
+ # to
653
+ products = Product.search "chuck taylor", aggs: [:brand]
654
+ ```
655
+
656
+ 2. Replace the `facets` method with `aggs` for results.
657
+
658
+ ```ruby
659
+ products.facets
660
+ # to
661
+ products.aggs
662
+ ```
663
+
664
+ The keys in results differ slightly. Instead of:
665
+
666
+ ```json
667
+ {
668
+ "_type":"terms",
669
+ "missing":0,
670
+ "total":45,
671
+ "other":34,
672
+ "terms":[
673
+ {"term":14.0,"count":11}
674
+ ]
675
+ }
676
+ ```
677
+
678
+ You get:
679
+
680
+ ```json
681
+ {
682
+ "doc_count":45,
683
+ "doc_count_error_upper_bound":0,
684
+ "sum_other_doc_count":34,
685
+ "buckets":[
686
+ {"key":14.0,"doc_count":11}
687
+ ]
688
+ }
689
+ ```
690
+
691
+ Update your application to handle this.
692
+
693
+ 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`.
694
+
695
+ 4. If you have any range facets with dates, change the key from `ranges` to `date_ranges`.
696
+
697
+ ```ruby
698
+ facets: {date_field: {ranges: date_ranges}}
699
+ # to
700
+ aggs: {date_field: {date_ranges: date_ranges}}
701
+ ```
702
+
703
+ ### Facets [deprecated]
704
+
705
+ Facets have been deprecated in favor of aggregations as of Searchkick 1.0. See [how to upgrade](#moving-from-facets).
706
+
707
+ ```ruby
708
+ products = Product.search "chuck taylor", facets: [:product_type, :gender, :brand]
709
+ p products.facets
710
+ ```
711
+
712
+ By default, `where` conditions are not applied to facets (for backward compatibility).
713
+
714
+ ```ruby
715
+ Product.search "wingtips", where: {color: "brandy"}, facets: [:size]
716
+ # facets *not* filtered by color :(
717
+ ```
718
+
719
+ Change this with:
720
+
721
+ ```ruby
722
+ Product.search "wingtips", where: {color: "brandy"}, facets: [:size], smart_facets: true
723
+ ```
724
+
725
+ or set `where` conditions for each facet separately:
726
+
727
+ ```ruby
728
+ Product.search "wingtips", facets: {size: {where: {color: "brandy"}}}
729
+ ```
730
+
731
+ Limit
732
+
733
+ ```ruby
734
+ Product.search "apples", facets: {store_id: {limit: 10}}
735
+ ```
736
+
737
+ Ranges
738
+
739
+ ```ruby
740
+ price_ranges = [{to: 20}, {from: 20, to: 50}, {from: 50}]
741
+ Product.search "*", facets: {price: {ranges: price_ranges}}
742
+ ```
743
+
744
+ Use the `stats` option to get to max, min, mean, and total scores for each facet
745
+
746
+ ```ruby
747
+ Product.search "*", facets: {store_id: {stats: true}}
748
+ ```
749
+
750
+ ### Highlight
751
+
752
+ Specify which fields to index with highlighting.
753
+
754
+ ```ruby
755
+ class Product < ActiveRecord::Base
756
+ searchkick highlight: [:name]
757
+ end
758
+ ```
759
+
760
+ Highlight the search query in the results.
761
+
762
+ ```ruby
763
+ bands = Band.search "cinema", fields: [:name], highlight: true
764
+ ```
765
+
766
+ **Note:** The `fields` option is required, unless highlight options are given - see below.
767
+
768
+ View the highlighted fields with:
769
+
770
+ ```ruby
771
+ bands.with_details.each do |band, details|
772
+ puts details[:highlight][:name] # "Two Door <em>Cinema</em> Club"
773
+ end
774
+ ```
775
+
776
+ To change the tag, use:
777
+
778
+ ```ruby
779
+ Band.search "cinema", fields: [:name], highlight: {tag: "<strong>"}
780
+ ```
781
+
782
+ To highlight and search different fields, use:
783
+
784
+ ```ruby
785
+ Band.search "cinema", fields: [:name], highlight: {fields: [:description]}
786
+ ```
787
+
788
+ Additional options, including fragment size, can be specified for each field:
789
+
790
+ ```ruby
791
+ Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
792
+ ```
793
+
794
+ 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).
795
+
796
+ ### Similar Items
797
+
798
+ Find similar items.
799
+
800
+ ```ruby
801
+ product = Product.first
802
+ product.similar(fields: ["name"], where: {size: "12 oz"})
803
+ ```
804
+
805
+ ### Geospatial Searches
806
+
807
+ ```ruby
808
+ class City < ActiveRecord::Base
809
+ searchkick locations: ["location"]
810
+
811
+ def search_data
812
+ attributes.merge location: {lat: latitude, lon: longitude}
813
+ end
814
+ end
815
+ ```
816
+
817
+ Reindex and search with:
818
+
819
+ ```ruby
820
+ City.search "san", where: {location: {near: {lat: 37, lon: -114}, within: "100mi"}} # or 160km
821
+ ```
822
+
823
+ Bounded by a box
824
+
825
+ ```ruby
826
+ City.search "san", where: {location: {top_left: {lat: 38, lon: -123}, bottom_right: {lat: 37, lon: -122}}}
827
+ ```
828
+
829
+ ### Boost By Distance
830
+
831
+ Boost results by distance - closer results are boosted more
832
+
833
+ ```ruby
834
+ City.search "san", boost_by_distance: {field: :location, origin: {lat: 37, lon: -122}}
835
+ ```
836
+
837
+ Also supports [additional options](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#_decay_functions)
838
+
839
+ ```ruby
840
+ City.search "san", boost_by_distance: {field: :location, origin: {lat: 37, lon: -122}, function: :linear, scale: "30mi", decay: 0.5}
841
+ ```
842
+
843
+ ### Routing
844
+
845
+ Searchkick supports [Elasticsearch’s routing feature](https://www.elastic.co/blog/customizing-your-document-routing).
846
+
847
+ ```ruby
848
+ class Business < ActiveRecord::Base
849
+ searchkick routing: true
850
+
851
+ def search_routing
852
+ city_id
853
+ end
854
+ end
855
+ ```
856
+
857
+ Reindex and search with:
858
+
859
+ ```ruby
860
+ Business.search "ice cream", routing: params[:city_id]
861
+ ```
862
+
863
+ ## Inheritance
864
+
865
+ Searchkick supports single table inheritance.
866
+
867
+ ```ruby
868
+ class Dog < Animal
869
+ end
870
+ ```
871
+
872
+ The parent and child model can both reindex.
873
+
874
+ ```ruby
875
+ Animal.reindex
876
+ Dog.reindex # equivalent
877
+ ```
878
+
879
+ And to search, use:
880
+
881
+ ```ruby
882
+ Animal.search "*" # all animals
883
+ Dog.search "*" # just dogs
884
+ Animal.search "*", type: [Dog, Cat] # just cats and dogs
885
+ ```
886
+
887
+ **Note:** The `suggest` option retrieves suggestions from the parent at the moment.
888
+
889
+ ```ruby
890
+ Dog.search "airbudd", suggest: true # suggestions for all animals
891
+ ```
892
+
893
+ ## Debugging Queries
894
+
895
+ See how Elasticsearch scores your queries with:
896
+
897
+ ```ruby
898
+ Product.search("soap", explain: true).response
899
+ ```
900
+
901
+ See how Elasticsearch tokenizes your queries with:
902
+
903
+ ```ruby
904
+ Product.searchkick_index.tokens("Dish Washer Soap", analyzer: "default_index")
905
+ # ["dish", "dishwash", "washer", "washersoap", "soap"]
906
+
907
+ Product.searchkick_index.tokens("dishwasher soap", analyzer: "searchkick_search")
908
+ # ["dishwashersoap"] - no match
909
+
910
+ Product.searchkick_index.tokens("dishwasher soap", analyzer: "searchkick_search2")
911
+ # ["dishwash", "soap"] - match!!
912
+ ```
913
+
914
+ Partial matches
915
+
916
+ ```ruby
917
+ Product.searchkick_index.tokens("San Diego", analyzer: "searchkick_word_start_index")
918
+ # ["s", "sa", "san", "d", "di", "die", "dieg", "diego"]
919
+
920
+ Product.searchkick_index.tokens("dieg", analyzer: "searchkick_word_search")
921
+ # ["dieg"] - match!!
922
+ ```
923
+
924
+ See the [complete list of analyzers](lib/searchkick/index.rb#L209).
925
+
926
+ ## Deployment
927
+
928
+ Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
929
+
930
+ ### Heroku
931
+
932
+ 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).
933
+
934
+ ```sh
935
+ # SearchBox
936
+ heroku addons:create searchbox:starter
937
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get SEARCHBOX_URL`
938
+
939
+ # Bonsai
940
+ heroku addons:create bonsai
941
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get BONSAI_URL`
942
+
943
+ # Found
944
+ heroku addons:create foundelasticsearch
945
+ heroku config:set ELASTICSEARCH_URL=`heroku config:get FOUNDELASTICSEARCH_URL`
946
+ ```
947
+
948
+ Then deploy and reindex:
949
+
950
+ ```sh
951
+ heroku run rake searchkick:reindex CLASS=Product
952
+ ```
953
+
954
+ ### Amazon Elasticsearch Service
955
+
956
+ Include `elasticsearch 1.0.15` or greater in your Gemfile.
957
+
958
+ ```ruby
959
+ gem "elasticsearch", ">= 1.0.15"
960
+ ```
961
+
962
+ Create an initializer `config/initializers/elasticsearch.rb` with:
963
+
964
+ ```ruby
965
+ ENV["ELASTICSEARCH_URL"] = "https://es-domain-1234.us-east-1.es.amazonaws.com"
966
+ ```
967
+
968
+ To use signed request, include in your Gemfile:
969
+
970
+ ```ruby
971
+ gem 'faraday_middleware-aws-signers-v4'
972
+ ```
973
+
974
+ and add to your initializer:
975
+
976
+ ```ruby
977
+ require "faraday_middleware/aws_signers_v4"
978
+ Searchkick.client =
979
+ Elasticsearch::Client.new(
980
+ url: ENV["ELASTICSEARCH_URL"],
981
+ transport_options: {request: {timeout: 10}}
982
+ ) do |f|
983
+ f.request :aws_signers_v4, {
984
+ credentials: Aws::Credentials.new(ENV["AWS_ACCESS_KEY_ID"], ENV["AWS_SECRET_ACCESS_KEY"]),
985
+ service_name: "es",
986
+ region: "us-east-1"
987
+ }
988
+ end
989
+ ```
990
+
991
+ Then deploy and reindex:
992
+
993
+ ```sh
994
+ rake searchkick:reindex CLASS=Product
995
+ ```
996
+
997
+ ### Other
998
+
999
+ Create an initializer `config/initializers/elasticsearch.rb` with:
1000
+
1001
+ ```ruby
1002
+ ENV["ELASTICSEARCH_URL"] = "http://username:password@api.searchbox.io"
1003
+ ```
1004
+
1005
+ Then deploy and reindex:
1006
+
1007
+ ```sh
1008
+ rake searchkick:reindex CLASS=Product
1009
+ ```
1010
+
1011
+ ### Performance
1012
+
1013
+ For the best performance, add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile.
1014
+
1015
+ ```ruby
1016
+ gem 'typhoeus'
1017
+ ```
1018
+
1019
+ And create an initializer with:
1020
+
1021
+ ```ruby
1022
+ require "typhoeus/adapters/faraday"
1023
+ Ethon.logger = Logger.new("/dev/null")
1024
+ ```
1025
+
1026
+ **Note:** Typhoeus is not available for Windows.
1027
+
1028
+ ### Automatic Failover
1029
+
1030
+ Create an initializer `config/initializers/elasticsearch.rb` with multiple hosts:
1031
+
1032
+ ```ruby
1033
+ Searchkick.client = Elasticsearch::Client.new(hosts: ["localhost:9200", "localhost:9201"], retry_on_failure: true)
1034
+ ```
1035
+
1036
+ See [elasticsearch-transport](https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-transport) for a complete list of options.
1037
+
1038
+ ### Lograge
1039
+
1040
+ Add the following to `config/environments/production.rb`:
1041
+
1042
+ ```ruby
1043
+ config.lograge.custom_options = lambda do |event|
1044
+ options = {}
1045
+ options[:search] = event.payload[:searchkick_runtime] if event.payload[:searchkick_runtime].to_f > 0
1046
+ options
1047
+ end
1048
+ ```
1049
+
1050
+ See [Production Rails](https://github.com/ankane/production_rails) for other good practices.
1051
+
1052
+ ## Advanced
1053
+
1054
+ Prefer to use the [Elasticsearch DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-queries.html) but still want awesome features like zero-downtime reindexing?
1055
+
1056
+ ### Advanced Mapping
1057
+
1058
+ Create a custom mapping:
1059
+
1060
+ ```ruby
1061
+ class Product < ActiveRecord::Base
1062
+ searchkick mappings: {
1063
+ product: {
1064
+ properties: {
1065
+ name: {type: "string", analyzer: "keyword"}
1066
+ }
1067
+ }
1068
+ }
1069
+ end
1070
+ ```
1071
+
1072
+ To keep the mappings and settings generated by Searchkick, use:
1073
+
1074
+ ```ruby
1075
+ class Product < ActiveRecord::Base
1076
+ searchkick merge_mappings: true, mappings: {...}
1077
+ end
1078
+ ```
1079
+
1080
+ ### Advanced Search
1081
+
1082
+ And use the `body` option to search:
1083
+
1084
+ ```ruby
1085
+ products = Product.search body: {match: {name: "milk"}}
1086
+ ```
1087
+
1088
+ **Note:** This replaces the entire body, so other options are ignored.
1089
+
1090
+ View the response with:
1091
+
1092
+ ```ruby
1093
+ products.response
1094
+ ```
1095
+
1096
+ To modify the query generated by Searchkick, use:
1097
+
1098
+ ```ruby
1099
+ products =
1100
+ Product.search "apples" do |body|
1101
+ body[:query] = {match_all: {}}
1102
+ end
1103
+ ```
1104
+
1105
+ ### Multi Search
1106
+
1107
+ To batch search requests for performance, use:
1108
+
1109
+ ```ruby
1110
+ fresh_products = Product.search("fresh", execute: false)
1111
+ frozen_products = Product.search("frozen", execute: false)
1112
+ Searchkick.multi_search([fresh_products, frozen_products])
1113
+ ```
1114
+
1115
+ Then use `fresh_products` and `frozen_products` as typical results.
1116
+
1117
+ **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.
1118
+
1119
+ ## Reference
1120
+
1121
+ Reindex one record
1122
+
1123
+ ```ruby
1124
+ product = Product.find 10
1125
+ product.reindex
1126
+ # or to reindex in the background
1127
+ product.reindex_async
1128
+ ```
1129
+
1130
+ Reindex more than one record without recreating the index
1131
+
1132
+ ```ruby
1133
+ # do this ...
1134
+ some_company.products.each { |p| p.reindex }
1135
+ # or this ...
1136
+ Product.searchkick_index.import(some_company.products)
1137
+ # don't do the following as it will recreate the index with some_company's products only
1138
+ some_company.products.reindex
1139
+ ```
1140
+
1141
+ Reindex large set of records in batches
1142
+
1143
+ ```ruby
1144
+ Product.where("id > 100000").find_in_batches do |batch|
1145
+ Product.searchkick_index.import(batch)
1146
+ end
1147
+ ```
1148
+
1149
+ Remove old indices
1150
+
1151
+ ```ruby
1152
+ Product.clean_indices
1153
+ ```
1154
+
1155
+ Use custom settings
1156
+
1157
+ ```ruby
1158
+ class Product < ActiveRecord::Base
1159
+ searchkick settings: {number_of_shards: 3}
1160
+ end
1161
+ ```
1162
+
1163
+ Use a different index name
1164
+
1165
+ ```ruby
1166
+ class Product < ActiveRecord::Base
1167
+ searchkick index_name: "products_v2"
1168
+ end
1169
+ ```
1170
+
1171
+ Use a dynamic index name
1172
+
1173
+ ```ruby
1174
+ class Product < ActiveRecord::Base
1175
+ searchkick index_name: -> { "#{name.tableize}-#{I18n.locale}" }
1176
+ end
1177
+ ```
1178
+
1179
+ Prefix the index name
1180
+
1181
+ ```ruby
1182
+ class Product < ActiveRecord::Base
1183
+ searchkick index_prefix: "datakick"
1184
+ end
1185
+ ```
1186
+
1187
+ Change timeout
1188
+
1189
+ ```ruby
1190
+ Searchkick.timeout = 15 # defaults to 10
1191
+ ```
1192
+
1193
+ Set a lower timeout for searches
1194
+
1195
+ ```ruby
1196
+ Searchkick.search_timeout = 3
1197
+ ```
1198
+
1199
+ Change the search method name in `config/initializers/searchkick.rb`
1200
+
1201
+ ```ruby
1202
+ Searchkick.search_method_name = :lookup
1203
+ ```
1204
+
1205
+ Eager load associations
1206
+
1207
+ ```ruby
1208
+ Product.search "milk", include: [:brand, :stores]
1209
+ ```
1210
+
1211
+ Do not load models
1212
+
1213
+ ```ruby
1214
+ Product.search "milk", load: false
1215
+ ```
1216
+
1217
+ Turn off special characters
1218
+
1219
+ ```ruby
1220
+ class Product < ActiveRecord::Base
1221
+ # A will not match Ä
1222
+ searchkick special_characters: false
1223
+ end
1224
+ ```
1225
+
1226
+ Use [Okapi BM25](https://www.elastic.co/guide/en/elasticsearch/guide/current/pluggable-similarites.html) for ranking
1227
+
1228
+ ```ruby
1229
+ class Product < ActiveRecord::Base
1230
+ searchkick similarity: "BM25"
1231
+ end
1232
+ ```
1233
+
1234
+ Change import batch size
1235
+
1236
+ ```ruby
1237
+ class Product < ActiveRecord::Base
1238
+ searchkick batch_size: 200 # defaults to 1000
1239
+ end
1240
+ ```
1241
+
1242
+ Create index without importing
1243
+
1244
+ ```ruby
1245
+ Product.reindex(import: false)
1246
+ ```
1247
+
1248
+ Lazy searching
1249
+
1250
+ ```ruby
1251
+ products = Product.search("carrots", execute: false)
1252
+ products.each { ... } # search not executed until here
1253
+ ```
1254
+
1255
+ Make fields unsearchable but include in the source
1256
+
1257
+ ```ruby
1258
+ class Product < ActiveRecord::Base
1259
+ searchkick unsearchable: [:color]
1260
+ end
1261
+ ```
1262
+
1263
+ Reindex conditionally
1264
+
1265
+ **Note:** With ActiveRecord, use this feature with caution - [transaction rollbacks can cause data inconsistencies](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-model/README.md#custom-callbacks)
1266
+
1267
+ ```ruby
1268
+ class Product < ActiveRecord::Base
1269
+ searchkick callbacks: false
1270
+
1271
+ # add the callbacks manually
1272
+ after_save :reindex, if: proc{|model| model.name_changed? } # use your own condition
1273
+ after_destroy :reindex
1274
+ end
1275
+ ```
1276
+
1277
+ Search multiple models
1278
+
1279
+ ```ruby
1280
+ Searchkick.search "milk", index_name: [Product, Category]
1281
+ ```
1282
+
1283
+ Reindex all models - Rails only
1284
+
1285
+ ```sh
1286
+ rake searchkick:reindex:all
1287
+ ```
1288
+
1289
+ Turn on misspellings after a certain number of characters
1290
+
1291
+ ```ruby
1292
+ Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi
1293
+ ```
1294
+
1295
+ **Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off
1296
+
1297
+ ```ruby
1298
+ Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha
1299
+ ```
1300
+
1301
+ ## Large Data Sets
1302
+
1303
+ For large data sets, check out [Keeping Elasticsearch in Sync](https://www.elastic.co/blog/found-keeping-elasticsearch-in-sync). Searchkick will make this easy in the future.
1304
+
1305
+ ## Testing
1306
+
1307
+ This section could use some love.
1308
+
1309
+ ### RSpec
1310
+
1311
+ ```ruby
1312
+ describe Product do
1313
+ it "searches" do
1314
+ Product.reindex
1315
+ Product.searchkick_index.refresh # don't forget this
1316
+ # test goes here...
1317
+ end
1318
+ end
1319
+ ```
1320
+
1321
+ ### Factory Girl
1322
+
1323
+ ```ruby
1324
+ product = FactoryGirl.create(:product)
1325
+ product.reindex # don't forget this
1326
+ Product.searchkick_index.refresh # or this
1327
+ ```
1328
+
1329
+ ## Multi-Tenancy
1330
+
1331
+ 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.
1332
+
1333
+ ## Migrating from Tire
1334
+
1335
+ 1. Change `search` methods to `tire.search` and add index name in existing search calls
1336
+
1337
+ ```ruby
1338
+ Product.search "fruit"
1339
+ ```
1340
+
1341
+ should be replaced with
1342
+
1343
+ ```ruby
1344
+ Product.tire.search "fruit", index: "products"
1345
+ ```
1346
+
1347
+ 2. Replace tire mapping w/ searchkick method
1348
+
1349
+ ```ruby
1350
+ class Product < ActiveRecord::Base
1351
+ searchkick
1352
+ end
1353
+ ```
1354
+
1355
+ 3. Deploy and reindex
1356
+
1357
+ ```ruby
1358
+ rake searchkick:reindex CLASS=Product # or Product.reindex in the console
1359
+ ```
1360
+
1361
+ 4. Once it finishes, replace search calls w/ searchkick calls
1362
+
1363
+ ## Upgrading
1364
+
1365
+ View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
1366
+
1367
+ Important notes are listed below.
1368
+
1369
+ ### 1.0.0
1370
+
1371
+ - Added support for Elasticsearch 2.0
1372
+ - Facets are deprecated in favor of [aggregations](#aggregations) - see [how to upgrade](#moving-from-facets)
1373
+
1374
+ #### Breaking Changes
1375
+
1376
+ - **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.
1377
+
1378
+ ```ruby
1379
+ Product.where(color: "brandy").reindex # error!
1380
+ ```
1381
+
1382
+ If this is what you intend to do, use:
1383
+
1384
+ ```ruby
1385
+ Product.where(color: "brandy").reindex(accept_danger: true)
1386
+ ```
1387
+
1388
+ - Misspellings are enabled by default for [partial matches](#partial-matches). Use `misspellings: false` to disable.
1389
+ - [Transpositions](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance) are enabled by default for misspellings. Use `misspellings: {transpositions: false}` to disable.
1390
+
1391
+ ### 0.6.0 and 0.7.0
1392
+
1393
+ 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.
1394
+
1395
+ ### 0.3.0
1396
+
1397
+ Before `0.3.0`, locations were indexed incorrectly. When upgrading, be sure to reindex immediately.
1398
+
1399
+ ## Elasticsearch Gotchas
1400
+
1401
+ ### Inconsistent Scores
1402
+
1403
+ 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:
1404
+
1405
+ ```ruby
1406
+ class Product < ActiveRecord::Base
1407
+ searchkick settings: {number_of_shards: 1}
1408
+ end
1409
+ ```
1410
+
1411
+ For convenience, this is set by default in the test environment.
1412
+
1413
+ ## Thanks
1414
+
1415
+ 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).
1416
+
1417
+ ## Roadmap
1418
+
1419
+ - More features for large data sets
1420
+ - Improve section on testing
1421
+ - Semantic search features
1422
+ - Search multiple fields for different terms
1423
+ - Search across models
1424
+ - Search nested objects
1425
+ - Much finer customization
1426
+
1427
+ ## Contributing
1428
+
1429
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
1430
+
1431
+ - [Report bugs](https://github.com/ankane/searchkick/issues)
1432
+ - Fix bugs and [submit pull requests](https://github.com/ankane/searchkick/pulls)
1433
+ - Write, clarify, or fix documentation
1434
+ - Suggest or add new features
1435
+
1436
+ To get started with development and testing:
1437
+
1438
+ ```sh
1439
+ git clone https://github.com/ankane/searchkick.git
1440
+ cd searchkick
1441
+ bundle install
1442
+ rake test
1443
+ ```