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