searchkick 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +76 -7
- data/lib/searchkick.rb +2 -1
- data/lib/searchkick/model.rb +16 -3
- data/lib/searchkick/reindex.rb +61 -7
- data/lib/searchkick/results.rb +17 -0
- data/lib/searchkick/search.rb +94 -37
- data/lib/searchkick/version.rb +1 -1
- data/test/boost_test.rb +10 -0
- data/test/match_test.rb +27 -3
- data/test/sql_test.rb +19 -0
- data/test/suggest_test.rb +70 -0
- data/test/test_helper.rb +13 -5
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9afc0023095cdc587ed6df4faedb9c6ba0927b61
|
4
|
+
data.tar.gz: ff38cab705eb7d2692e81510ff237707820f1bda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af68e2a80fc2f8cead9075b8e2e2928ab81dcf98952949f2901d24f151de235b5e648316dce80c84b9197d6b549efc43fb9ed1f04e8f79a8a834e671e68171aa
|
7
|
+
data.tar.gz: b0a23d33695f2b82d84eb2152b07ca43d610ff1ba2200b917baaba61c3a7e8e7292161108270800afba3673484fefc8f19a7757ff2cbac0d131e7c36cccd3a5c
|
data/README.md
CHANGED
@@ -16,6 +16,9 @@ Plus:
|
|
16
16
|
|
17
17
|
- query like SQL - no need to learn a new query language
|
18
18
|
- reindex without downtime
|
19
|
+
- easily personalize results for each user
|
20
|
+
- autocomplete
|
21
|
+
- “Did you mean” suggestions
|
19
22
|
|
20
23
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com)
|
21
24
|
|
@@ -129,6 +132,24 @@ To change this, use:
|
|
129
132
|
Product.search "fresh honey", partial: true # fresh OR honey
|
130
133
|
```
|
131
134
|
|
135
|
+
### Autocomplete
|
136
|
+
|
137
|
+

|
138
|
+
|
139
|
+
You must specify which fields use this feature since this can increase the index size significantly. Don’t worry - this gives you blazing faster queries.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
class Website < ActiveRecord::Base
|
143
|
+
searchkick autocomplete: ["title"]
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
Reindex and search with:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Website.search "where", autocomplete: true
|
151
|
+
```
|
152
|
+
|
132
153
|
### Synonyms
|
133
154
|
|
134
155
|
```ruby
|
@@ -137,11 +158,11 @@ class Product < ActiveRecord::Base
|
|
137
158
|
end
|
138
159
|
```
|
139
160
|
|
140
|
-
|
161
|
+
Call `Product.reindex` after changing synonyms.
|
141
162
|
|
142
163
|
### Indexing
|
143
164
|
|
144
|
-
Control what data is indexed with the `search_data` method.
|
165
|
+
Control what data is indexed with the `search_data` method. Call `Product.reindex` after changing this method.
|
145
166
|
|
146
167
|
```ruby
|
147
168
|
class Product < ActiveRecord::Base
|
@@ -182,14 +203,19 @@ end
|
|
182
203
|
|
183
204
|
Add conversions to the index.
|
184
205
|
|
206
|
+
**Note**: You must specify the conversions field as of version `0.2.0`.
|
207
|
+
|
185
208
|
```ruby
|
186
209
|
class Product < ActiveRecord::Base
|
187
210
|
has_many :searches
|
188
211
|
|
212
|
+
searchkick conversions: "conversions" # name of field
|
213
|
+
|
189
214
|
def search_data
|
190
215
|
{
|
191
216
|
name: name,
|
192
217
|
conversions: searches.group("query").count
|
218
|
+
# {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
|
193
219
|
}
|
194
220
|
end
|
195
221
|
end
|
@@ -201,11 +227,56 @@ Reindex and set up a cron job to add new conversions daily.
|
|
201
227
|
rake searchkick:reindex CLASS=Product
|
202
228
|
```
|
203
229
|
|
230
|
+
### Personalized Results
|
231
|
+
|
232
|
+
Order results differently for each user. For example, show a user’s previously purchased products before other results.
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class Product < ActiveRecord::Base
|
236
|
+
searchkick personalize: "user_ids"
|
237
|
+
|
238
|
+
def search_data
|
239
|
+
{
|
240
|
+
name: name,
|
241
|
+
user_ids: orders.pluck(:user_id) # boost this product for these users
|
242
|
+
# [4, 8, 15, 16, 23, 42]
|
243
|
+
}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
Reindex and search with:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
Product.search "milk", user_id: 8
|
252
|
+
```
|
253
|
+
|
254
|
+
### Suggestions
|
255
|
+
|
256
|
+

|
257
|
+
|
258
|
+
```ruby
|
259
|
+
class Product < ActiveRecord::Base
|
260
|
+
searchkick suggest: ["name"] # fields to generate suggestions
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
Reindex and search with:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
products = Product.search "peantu butta", suggest: true
|
268
|
+
products.suggestions # ["peanut butter"]
|
269
|
+
```
|
270
|
+
|
204
271
|
### Facets
|
205
272
|
|
273
|
+
[Facets](http://www.elasticsearch.org/guide/reference/api/search/facets/) provide aggregated search data.
|
274
|
+
|
275
|
+

|
276
|
+
|
206
277
|
```ruby
|
207
|
-
|
208
|
-
p
|
278
|
+
products = Product.search "chuck taylor", facets: [:product_type, :gender, :brand]
|
279
|
+
p products.facets
|
209
280
|
```
|
210
281
|
|
211
282
|
Advanced
|
@@ -323,13 +394,11 @@ end
|
|
323
394
|
|
324
395
|
## Thanks
|
325
396
|
|
326
|
-
Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire)
|
397
|
+
Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire), Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884), and Alex Leschenko for [Elasticsearch autocomplete](https://github.com/leschenko/elasticsearch_autocomplete).
|
327
398
|
|
328
399
|
## TODO
|
329
400
|
|
330
|
-
- Custom results for each user
|
331
401
|
- Make Searchkick work with any language
|
332
|
-
- Built-in synonyms from WordNet
|
333
402
|
|
334
403
|
## Contributing
|
335
404
|
|
data/lib/searchkick.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
require "tire"
|
1
2
|
require "searchkick/version"
|
2
3
|
require "searchkick/reindex"
|
4
|
+
require "searchkick/results"
|
3
5
|
require "searchkick/search"
|
4
6
|
require "searchkick/model"
|
5
7
|
require "searchkick/tasks"
|
6
|
-
require "tire"
|
7
8
|
|
8
9
|
# TODO find better ActiveModel hook
|
9
10
|
ActiveModel::AttributeMethods::ClassMethods.send(:include, Searchkick::Model)
|
data/lib/searchkick/model.rb
CHANGED
@@ -3,7 +3,6 @@ module Searchkick
|
|
3
3
|
|
4
4
|
def searchkick(options = {})
|
5
5
|
@searchkick_options = options.dup
|
6
|
-
@searchkick_options[:conversions] = true if options[:conversions].nil?
|
7
6
|
|
8
7
|
class_eval do
|
9
8
|
extend Searchkick::Search
|
@@ -24,9 +23,23 @@ module Searchkick
|
|
24
23
|
|
25
24
|
def to_indexed_json
|
26
25
|
source = search_data
|
27
|
-
|
28
|
-
|
26
|
+
|
27
|
+
# stringify fields
|
28
|
+
source = source.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}
|
29
|
+
|
30
|
+
options = self.class.instance_variable_get("@searchkick_options")
|
31
|
+
|
32
|
+
# conversions
|
33
|
+
conversions_field = options[:conversions]
|
34
|
+
if conversions_field and source[conversions_field]
|
35
|
+
source[conversions_field] = source[conversions_field].map{|k, v| {query: k, count: v} }
|
29
36
|
end
|
37
|
+
|
38
|
+
# hack to prevent generator field doesn't exist error
|
39
|
+
(options[:suggest] || []).map(&:to_s).each do |field|
|
40
|
+
source[field] = "a" if !source[field]
|
41
|
+
end
|
42
|
+
|
30
43
|
source.to_json
|
31
44
|
end
|
32
45
|
end
|
data/lib/searchkick/reindex.rb
CHANGED
@@ -7,7 +7,8 @@ module Searchkick
|
|
7
7
|
new_index = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S")
|
8
8
|
index = Tire::Index.new(new_index)
|
9
9
|
|
10
|
-
index.create searchkick_index_options
|
10
|
+
success = index.create searchkick_index_options
|
11
|
+
raise index.response.to_s if !success
|
11
12
|
|
12
13
|
# use scope for import
|
13
14
|
scope = respond_to?(:search_import) ? search_import : self
|
@@ -67,6 +68,22 @@ module Searchkick
|
|
67
68
|
type: "custom",
|
68
69
|
tokenizer: "standard",
|
69
70
|
filter: ["standard", "lowercase", "asciifolding", "stop", "snowball"]
|
71
|
+
},
|
72
|
+
# https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
|
73
|
+
searchkick_autocomplete_index: {
|
74
|
+
type: "custom",
|
75
|
+
tokenizer: "searchkick_autocomplete_ngram",
|
76
|
+
filter: ["lowercase", "asciifolding"]
|
77
|
+
},
|
78
|
+
searchkick_autocomplete_search: {
|
79
|
+
type: "custom",
|
80
|
+
tokenizer: "keyword",
|
81
|
+
filter: ["lowercase", "asciifolding"]
|
82
|
+
},
|
83
|
+
searchkick_suggest_index: {
|
84
|
+
type: "custom",
|
85
|
+
tokenizer: "standard",
|
86
|
+
filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
|
70
87
|
}
|
71
88
|
},
|
72
89
|
filter: {
|
@@ -80,28 +97,45 @@ module Searchkick
|
|
80
97
|
token_separator: "",
|
81
98
|
output_unigrams: false,
|
82
99
|
output_unigrams_if_no_shingles: true
|
100
|
+
},
|
101
|
+
searchkick_suggest_shingle: {
|
102
|
+
type: "shingle",
|
103
|
+
max_shingle_size: 5
|
104
|
+
}
|
105
|
+
},
|
106
|
+
tokenizer: {
|
107
|
+
searchkick_autocomplete_ngram: {
|
108
|
+
type: "edgeNGram",
|
109
|
+
min_gram: 1,
|
110
|
+
max_gram: 50
|
83
111
|
}
|
84
112
|
}
|
85
113
|
}
|
86
114
|
}.merge(options[:settings] || {})
|
115
|
+
|
116
|
+
# synonyms
|
87
117
|
synonyms = options[:synonyms] || []
|
88
118
|
if synonyms.any?
|
89
119
|
settings[:analysis][:filter][:searchkick_synonym] = {
|
90
120
|
type: "synonym",
|
91
|
-
|
92
|
-
synonyms: synonyms.select{|s| s.size > 1 }.map{|s| "#{s[0..-2].join(",")} => #{s[-1]}" }
|
121
|
+
synonyms: synonyms.select{|s| s.size > 1 }.map{|s| s.join(",") }
|
93
122
|
}
|
94
123
|
# choosing a place for the synonym filter when stemming is not easy
|
95
124
|
# https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
|
96
125
|
# TODO use a snowball stemmer on synonyms when creating the token filter
|
97
|
-
|
126
|
+
|
127
|
+
# http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
|
128
|
+
# I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
|
129
|
+
# - Only apply the synonym expansion at index time
|
130
|
+
# - Don't have the synonym filter applied search
|
131
|
+
# - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
|
132
|
+
settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_synonym")
|
98
133
|
settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
|
99
|
-
settings[:analysis][:analyzer][:searchkick_search][:filter].insert(-4, "searchkick_synonym")
|
100
|
-
settings[:analysis][:analyzer][:searchkick_search][:filter] << "searchkick_synonym"
|
101
|
-
settings[:analysis][:analyzer][:searchkick_search2][:filter] << "searchkick_synonym"
|
102
134
|
end
|
103
135
|
|
104
136
|
mapping = {}
|
137
|
+
|
138
|
+
# conversions
|
105
139
|
if options[:conversions]
|
106
140
|
mapping[:conversions] = {
|
107
141
|
type: "nested",
|
@@ -112,6 +146,26 @@ module Searchkick
|
|
112
146
|
}
|
113
147
|
end
|
114
148
|
|
149
|
+
# autocomplete and suggest
|
150
|
+
autocomplete = (options[:autocomplete] || []).map(&:to_s)
|
151
|
+
suggest = (options[:suggest] || []).map(&:to_s)
|
152
|
+
(autocomplete + suggest).uniq.each do |field|
|
153
|
+
field_mapping = {
|
154
|
+
type: "multi_field",
|
155
|
+
fields: {
|
156
|
+
field => {type: "string", index: "not_analyzed"},
|
157
|
+
"analyzed" => {type: "string", index: "analyzed"}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
if autocomplete.include?(field)
|
161
|
+
field_mapping[:fields]["autocomplete"] = {type: "string", index: "analyzed", analyzer: "searchkick_autocomplete_index"}
|
162
|
+
end
|
163
|
+
if suggest.include?(field)
|
164
|
+
field_mapping[:fields]["suggest"] = {type: "string", index: "analyzed", analyzer: "searchkick_suggest_index"}
|
165
|
+
end
|
166
|
+
mapping[field] = field_mapping
|
167
|
+
end
|
168
|
+
|
115
169
|
mappings = {
|
116
170
|
document_type.to_sym => {
|
117
171
|
properties: mapping,
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Searchkick
|
2
|
+
class Results < Tire::Results::Collection
|
3
|
+
|
4
|
+
def suggestions
|
5
|
+
if @response["suggest"]
|
6
|
+
@response["suggest"].values.flat_map{|v| v.first["options"] }.sort_by{|o| -o["score"] }.map{|o| o["text"] }.uniq
|
7
|
+
else
|
8
|
+
raise "Pass `suggest: true` to the search method for suggestions"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# fixes deprecation warning
|
13
|
+
def __find_records_by_ids(klass, ids)
|
14
|
+
@options[:load] === true ? klass.find(ids) : klass.includes(@options[:load][:include]).find(ids)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/searchkick/search.rb
CHANGED
@@ -3,56 +3,93 @@ module Searchkick
|
|
3
3
|
|
4
4
|
def search(term, options = {})
|
5
5
|
term = term.to_s
|
6
|
-
fields =
|
6
|
+
fields =
|
7
|
+
if options[:fields]
|
8
|
+
if options[:autocomplete]
|
9
|
+
options[:fields].map{|f| "#{f}.autocomplete" }
|
10
|
+
else
|
11
|
+
options[:fields].map{|f| "#{f}.analyzed" }
|
12
|
+
end
|
13
|
+
else
|
14
|
+
if options[:autocomplete]
|
15
|
+
(@searchkick_options[:autocomplete] || []).map{|f| "#{f}.autocomplete" }
|
16
|
+
else
|
17
|
+
["_all"]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
7
21
|
operator = options[:partial] ? "or" : "and"
|
22
|
+
|
23
|
+
# model and eagar loading
|
8
24
|
load = options[:load].nil? ? true : options[:load]
|
9
25
|
load = (options[:include] ? {include: options[:include]} : true) if load
|
26
|
+
|
27
|
+
# pagination
|
10
28
|
page = options.has_key?(:page) ? [options[:page].to_i, 1].max : nil
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
29
|
+
per_page = options[:limit] || options[:per_page] || 100000
|
30
|
+
offset = options[:offset] || (page && (page - 1) * per_page)
|
31
|
+
index_name = options[:index_name] || index.name
|
32
|
+
|
33
|
+
conversions_field = @searchkick_options[:conversions]
|
34
|
+
personalize_field = @searchkick_options[:personalize]
|
35
|
+
|
36
|
+
# TODO lose Tire DSL for more flexibility
|
37
|
+
s =
|
38
|
+
Tire::Search::Search.new do
|
20
39
|
query do
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
custom_filters_score do
|
41
|
+
query do
|
42
|
+
boolean do
|
43
|
+
must do
|
44
|
+
if options[:autocomplete]
|
45
|
+
match fields, term, analyzer: "searchkick_autocomplete_search"
|
46
|
+
else
|
47
|
+
dis_max do
|
48
|
+
query do
|
49
|
+
match fields, term, use_dis_max: false, boost: 10, operator: operator, analyzer: "searchkick_search"
|
50
|
+
end
|
51
|
+
query do
|
52
|
+
match fields, term, use_dis_max: false, boost: 10, operator: operator, analyzer: "searchkick_search2"
|
53
|
+
end
|
54
|
+
query do
|
55
|
+
match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search"
|
56
|
+
end
|
57
|
+
query do
|
58
|
+
match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search2"
|
59
|
+
end
|
60
|
+
end
|
38
61
|
end
|
39
62
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
63
|
+
if conversions_field and options[:conversions] != false
|
64
|
+
should do
|
65
|
+
nested path: conversions_field, score_mode: "total" do
|
66
|
+
query do
|
67
|
+
custom_score script: "doc['count'].value" do
|
68
|
+
match "query", term
|
69
|
+
end
|
70
|
+
end
|
48
71
|
end
|
49
72
|
end
|
50
73
|
end
|
51
74
|
end
|
52
75
|
end
|
76
|
+
if options[:boost]
|
77
|
+
filter do
|
78
|
+
filter :exists, field: options[:boost]
|
79
|
+
script "log(doc['#{options[:boost]}'].value + 2.718281828)"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
if options[:user_id] and personalize_field
|
83
|
+
filter do
|
84
|
+
filter :term, personalize_field => options[:user_id]
|
85
|
+
boost 100
|
86
|
+
end
|
87
|
+
end
|
88
|
+
score_mode "total"
|
53
89
|
end
|
54
90
|
end
|
55
|
-
|
91
|
+
size per_page
|
92
|
+
from offset if offset
|
56
93
|
explain options[:explain] if options[:explain]
|
57
94
|
|
58
95
|
# order
|
@@ -143,7 +180,27 @@ module Searchkick
|
|
143
180
|
end
|
144
181
|
end
|
145
182
|
|
146
|
-
|
183
|
+
payload = s.to_hash
|
184
|
+
|
185
|
+
# suggestions
|
186
|
+
if options[:suggest]
|
187
|
+
suggest_fields = (@searchkick_options[:suggest] || []).map(&:to_s)
|
188
|
+
# intersection
|
189
|
+
suggest_fields = suggest_fields & options[:fields].map(&:to_s) if options[:fields]
|
190
|
+
if suggest_fields.any?
|
191
|
+
payload[:suggest] = {text: term}
|
192
|
+
suggest_fields.each do |field|
|
193
|
+
payload[:suggest][field] = {
|
194
|
+
phrase: {
|
195
|
+
field: "#{field}.suggest"
|
196
|
+
}
|
197
|
+
}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
search = Tire::Search::Search.new(index_name, load: load, payload: payload)
|
203
|
+
Searchkick::Results.new(search.json, search.options.merge(term: term))
|
147
204
|
end
|
148
205
|
|
149
206
|
end
|
data/lib/searchkick/version.rb
CHANGED
data/test/boost_test.rb
CHANGED
@@ -46,4 +46,14 @@ class TestBoost < Minitest::Unit::TestCase
|
|
46
46
|
assert_order "product", ["Product Conversions", "Product Boost"], boost: "orders_count"
|
47
47
|
end
|
48
48
|
|
49
|
+
def test_user_id
|
50
|
+
store [
|
51
|
+
{name: "Tomato A"},
|
52
|
+
{name: "Tomato B", user_ids: [1, 2, 3]},
|
53
|
+
{name: "Tomato C"},
|
54
|
+
{name: "Tomato D"}
|
55
|
+
]
|
56
|
+
assert_first "tomato", "Tomato B", user_id: 2
|
57
|
+
end
|
58
|
+
|
49
59
|
end
|
data/test/match_test.rb
CHANGED
@@ -60,14 +60,21 @@ class TestMatch < Minitest::Unit::TestCase
|
|
60
60
|
assert_search "fin", ["Finn"]
|
61
61
|
end
|
62
62
|
|
63
|
-
def
|
63
|
+
def test_edit_distance_two
|
64
64
|
store_names ["Bingo"]
|
65
65
|
assert_search "bin", []
|
66
|
+
assert_search "bingooo", []
|
67
|
+
assert_search "mango", []
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_edit_distance_one
|
71
|
+
store_names ["Bingo"]
|
66
72
|
assert_search "bing", ["Bingo"]
|
67
73
|
assert_search "bingoo", ["Bingo"]
|
68
|
-
assert_search "bingooo", []
|
69
74
|
assert_search "ringo", ["Bingo"]
|
70
|
-
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_edit_distance_long_word
|
71
78
|
store_names ["thisisareallylongword"]
|
72
79
|
assert_search "thisisareallylongwor", ["thisisareallylongword"] # missing letter
|
73
80
|
assert_search "thisisareelylongword", [] # edit distance = 2
|
@@ -110,4 +117,21 @@ class TestMatch < Minitest::Unit::TestCase
|
|
110
117
|
assert_search "almondmilks", ["Almond Milk"]
|
111
118
|
end
|
112
119
|
|
120
|
+
# autocomplete
|
121
|
+
|
122
|
+
def test_autocomplete
|
123
|
+
store_names ["Hummus"]
|
124
|
+
assert_search "hum", ["Hummus"], autocomplete: true
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_autocomplete_two_words
|
128
|
+
store_names ["Organic Hummus"]
|
129
|
+
assert_search "hum", [], autocomplete: true
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_autocomplete_fields
|
133
|
+
store_names ["Hummus"]
|
134
|
+
assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name]
|
135
|
+
end
|
136
|
+
|
113
137
|
end
|
data/test/sql_test.rb
CHANGED
@@ -7,6 +7,12 @@ class TestSql < Minitest::Unit::TestCase
|
|
7
7
|
assert_order "product", ["Product A", "Product B"], order: {name: :asc}, limit: 2
|
8
8
|
end
|
9
9
|
|
10
|
+
def test_no_limit
|
11
|
+
names = 20.times.map{|i| "Product #{i}" }
|
12
|
+
store_names names
|
13
|
+
assert_search "product", names
|
14
|
+
end
|
15
|
+
|
10
16
|
def test_offset
|
11
17
|
store_names ["Product A", "Product B", "Product C", "Product D"]
|
12
18
|
assert_order "product", ["Product C", "Product D"], order: {name: :asc}, offset: 2
|
@@ -81,6 +87,19 @@ class TestSql < Minitest::Unit::TestCase
|
|
81
87
|
assert_search "blue", ["red"], fields: ["color"]
|
82
88
|
end
|
83
89
|
|
90
|
+
def test_non_existent_field
|
91
|
+
store_names ["Milk"]
|
92
|
+
assert_search "milk", [], fields: ["not_here"]
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_fields_both_match
|
96
|
+
store [
|
97
|
+
{name: "Blue A", color: "red"},
|
98
|
+
{name: "Blue B", color: "light blue"}
|
99
|
+
]
|
100
|
+
assert_first "blue", "Blue B", fields: [:name, :color]
|
101
|
+
end
|
102
|
+
|
84
103
|
# load
|
85
104
|
|
86
105
|
def test_load_default
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestSuggest < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_basic
|
6
|
+
store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
|
7
|
+
assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark"
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_perfect
|
11
|
+
store_names ["Tiger Shark", "Great White Shark"]
|
12
|
+
assert_suggest "Tiger Shark", nil # no correction
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_phrase
|
16
|
+
store_names ["Big Tiger Shark", "Tiger Sharp Teeth", "Tiger Sharp Mind"]
|
17
|
+
assert_suggest "How to catch a big tiger shar", "how to catch a big tiger shark"
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_without_option
|
21
|
+
assert_raises(RuntimeError){ Product.search("hi").suggestions }
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_multiple_fields
|
25
|
+
store [
|
26
|
+
{name: "Shark", color: "Sharp"}
|
27
|
+
]
|
28
|
+
assert_suggest_all "shar", ["shark", "sharp"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_multiple_fields_highest_score_first
|
32
|
+
store [
|
33
|
+
{name: "Tiger Shark", color: "Sharp"}
|
34
|
+
]
|
35
|
+
assert_suggest "tiger shar", "tiger shark"
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_multiple_fields_same_value
|
39
|
+
store [
|
40
|
+
{name: "Shark", color: "Shark"}
|
41
|
+
]
|
42
|
+
assert_suggest_all "shar", ["shark"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_fields_option
|
46
|
+
store [
|
47
|
+
{name: "Shark", color: "Sharp"}
|
48
|
+
]
|
49
|
+
assert_suggest_all "shar", ["shark"], fields: [:name]
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_fields_option_multiple
|
53
|
+
store [
|
54
|
+
{name: "Shark"}
|
55
|
+
]
|
56
|
+
assert_suggest "shar", "shark", fields: [:name, :unknown]
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def assert_suggest(term, expected, options = {})
|
62
|
+
assert_equal expected, Product.search(term, options.merge(suggest: true)).suggestions.first
|
63
|
+
end
|
64
|
+
|
65
|
+
# any order
|
66
|
+
def assert_suggest_all(term, expected, options = {})
|
67
|
+
assert_equal expected.sort, Product.search(term, options.merge(suggest: true)).suggestions.sort
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -44,15 +44,19 @@ class Product < ActiveRecord::Base
|
|
44
44
|
["clorox", "bleach"],
|
45
45
|
["scallion", "greenonion"],
|
46
46
|
["saranwrap", "plasticwrap"],
|
47
|
-
["qtip", "
|
47
|
+
["qtip", "cottonswab"],
|
48
48
|
["burger", "hamburger"],
|
49
49
|
["bandaid", "bandag"]
|
50
|
-
]
|
50
|
+
],
|
51
|
+
autocomplete: [:name],
|
52
|
+
suggest: [:name, :color],
|
53
|
+
conversions: "conversions",
|
54
|
+
personalize: "user_ids"
|
51
55
|
|
52
|
-
attr_accessor :conversions
|
56
|
+
attr_accessor :conversions, :user_ids
|
53
57
|
|
54
58
|
def search_data
|
55
|
-
as_json.merge conversions: conversions
|
59
|
+
as_json.merge conversions: conversions, user_ids: user_ids
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
@@ -70,7 +74,7 @@ class MiniTest::Unit::TestCase
|
|
70
74
|
protected
|
71
75
|
|
72
76
|
def store(documents)
|
73
|
-
documents.each do |document|
|
77
|
+
documents.shuffle.each do |document|
|
74
78
|
Product.create!(document)
|
75
79
|
end
|
76
80
|
Product.index.refresh
|
@@ -89,4 +93,8 @@ class MiniTest::Unit::TestCase
|
|
89
93
|
assert_equal expected, Product.search(term, options).map(&:name)
|
90
94
|
end
|
91
95
|
|
96
|
+
def assert_first(term, expected, options = {})
|
97
|
+
assert_equal expected, Product.search(term, options).map(&:name).first
|
98
|
+
end
|
99
|
+
|
92
100
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: searchkick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-08-
|
11
|
+
date: 2013-08-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tire
|
@@ -109,6 +109,7 @@ files:
|
|
109
109
|
- lib/searchkick.rb
|
110
110
|
- lib/searchkick/model.rb
|
111
111
|
- lib/searchkick/reindex.rb
|
112
|
+
- lib/searchkick/results.rb
|
112
113
|
- lib/searchkick/search.rb
|
113
114
|
- lib/searchkick/tasks.rb
|
114
115
|
- lib/searchkick/version.rb
|
@@ -117,6 +118,7 @@ files:
|
|
117
118
|
- test/facets_test.rb
|
118
119
|
- test/match_test.rb
|
119
120
|
- test/sql_test.rb
|
121
|
+
- test/suggest_test.rb
|
120
122
|
- test/synonyms_test.rb
|
121
123
|
- test/test_helper.rb
|
122
124
|
homepage: https://github.com/ankane/searchkick
|
@@ -148,5 +150,6 @@ test_files:
|
|
148
150
|
- test/facets_test.rb
|
149
151
|
- test/match_test.rb
|
150
152
|
- test/sql_test.rb
|
153
|
+
- test/suggest_test.rb
|
151
154
|
- test/synonyms_test.rb
|
152
155
|
- test/test_helper.rb
|