searchkick 0.1.2 → 0.1.3
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 +10 -8
- data/lib/searchkick/reindex.rb +38 -12
- data/lib/searchkick/search.rb +14 -7
- data/lib/searchkick/version.rb +1 -1
- data/test/boost_test.rb +41 -0
- data/test/facets_test.rb +16 -0
- data/test/match_test.rb +113 -0
- data/test/sql_test.rb +106 -0
- data/test/synonyms_test.rb +45 -0
- data/test/test_helper.rb +63 -6
- metadata +12 -4
- data/test/searchkick_test.rb +0 -286
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5398d5720ac0d793c3e056ef46c93257da4e79a
|
4
|
+
data.tar.gz: 71d3ca9b8a853231cd5a285bb5b300912a7cbf5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ccea382ae95b340d7bb9df48cde62f84dbceca61451cfd5dce6ca0212e73b8da464a1d9342a20be26fe9b3ef24fadf9599fbd4b09715790c9ab37c6fd6d48a8
|
7
|
+
data.tar.gz: 72f1350835f6a260d7fd051e677f24c3cacee5a469035b8fbc5da80c9b55c123b88e36eab0f41ce1955dd08ca55db6e8762ac0e8b9916b7f38d21a9f4424840a
|
data/README.md
CHANGED
@@ -167,13 +167,11 @@ class Product < ActiveRecord::Base
|
|
167
167
|
end
|
168
168
|
```
|
169
169
|
|
170
|
-
###
|
170
|
+
### Keep Getting Better
|
171
171
|
|
172
|
-
Searchkick uses conversion data to learn what users are looking for.
|
172
|
+
Searchkick uses 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.
|
173
173
|
|
174
|
-
|
175
|
-
|
176
|
-
Next, track the conversions. The database works well for low volume, but feel free to use Redis or another datastore.
|
174
|
+
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.
|
177
175
|
|
178
176
|
```ruby
|
179
177
|
class Search < ActiveRecord::Base
|
@@ -200,7 +198,7 @@ end
|
|
200
198
|
Reindex and set up a cron job to add new conversions daily.
|
201
199
|
|
202
200
|
```ruby
|
203
|
-
|
201
|
+
rake searchkick:reindex CLASS=Product
|
204
202
|
```
|
205
203
|
|
206
204
|
### Facets
|
@@ -298,7 +296,9 @@ Product.search "milk", load: false
|
|
298
296
|
2. Replace tire mapping w/ searchkick method
|
299
297
|
|
300
298
|
```ruby
|
301
|
-
|
299
|
+
class Product < ActiveRecord::Base
|
300
|
+
searchkick
|
301
|
+
end
|
302
302
|
```
|
303
303
|
|
304
304
|
3. Deploy and reindex
|
@@ -323,10 +323,12 @@ end
|
|
323
323
|
|
324
324
|
## Thanks
|
325
325
|
|
326
|
-
Thanks to
|
326
|
+
Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire) and Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884).
|
327
327
|
|
328
328
|
## TODO
|
329
329
|
|
330
|
+
- Custom results for each user
|
331
|
+
- Make Searchkick work with any language
|
330
332
|
- Built-in synonyms from WordNet
|
331
333
|
|
332
334
|
## Contributing
|
data/lib/searchkick/reindex.rb
CHANGED
@@ -22,19 +22,20 @@ module Searchkick
|
|
22
22
|
end
|
23
23
|
|
24
24
|
a.indices.add new_index
|
25
|
-
a.save
|
25
|
+
response = a.save
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
if response.success?
|
28
|
+
old_indices.each do |index|
|
29
|
+
i = Tire::Index.new(index)
|
30
|
+
i.delete
|
31
|
+
end
|
30
32
|
end
|
31
33
|
else
|
32
|
-
|
33
|
-
|
34
|
-
Tire::Alias.create(name: alias_name, indices: [new_index])
|
34
|
+
tire.index.delete
|
35
|
+
response = Tire::Alias.create(name: alias_name, indices: [new_index])
|
35
36
|
end
|
36
37
|
|
37
|
-
|
38
|
+
response.success? || (raise response.to_s)
|
38
39
|
end
|
39
40
|
|
40
41
|
private
|
@@ -55,12 +56,12 @@ module Searchkick
|
|
55
56
|
tokenizer: "standard",
|
56
57
|
# synonym should come last, after stemming and shingle
|
57
58
|
# shingle must come before snowball
|
58
|
-
filter: ["standard", "lowercase", "asciifolding", "stop", "snowball", "searchkick_index_shingle"]
|
59
|
+
filter: ["standard", "lowercase", "asciifolding", "stop", "snowball", "searchkick_index_shingle", "snowball"]
|
59
60
|
},
|
60
61
|
searchkick_search: {
|
61
62
|
type: "custom",
|
62
63
|
tokenizer: "standard",
|
63
|
-
filter: ["standard", "lowercase", "asciifolding", "stop", "snowball", "searchkick_search_shingle"]
|
64
|
+
filter: ["standard", "lowercase", "asciifolding", "stop", "snowball", "searchkick_search_shingle", "snowball"]
|
64
65
|
},
|
65
66
|
searchkick_search2: {
|
66
67
|
type: "custom",
|
@@ -90,8 +91,12 @@ module Searchkick
|
|
90
91
|
ignore_case: true,
|
91
92
|
synonyms: synonyms.select{|s| s.size > 1 }.map{|s| "#{s[0..-2].join(",")} => #{s[-1]}" }
|
92
93
|
}
|
94
|
+
# choosing a place for the synonym filter when stemming is not easy
|
95
|
+
# https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
|
96
|
+
# TODO use a snowball stemmer on synonyms when creating the token filter
|
97
|
+
settings[:analysis][:analyzer][:default_index][:filter].insert(-4, "searchkick_synonym")
|
93
98
|
settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
|
94
|
-
settings[:analysis][:analyzer][:searchkick_search][:filter].insert(-
|
99
|
+
settings[:analysis][:analyzer][:searchkick_search][:filter].insert(-4, "searchkick_synonym")
|
95
100
|
settings[:analysis][:analyzer][:searchkick_search][:filter] << "searchkick_synonym"
|
96
101
|
settings[:analysis][:analyzer][:searchkick_search2][:filter] << "searchkick_synonym"
|
97
102
|
end
|
@@ -109,7 +114,28 @@ module Searchkick
|
|
109
114
|
|
110
115
|
mappings = {
|
111
116
|
document_type.to_sym => {
|
112
|
-
properties: mapping
|
117
|
+
properties: mapping,
|
118
|
+
# https://gist.github.com/kimchy/2898285
|
119
|
+
dynamic_templates: [
|
120
|
+
{
|
121
|
+
string_template: {
|
122
|
+
match: "*",
|
123
|
+
match_mapping_type: "string",
|
124
|
+
mapping: {
|
125
|
+
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
126
|
+
type: "multi_field",
|
127
|
+
fields: {
|
128
|
+
# analyzed field must be the default field for include_in_all
|
129
|
+
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
130
|
+
# however, we can include the not_analyzed field in _all
|
131
|
+
# and the _all index analyzer will take care of it
|
132
|
+
"{name}" => {type: "string", index: "not_analyzed"},
|
133
|
+
"analyzed" => {type: "string", index: "analyzed"}
|
134
|
+
}
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
]
|
113
139
|
}
|
114
140
|
}
|
115
141
|
|
data/lib/searchkick/search.rb
CHANGED
@@ -6,10 +6,17 @@ module Searchkick
|
|
6
6
|
fields = options[:fields] || ["_all"]
|
7
7
|
operator = options[:partial] ? "or" : "and"
|
8
8
|
load = options[:load].nil? ? true : options[:load]
|
9
|
-
load = (options[:include]
|
9
|
+
load = (options[:include] ? {include: options[:include]} : true) if load
|
10
|
+
page = options.has_key?(:page) ? [options[:page].to_i, 1].max : nil
|
11
|
+
tire_options = {
|
12
|
+
load: load,
|
13
|
+
page: page,
|
14
|
+
per_page: options[:limit] || options[:per_page] || 100000 # return all
|
15
|
+
}
|
16
|
+
tire_options[:index] = options[:index_name] if options[:index_name]
|
10
17
|
|
11
18
|
collection =
|
12
|
-
tire.search
|
19
|
+
tire.search tire_options do
|
13
20
|
query do
|
14
21
|
boolean do
|
15
22
|
must do
|
@@ -24,10 +31,10 @@ module Searchkick
|
|
24
31
|
match fields, term, boost: 10, operator: operator, analyzer: "searchkick_search2"
|
25
32
|
end
|
26
33
|
query do
|
27
|
-
match fields, term, use_dis_max: false, fuzziness:
|
34
|
+
match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search"
|
28
35
|
end
|
29
36
|
query do
|
30
|
-
match fields, term, use_dis_max: false, fuzziness:
|
37
|
+
match fields, term, use_dis_max: false, fuzziness: 1, max_expansions: 1, operator: operator, analyzer: "searchkick_search2"
|
31
38
|
end
|
32
39
|
end
|
33
40
|
end
|
@@ -36,7 +43,7 @@ module Searchkick
|
|
36
43
|
should do
|
37
44
|
nested path: "conversions", score_mode: "total" do
|
38
45
|
query do
|
39
|
-
custom_score script: "
|
46
|
+
custom_score script: "doc['count'].value" do
|
40
47
|
match "query", term
|
41
48
|
end
|
42
49
|
end
|
@@ -45,14 +52,14 @@ module Searchkick
|
|
45
52
|
end
|
46
53
|
end
|
47
54
|
end
|
48
|
-
size options[:limit] || 100000 # return all - like sql query
|
49
55
|
from options[:offset] if options[:offset]
|
50
56
|
explain options[:explain] if options[:explain]
|
51
57
|
|
52
58
|
# order
|
53
59
|
if options[:order]
|
60
|
+
order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
|
54
61
|
sort do
|
55
|
-
|
62
|
+
order.each do |k, v|
|
56
63
|
by k, v
|
57
64
|
end
|
58
65
|
end
|
data/lib/searchkick/version.rb
CHANGED
data/test/boost_test.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestBoost < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
# conversions
|
6
|
+
|
7
|
+
def test_conversions
|
8
|
+
store [
|
9
|
+
{name: "Tomato A", conversions: {"tomato" => 1}},
|
10
|
+
{name: "Tomato B", conversions: {"tomato" => 2}},
|
11
|
+
{name: "Tomato C", conversions: {"tomato" => 3}}
|
12
|
+
]
|
13
|
+
assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_conversions_stemmed
|
17
|
+
store [
|
18
|
+
{name: "Tomato A", conversions: {"tomato" => 1, "tomatos" => 1, "Tomatoes" => 1}},
|
19
|
+
{name: "Tomato B", conversions: {"tomato" => 2}}
|
20
|
+
]
|
21
|
+
assert_order "tomato", ["Tomato A", "Tomato B"]
|
22
|
+
end
|
23
|
+
|
24
|
+
# global boost
|
25
|
+
|
26
|
+
def test_boost
|
27
|
+
store [
|
28
|
+
{name: "Organic Tomato A"},
|
29
|
+
{name: "Tomato B", orders_count: 10}
|
30
|
+
]
|
31
|
+
assert_order "tomato", ["Tomato B", "Organic Tomato A"], boost: "orders_count"
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_boost_zero
|
35
|
+
store [
|
36
|
+
{name: "Zero Boost", orders_count: 0}
|
37
|
+
]
|
38
|
+
assert_order "zero", ["Zero Boost"], boost: "orders_count"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/test/facets_test.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestFacets < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_facets
|
6
|
+
store [
|
7
|
+
{name: "Product Show", store_id: 1, in_stock: true, color: "blue"},
|
8
|
+
{name: "Product Hide", store_id: 2, in_stock: false, color: "green"},
|
9
|
+
{name: "Product B", store_id: 2, in_stock: false, color: "red"}
|
10
|
+
]
|
11
|
+
assert_equal 2, Product.search("Product", facets: [:store_id]).facets["store_id"]["terms"].size
|
12
|
+
assert_equal 1, Product.search("Product", facets: {store_id: {where: {in_stock: true}}}).facets["store_id"]["terms"].size
|
13
|
+
assert_equal 1, Product.search("Product", facets: {store_id: {where: {in_stock: true, color: "blue"}}}).facets["store_id"]["terms"].size
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
data/test/match_test.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestMatch < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
# exact
|
6
|
+
|
7
|
+
def test_match
|
8
|
+
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
9
|
+
assert_search "milk", ["Milk", "Whole Milk", "Fat Free Milk"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_case
|
13
|
+
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
14
|
+
assert_search "MILK", ["Milk", "Whole Milk", "Fat Free Milk"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_cheese_space_in_index
|
18
|
+
store_names ["Pepper Jack Cheese Skewers"]
|
19
|
+
assert_search "pepperjack cheese skewers", ["Pepper Jack Cheese Skewers"]
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_cheese_space_in_query
|
23
|
+
store_names ["Pepperjack Cheese Skewers"]
|
24
|
+
assert_search "pepper jack cheese skewers", ["Pepperjack Cheese Skewers"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_middle_token
|
28
|
+
store_names ["Dish Washer Amazing Organic Soap"]
|
29
|
+
assert_search "dish soap", ["Dish Washer Amazing Organic Soap"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_percent
|
33
|
+
store_names ["1% Milk", "2% Milk", "Whole Milk"]
|
34
|
+
assert_search "1%", ["1% Milk"]
|
35
|
+
end
|
36
|
+
|
37
|
+
# ascii
|
38
|
+
|
39
|
+
def test_jalapenos
|
40
|
+
store_names ["Jalapeño"]
|
41
|
+
assert_search "jalapeno", ["Jalapeño"]
|
42
|
+
end
|
43
|
+
|
44
|
+
# stemming
|
45
|
+
|
46
|
+
def test_stemming
|
47
|
+
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
48
|
+
assert_search "milks", ["Milk", "Whole Milk", "Fat Free Milk"]
|
49
|
+
end
|
50
|
+
|
51
|
+
# fuzzy
|
52
|
+
|
53
|
+
def test_misspelling_sriracha
|
54
|
+
store_names ["Sriracha"]
|
55
|
+
assert_search "siracha", ["Sriracha"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_short_word
|
59
|
+
store_names ["Finn"]
|
60
|
+
assert_search "fin", ["Finn"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_edit_distance
|
64
|
+
store_names ["Bingo"]
|
65
|
+
assert_search "bin", []
|
66
|
+
assert_search "bing", ["Bingo"]
|
67
|
+
assert_search "bingoo", ["Bingo"]
|
68
|
+
assert_search "bingooo", []
|
69
|
+
assert_search "ringo", ["Bingo"]
|
70
|
+
assert_search "mango", []
|
71
|
+
store_names ["thisisareallylongword"]
|
72
|
+
assert_search "thisisareallylongwor", ["thisisareallylongword"] # missing letter
|
73
|
+
assert_search "thisisareelylongword", [] # edit distance = 2
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_misspelling_tabasco
|
77
|
+
store_names ["Tabasco"]
|
78
|
+
assert_search "tobasco", ["Tabasco"]
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_misspelling_zucchini
|
82
|
+
store_names ["Zucchini"]
|
83
|
+
assert_search "zuchini", ["Zucchini"]
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_misspelling_ziploc
|
87
|
+
store_names ["Ziploc"]
|
88
|
+
assert_search "zip lock", ["Ziploc"]
|
89
|
+
end
|
90
|
+
|
91
|
+
# spaces
|
92
|
+
|
93
|
+
def test_spaces_in_field
|
94
|
+
store_names ["Red Bull"]
|
95
|
+
assert_search "redbull", ["Red Bull"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_spaces_in_query
|
99
|
+
store_names ["Dishwasher"]
|
100
|
+
assert_search "dish washer", ["Dishwasher"]
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_spaces_three_words
|
104
|
+
store_names ["Dish Washer Soap", "Dish Washer"]
|
105
|
+
assert_search "dish washer soap", ["Dish Washer Soap"]
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_spaces_stemming
|
109
|
+
store_names ["Almond Milk"]
|
110
|
+
assert_search "almondmilks", ["Almond Milk"]
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/test/sql_test.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestSql < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_limit
|
6
|
+
store_names ["Product A", "Product B", "Product C", "Product D"]
|
7
|
+
assert_search "product", ["Product A", "Product B"], order: {name: :asc}, limit: 2
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_offset
|
11
|
+
store_names ["Product A", "Product B", "Product C", "Product D"]
|
12
|
+
assert_search "product", ["Product C", "Product D"], order: {name: :asc}, offset: 2
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_pagination
|
16
|
+
store_names ["Product A", "Product B", "Product C", "Product D", "Product E"]
|
17
|
+
assert_search "product", ["Product C", "Product D"], order: {name: :asc}, page: 2, per_page: 2
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_pagination_nil_page
|
21
|
+
store_names ["Product A", "Product B", "Product C", "Product D", "Product E"]
|
22
|
+
assert_search "product", ["Product A", "Product B"], order: {name: :asc}, page: nil, per_page: 2
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_where
|
26
|
+
now = Time.now
|
27
|
+
store [
|
28
|
+
{name: "Product A", store_id: 1, in_stock: true, backordered: true, created_at: now, orders_count: 4},
|
29
|
+
{name: "Product B", store_id: 2, in_stock: true, backordered: false, created_at: now - 1, orders_count: 3},
|
30
|
+
{name: "Product C", store_id: 3, in_stock: false, backordered: true, created_at: now - 2, orders_count: 2},
|
31
|
+
{name: "Product D", store_id: 4, in_stock: false, backordered: false, created_at: now - 3, orders_count: 1},
|
32
|
+
]
|
33
|
+
assert_search "product", ["Product A", "Product B"], where: {in_stock: true}
|
34
|
+
# date
|
35
|
+
assert_search "product", ["Product A"], where: {created_at: {gt: now - 1}}
|
36
|
+
assert_search "product", ["Product A", "Product B"], where: {created_at: {gte: now - 1}}
|
37
|
+
assert_search "product", ["Product D"], where: {created_at: {lt: now - 2}}
|
38
|
+
assert_search "product", ["Product C", "Product D"], where: {created_at: {lte: now - 2}}
|
39
|
+
# integer
|
40
|
+
assert_search "product", ["Product A"], where: {store_id: {lt: 2}}
|
41
|
+
assert_search "product", ["Product A", "Product B"], where: {store_id: {lte: 2}}
|
42
|
+
assert_search "product", ["Product D"], where: {store_id: {gt: 3}}
|
43
|
+
assert_search "product", ["Product C", "Product D"], where: {store_id: {gte: 3}}
|
44
|
+
# range
|
45
|
+
assert_search "product", ["Product A", "Product B"], where: {store_id: 1..2}
|
46
|
+
assert_search "product", ["Product A"], where: {store_id: 1...2}
|
47
|
+
assert_search "product", ["Product A", "Product B"], where: {store_id: [1, 2]}
|
48
|
+
assert_search "product", ["Product B", "Product C", "Product D"], where: {store_id: {not: 1}}
|
49
|
+
assert_search "product", ["Product C", "Product D"], where: {store_id: {not: [1, 2]}}
|
50
|
+
assert_search "product", ["Product A", "Product B", "Product C"], where: {or: [[{in_stock: true}, {store_id: 3}]]}
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_where_string
|
54
|
+
store [
|
55
|
+
{name: "Product A", color: "RED"}
|
56
|
+
]
|
57
|
+
assert_search "product", ["Product A"], where: {color: ["RED"]}
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_order_hash
|
61
|
+
store_names ["Product A", "Product B", "Product C", "Product D"]
|
62
|
+
assert_search "product", ["Product D", "Product C", "Product B", "Product A"], order: {name: :desc}
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_order_string
|
66
|
+
store_names ["Product A", "Product B", "Product C", "Product D"]
|
67
|
+
assert_search "product", ["Product A", "Product B", "Product C", "Product D"], order: "name"
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_partial
|
71
|
+
store_names ["Honey"]
|
72
|
+
assert_search "fresh honey", []
|
73
|
+
assert_search "fresh honey", ["Honey"], partial: true
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_fields
|
77
|
+
store [
|
78
|
+
{name: "red", color: "blue"},
|
79
|
+
{name: "blue", color: "red"}
|
80
|
+
]
|
81
|
+
assert_search "blue", ["red"], fields: ["color"]
|
82
|
+
end
|
83
|
+
|
84
|
+
# load
|
85
|
+
|
86
|
+
def test_load_default
|
87
|
+
store_names ["Product A"]
|
88
|
+
assert_kind_of Product, Product.search("product").first
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_load_false
|
92
|
+
store_names ["Product A"]
|
93
|
+
assert_kind_of Tire::Results::Item, Product.search("product", load: false).first
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_load_false_with_include
|
97
|
+
store_names ["Product A"]
|
98
|
+
assert_kind_of Tire::Results::Item, Product.search("product", load: false, include: [:store]).first
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_include
|
102
|
+
store_names ["Product A"]
|
103
|
+
assert Product.search("product", include: [:store]).first.association(:store).loaded?
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative "test_helper"
|
2
|
+
|
3
|
+
class TestSynonyms < Minitest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_bleach
|
6
|
+
store_names ["Clorox Bleach", "Kroger Bleach"]
|
7
|
+
assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_saran_wrap
|
11
|
+
store_names ["Saran Wrap", "Kroger Plastic Wrap"]
|
12
|
+
assert_search "saran wrap", ["Saran Wrap", "Kroger Plastic Wrap"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_burger_buns
|
16
|
+
store_names ["Hamburger Buns"]
|
17
|
+
assert_search "burger buns", ["Hamburger Buns"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_bandaids
|
21
|
+
store_names ["Band-Aid", "Kroger 12-Pack Bandages"]
|
22
|
+
assert_search "bandaids", ["Band-Aid", "Kroger 12-Pack Bandages"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_qtips
|
26
|
+
store_names ["Q Tips", "Kroger Cotton Swabs"]
|
27
|
+
assert_search "q tips", ["Q Tips", "Kroger Cotton Swabs"]
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_reverse
|
31
|
+
store_names ["Scallions"]
|
32
|
+
assert_search "green onions", ["Scallions"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_exact
|
36
|
+
store_names ["Green Onions", "Yellow Onions"]
|
37
|
+
assert_search "scallion", ["Green Onions"]
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_stemmed
|
41
|
+
store_names ["Green Onions", "Yellow Onions"]
|
42
|
+
assert_search "scallions", ["Green Onions"]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -24,12 +24,69 @@ ActiveRecord::Migration.create_table :products, :force => true do |t|
|
|
24
24
|
t.timestamps
|
25
25
|
end
|
26
26
|
|
27
|
-
ActiveRecord::Migration.create_table :
|
28
|
-
t.string :query
|
29
|
-
t.timestamp :searched_at
|
30
|
-
t.timestamp :converted_at
|
31
|
-
t.references :product
|
27
|
+
ActiveRecord::Migration.create_table :store, :force => true do |t|
|
32
28
|
end
|
33
29
|
|
34
30
|
File.delete("elasticsearch.log") if File.exists?("elasticsearch.log")
|
35
|
-
Tire.configure
|
31
|
+
Tire.configure do
|
32
|
+
logger "elasticsearch.log", :level => "debug"
|
33
|
+
pretty true
|
34
|
+
end
|
35
|
+
|
36
|
+
class Product < ActiveRecord::Base
|
37
|
+
belongs_to :store
|
38
|
+
|
39
|
+
searchkick \
|
40
|
+
settings: {
|
41
|
+
number_of_shards: 1
|
42
|
+
},
|
43
|
+
synonyms: [
|
44
|
+
["clorox", "bleach"],
|
45
|
+
["scallion", "greenonion"],
|
46
|
+
["saranwrap", "plasticwrap"],
|
47
|
+
["qtip", "cotton swab"],
|
48
|
+
["burger", "hamburger"],
|
49
|
+
["bandaid", "bandag"]
|
50
|
+
]
|
51
|
+
|
52
|
+
attr_accessor :conversions
|
53
|
+
|
54
|
+
def search_data
|
55
|
+
as_json.merge conversions: conversions
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Store < ActiveRecord::Base
|
60
|
+
end
|
61
|
+
|
62
|
+
Product.reindex
|
63
|
+
|
64
|
+
class MiniTest::Unit::TestCase
|
65
|
+
|
66
|
+
def setup
|
67
|
+
Product.destroy_all
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def store(documents)
|
73
|
+
documents.each do |document|
|
74
|
+
Product.create!(document)
|
75
|
+
end
|
76
|
+
Product.index.refresh
|
77
|
+
end
|
78
|
+
|
79
|
+
def store_names(names)
|
80
|
+
store names.map{|name| {name: name} }
|
81
|
+
end
|
82
|
+
|
83
|
+
# no order
|
84
|
+
def assert_search(term, expected, options = {})
|
85
|
+
assert_equal expected.sort, Product.search(term, options).map(&:name).sort
|
86
|
+
end
|
87
|
+
|
88
|
+
def assert_order(term, expected, options = {})
|
89
|
+
assert_equal expected, Product.search(term, options).map(&:name)
|
90
|
+
end
|
91
|
+
|
92
|
+
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.1.
|
4
|
+
version: 0.1.3
|
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-
|
11
|
+
date: 2013-08-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tire
|
@@ -113,7 +113,11 @@ files:
|
|
113
113
|
- lib/searchkick/tasks.rb
|
114
114
|
- lib/searchkick/version.rb
|
115
115
|
- searchkick.gemspec
|
116
|
-
- test/
|
116
|
+
- test/boost_test.rb
|
117
|
+
- test/facets_test.rb
|
118
|
+
- test/match_test.rb
|
119
|
+
- test/sql_test.rb
|
120
|
+
- test/synonyms_test.rb
|
117
121
|
- test/test_helper.rb
|
118
122
|
homepage: https://github.com/ankane/searchkick
|
119
123
|
licenses:
|
@@ -140,5 +144,9 @@ signing_key:
|
|
140
144
|
specification_version: 4
|
141
145
|
summary: Search made easy
|
142
146
|
test_files:
|
143
|
-
- test/
|
147
|
+
- test/boost_test.rb
|
148
|
+
- test/facets_test.rb
|
149
|
+
- test/match_test.rb
|
150
|
+
- test/sql_test.rb
|
151
|
+
- test/synonyms_test.rb
|
144
152
|
- test/test_helper.rb
|
data/test/searchkick_test.rb
DELETED
@@ -1,286 +0,0 @@
|
|
1
|
-
require "test_helper"
|
2
|
-
|
3
|
-
class Product < ActiveRecord::Base
|
4
|
-
has_many :searches
|
5
|
-
|
6
|
-
searchkick \
|
7
|
-
synonyms: [
|
8
|
-
["clorox", "bleach"],
|
9
|
-
["scallion", "greenonion"],
|
10
|
-
["saranwrap", "plasticwrap"],
|
11
|
-
["qtip", "cotton swab"],
|
12
|
-
["burger", "hamburger"],
|
13
|
-
["bandaid", "bandag"]
|
14
|
-
],
|
15
|
-
settings: {
|
16
|
-
number_of_shards: 1
|
17
|
-
}
|
18
|
-
|
19
|
-
def search_data
|
20
|
-
as_json.merge conversions: searches.group("query").count
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class Search < ActiveRecord::Base
|
25
|
-
belongs_to :product
|
26
|
-
end
|
27
|
-
|
28
|
-
Product.reindex
|
29
|
-
|
30
|
-
class TestSearchkick < Minitest::Unit::TestCase
|
31
|
-
|
32
|
-
def setup
|
33
|
-
Search.delete_all
|
34
|
-
Product.destroy_all
|
35
|
-
end
|
36
|
-
|
37
|
-
# exact
|
38
|
-
|
39
|
-
def test_match
|
40
|
-
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
41
|
-
assert_search "milk", ["Milk", "Whole Milk", "Fat Free Milk"]
|
42
|
-
end
|
43
|
-
|
44
|
-
def test_case
|
45
|
-
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
46
|
-
assert_search "MILK", ["Milk", "Whole Milk", "Fat Free Milk"]
|
47
|
-
end
|
48
|
-
|
49
|
-
def test_cheese_space_in_index
|
50
|
-
store_names ["Pepper Jack Cheese Skewers"]
|
51
|
-
assert_search "pepperjack cheese skewers", ["Pepper Jack Cheese Skewers"]
|
52
|
-
end
|
53
|
-
|
54
|
-
def test_cheese_space_in_query
|
55
|
-
store_names ["Pepperjack Cheese Skewers"]
|
56
|
-
assert_search "pepper jack cheese skewers", ["Pepperjack Cheese Skewers"]
|
57
|
-
end
|
58
|
-
|
59
|
-
def test_middle_token
|
60
|
-
store_names ["Dish Washer Amazing Organic Soap"]
|
61
|
-
assert_search "dish soap", ["Dish Washer Amazing Organic Soap"]
|
62
|
-
end
|
63
|
-
|
64
|
-
def test_percent
|
65
|
-
store_names ["1% Milk", "2% Milk", "Whole Milk"]
|
66
|
-
assert_search "1%", ["1% Milk"]
|
67
|
-
end
|
68
|
-
|
69
|
-
# ascii
|
70
|
-
|
71
|
-
def test_jalapenos
|
72
|
-
store_names ["Jalapeño"]
|
73
|
-
assert_search "jalapeno", ["Jalapeño"]
|
74
|
-
end
|
75
|
-
|
76
|
-
# stemming
|
77
|
-
|
78
|
-
def test_stemming
|
79
|
-
store_names ["Whole Milk", "Fat Free Milk", "Milk"]
|
80
|
-
assert_search "milks", ["Milk", "Whole Milk", "Fat Free Milk"]
|
81
|
-
end
|
82
|
-
|
83
|
-
# fuzzy
|
84
|
-
|
85
|
-
def test_misspelling_sriracha
|
86
|
-
store_names ["Sriracha"]
|
87
|
-
assert_search "siracha", ["Sriracha"]
|
88
|
-
end
|
89
|
-
|
90
|
-
def test_misspelling_tabasco
|
91
|
-
store_names ["Tabasco"]
|
92
|
-
assert_search "tobasco", ["Tabasco"]
|
93
|
-
end
|
94
|
-
|
95
|
-
def test_misspelling_zucchini
|
96
|
-
store_names ["Zucchini"]
|
97
|
-
assert_search "zuchini", ["Zucchini"]
|
98
|
-
end
|
99
|
-
|
100
|
-
def test_misspelling_ziploc
|
101
|
-
store_names ["Ziploc"]
|
102
|
-
assert_search "zip lock", ["Ziploc"]
|
103
|
-
end
|
104
|
-
|
105
|
-
# conversions
|
106
|
-
|
107
|
-
def test_conversions
|
108
|
-
store_conversions [
|
109
|
-
{name: "Tomato Sauce", conversions: [{query: "tomato sauce", count: 5}, {query: "tomato", count: 20}]},
|
110
|
-
{name: "Tomato Paste", conversions: []},
|
111
|
-
{name: "Tomatoes", conversions: [{query: "tomato", count: 10}, {query: "tomato sauce", count: 2}]}
|
112
|
-
]
|
113
|
-
assert_search "tomato", ["Tomato Sauce", "Tomatoes", "Tomato Paste"]
|
114
|
-
end
|
115
|
-
|
116
|
-
def test_conversions_stemmed
|
117
|
-
store_conversions [
|
118
|
-
{name: "Tomato A", conversions: [{query: "tomato", count: 2}, {query: "tomatos", count: 2}, {query: "Tomatoes", count: 2}]},
|
119
|
-
{name: "Tomato B", conversions: [{query: "tomato", count: 4}]}
|
120
|
-
]
|
121
|
-
assert_search "tomato", ["Tomato A", "Tomato B"]
|
122
|
-
end
|
123
|
-
|
124
|
-
# spaces
|
125
|
-
|
126
|
-
def test_spaces_in_field
|
127
|
-
store_names ["Red Bull"]
|
128
|
-
assert_search "redbull", ["Red Bull"]
|
129
|
-
end
|
130
|
-
|
131
|
-
def test_spaces_in_query
|
132
|
-
store_names ["Dishwasher Soap"]
|
133
|
-
assert_search "dish washer", ["Dishwasher Soap"]
|
134
|
-
end
|
135
|
-
|
136
|
-
def test_spaces_three_words
|
137
|
-
store_names ["Dish Washer Soap", "Dish Washer"]
|
138
|
-
assert_search "dish washer soap", ["Dish Washer Soap"]
|
139
|
-
end
|
140
|
-
|
141
|
-
def test_spaces_stemming
|
142
|
-
store_names ["Almond Milk"]
|
143
|
-
assert_search "almondmilks", ["Almond Milk"]
|
144
|
-
end
|
145
|
-
|
146
|
-
# keywords
|
147
|
-
|
148
|
-
def test_keywords
|
149
|
-
store_names ["Clorox Bleach", "Kroger Bleach", "Saran Wrap", "Kroger Plastic Wrap", "Hamburger Buns", "Band-Aid", "Kroger 12-Pack Bandages"]
|
150
|
-
assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"]
|
151
|
-
assert_search "saran wrap", ["Saran Wrap", "Kroger Plastic Wrap"]
|
152
|
-
assert_search "burger buns", ["Hamburger Buns"]
|
153
|
-
assert_search "bandaids", ["Band-Aid", "Kroger 12-Pack Bandages"]
|
154
|
-
end
|
155
|
-
|
156
|
-
def test_keywords_qtips
|
157
|
-
store_names ["Q Tips", "Kroger Cotton Swabs"]
|
158
|
-
assert_search "q tips", ["Q Tips", "Kroger Cotton Swabs"]
|
159
|
-
end
|
160
|
-
|
161
|
-
def test_keywords_reverse
|
162
|
-
store_names ["Scallions"]
|
163
|
-
assert_search "green onions", ["Scallions"]
|
164
|
-
end
|
165
|
-
|
166
|
-
def test_keywords_exact
|
167
|
-
store_names ["Green Onions", "Yellow Onions"]
|
168
|
-
assert_search "scallion", ["Green Onions"]
|
169
|
-
end
|
170
|
-
|
171
|
-
def test_keywords_stemmed
|
172
|
-
store_names ["Green Onions", "Yellow Onions"]
|
173
|
-
assert_search "scallions", ["Green Onions"]
|
174
|
-
end
|
175
|
-
|
176
|
-
# global boost
|
177
|
-
|
178
|
-
def test_boost
|
179
|
-
store [
|
180
|
-
{name: "Organic Tomato A"},
|
181
|
-
{name: "Tomato B", orders_count: 10}
|
182
|
-
]
|
183
|
-
assert_search "tomato", ["Tomato B", "Organic Tomato A"], boost: "orders_count"
|
184
|
-
end
|
185
|
-
|
186
|
-
def test_boost_zero
|
187
|
-
store [
|
188
|
-
{name: "Zero Boost", orders_count: 0}
|
189
|
-
]
|
190
|
-
assert_search "zero", ["Zero Boost"], boost: "orders_count"
|
191
|
-
end
|
192
|
-
|
193
|
-
# search method
|
194
|
-
|
195
|
-
def test_limit
|
196
|
-
store_names ["Product A", "Product B"]
|
197
|
-
assert_equal 1, Product.search("Product", limit: 1).size
|
198
|
-
end
|
199
|
-
|
200
|
-
def test_offset
|
201
|
-
store_names ["Product A", "Product B"]
|
202
|
-
assert_equal 1, Product.search("Product", offset: 1).size
|
203
|
-
end
|
204
|
-
|
205
|
-
def test_where
|
206
|
-
now = Time.now
|
207
|
-
store [
|
208
|
-
{name: "Product A", store_id: 1, in_stock: true, backordered: true, created_at: now, orders_count: 4},
|
209
|
-
{name: "Product B", store_id: 2, in_stock: true, backordered: false, created_at: now - 1, orders_count: 3},
|
210
|
-
{name: "Product C", store_id: 3, in_stock: false, backordered: true, created_at: now - 2, orders_count: 2},
|
211
|
-
{name: "Product D", store_id: 4, in_stock: false, backordered: false, created_at: now - 3, orders_count: 1},
|
212
|
-
]
|
213
|
-
assert_search "product", ["Product A", "Product B"], where: {in_stock: true}
|
214
|
-
# date
|
215
|
-
assert_search "product", ["Product A"], where: {created_at: {gt: now - 1}}
|
216
|
-
assert_search "product", ["Product A", "Product B"], where: {created_at: {gte: now - 1}}
|
217
|
-
assert_search "product", ["Product D"], where: {created_at: {lt: now - 2}}
|
218
|
-
assert_search "product", ["Product C", "Product D"], where: {created_at: {lte: now - 2}}
|
219
|
-
# integer
|
220
|
-
assert_search "product", ["Product A"], where: {store_id: {lt: 2}}
|
221
|
-
assert_search "product", ["Product A", "Product B"], where: {store_id: {lte: 2}}
|
222
|
-
assert_search "product", ["Product D"], where: {store_id: {gt: 3}}
|
223
|
-
assert_search "product", ["Product C", "Product D"], where: {store_id: {gte: 3}}
|
224
|
-
# range
|
225
|
-
assert_search "product", ["Product A", "Product B"], where: {store_id: 1..2}
|
226
|
-
assert_search "product", ["Product A"], where: {store_id: 1...2}
|
227
|
-
assert_search "product", ["Product A", "Product B"], where: {store_id: [1, 2]}
|
228
|
-
assert_search "product", ["Product B", "Product C", "Product D"], where: {store_id: {not: 1}}
|
229
|
-
assert_search "product", ["Product C", "Product D"], where: {store_id: {not: [1, 2]}}
|
230
|
-
assert_search "product", ["Product A", "Product B", "Product C"], where: {or: [[{in_stock: true}, {store_id: 3}]]}
|
231
|
-
end
|
232
|
-
|
233
|
-
def test_order
|
234
|
-
store_names ["Product A", "Product B", "Product C", "Product D"]
|
235
|
-
assert_search "product", ["Product D", "Product C", "Product B", "Product A"], order: {name: :desc}
|
236
|
-
end
|
237
|
-
|
238
|
-
def test_facets
|
239
|
-
store [
|
240
|
-
{name: "Product Show", store_id: 1, in_stock: true, color: "blue"},
|
241
|
-
{name: "Product Hide", store_id: 2, in_stock: false, color: "green"},
|
242
|
-
{name: "Product B", store_id: 2, in_stock: false, color: "red"}
|
243
|
-
]
|
244
|
-
assert_equal 2, Product.search("Product", facets: [:store_id]).facets["store_id"]["terms"].size
|
245
|
-
assert_equal 1, Product.search("Product", facets: {store_id: {where: {in_stock: true}}}).facets["store_id"]["terms"].size
|
246
|
-
assert_equal 1, Product.search("Product", facets: {store_id: {where: {in_stock: true, color: "blue"}}}).facets["store_id"]["terms"].size
|
247
|
-
end
|
248
|
-
|
249
|
-
def test_partial
|
250
|
-
store_names ["Honey"]
|
251
|
-
assert_search "fresh honey", []
|
252
|
-
assert_search "fresh honey", ["Honey"], partial: true
|
253
|
-
end
|
254
|
-
|
255
|
-
protected
|
256
|
-
|
257
|
-
def store(documents)
|
258
|
-
documents.each do |document|
|
259
|
-
Product.create!(document)
|
260
|
-
end
|
261
|
-
Product.index.refresh
|
262
|
-
end
|
263
|
-
|
264
|
-
def store_names(names)
|
265
|
-
store names.map{|name| {name: name} }
|
266
|
-
end
|
267
|
-
|
268
|
-
def store_conversions(documents)
|
269
|
-
documents.each do |document|
|
270
|
-
conversions = document.delete(:conversions)
|
271
|
-
product = Product.create!(document)
|
272
|
-
conversions.each do |c|
|
273
|
-
c[:count].times do
|
274
|
-
product.searches.create!(query: c[:query])
|
275
|
-
end
|
276
|
-
end
|
277
|
-
end
|
278
|
-
Product.reindex
|
279
|
-
Product.index.refresh
|
280
|
-
end
|
281
|
-
|
282
|
-
def assert_search(term, expected, options = {})
|
283
|
-
assert_equal expected, Product.search(term, options.merge(fields: [:name])).map(&:name)
|
284
|
-
end
|
285
|
-
|
286
|
-
end
|