searchkick_bharthur 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ ```