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