searchkick 0.4.2 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93348e8748debeefc0be073340665c5024b961af
4
- data.tar.gz: a4c73f4e8147ba2120d7e3388205fa056128d392
3
+ metadata.gz: fdf75cc3dce90b3109799a5ee97e30c319d450ac
4
+ data.tar.gz: 646d884beeb278d6a056f662d9a9df6de2e0b6a4
5
5
  SHA512:
6
- metadata.gz: 1f75f44631fee7348ff9aaf3d24f99b1954d5cef1868cc17f73c44c1dc17918f6eaa158dd35c46a582b31ec7d226384782421cd3c15f33b5a5e1b1f426266d90
7
- data.tar.gz: 6be3ea5ed0ac0002b4628ad3cc067c0352b4724b9318836e2affe2c05c167132113e00b1ea8515f9e806054a57e3d7cb98a6f3c904f549885960fad72caa4e98
6
+ metadata.gz: 3f97d141a413008ecb2ce03db8c29bd09aa0127af473f40428d0c21b350e4a0266931e347164f978c695a25ebd4c26b57cfd921ce39768355821387ffccad52e
7
+ data.tar.gz: 91b030868ca63ad185a67b91062d682a414d942f1e17b6af3aa1ff5dd6392831c240507cf0eb18bf025f4dc3522a40b042d411420608df85a2de6227a4be5378
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.5.0
2
+
3
+ - Better control over partial matches
4
+ - Added merge_mappings option
5
+ - Added batch_size option
6
+ - Fixed bug with nil where clauses
7
+
1
8
  ## 0.4.2
2
9
 
3
10
  - Added `should_index?` method to control which records are indexed
data/README.md CHANGED
@@ -148,6 +148,32 @@ To change this, use:
148
148
  Product.search "fresh honey", partial: true # fresh OR honey
149
149
  ```
150
150
 
151
+ By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
152
+
153
+ ```ruby
154
+ class Product < ActiveRecord::Base
155
+ searchkick word_start: [:name]
156
+ end
157
+ ```
158
+
159
+ And to search:
160
+
161
+ ```ruby
162
+ Product.search "back", fields: [{name: :word_start}]
163
+ ```
164
+
165
+ Available options are:
166
+
167
+ ```ruby
168
+ :word # default
169
+ :word_start
170
+ :word_middle
171
+ :word_end
172
+ :text_start
173
+ :text_middle
174
+ :text_end
175
+ ```
176
+
151
177
  ### Synonyms
152
178
 
153
179
  ```ruby
@@ -293,14 +319,14 @@ First, specify which fields use this feature. This is necessary since autocompl
293
319
 
294
320
  ```ruby
295
321
  class City < ActiveRecord::Base
296
- searchkick autocomplete: ["name"]
322
+ searchkick text_start: [:name]
297
323
  end
298
324
  ```
299
325
 
300
326
  Reindex and search with:
301
327
 
302
328
  ```ruby
303
- City.search "san fr", autocomplete: true
329
+ City.search "san fr", fields: [{name: :text_start}]
304
330
  ```
305
331
 
306
332
  Typically, you want to use a Javascript library like [typeahead.js](http://twitter.github.io/typeahead.js/) or [jQuery UI](http://jqueryui.com/autocomplete/).
@@ -314,7 +340,7 @@ First, add a controller action.
314
340
  class CitiesController < ApplicationController
315
341
 
316
342
  def autocomplete
317
- render json: City.search(params[:query], autocomplete: true, limit: 10).map(&:name)
343
+ render json: City.search(params[:query], fields: [{name: :text_start}], limit: 10).map(&:name)
318
344
  end
319
345
 
320
346
  end
@@ -530,6 +556,14 @@ And use the `query` option to search:
530
556
  Product.search query: {match: {name: "milk"}}
531
557
  ```
532
558
 
559
+ [master] To keep the mappings and settings generated by Searchkick, use:
560
+
561
+ ```ruby
562
+ class Product < ActiveRecord::Base
563
+ searchkick merge_mappings: true, mappings: {...}
564
+ end
565
+ ```
566
+
533
567
  ## Reference
534
568
 
535
569
  Searchkick requires Elasticsearch `0.90.0` or higher.
@@ -601,6 +635,14 @@ class Product < ActiveRecord::Base
601
635
  end
602
636
  ```
603
637
 
638
+ Change import batch size [master]
639
+
640
+ ```ruby
641
+ class Product < ActiveRecord::Base
642
+ searchkick batch_size: 200 # defaults to 1000
643
+ end
644
+ ```
645
+
604
646
  Reindex all models (Rails only)
605
647
 
606
648
  ```sh
@@ -57,11 +57,13 @@ module Searchkick
57
57
  private
58
58
 
59
59
  def searchkick_import(index)
60
+ batch_size = searchkick_options[:batch_size] || 1000
61
+
60
62
  # use scope for import
61
63
  scope = searchkick_klass
62
64
  scope = scope.search_import if scope.respond_to?(:search_import)
63
65
  if scope.respond_to?(:find_in_batches)
64
- scope.find_in_batches do |batch|
66
+ scope.find_in_batches batch_size: batch_size do |batch|
65
67
  index.import batch.select{|item| item.should_index? }
66
68
  end
67
69
  else
@@ -70,7 +72,7 @@ module Searchkick
70
72
  items = []
71
73
  scope.all.each do |item|
72
74
  items << item if item.should_index?
73
- if items.length % 1000 == 0
75
+ if items.length % batch_size == 0
74
76
  index.import items
75
77
  items = []
76
78
  end
@@ -82,7 +84,7 @@ module Searchkick
82
84
  def searchkick_index_options
83
85
  options = searchkick_options
84
86
 
85
- if options[:mappings]
87
+ if options[:mappings] and !options[:merge_mappings]
86
88
  settings = options[:settings] || {}
87
89
  mappings = options[:mappings]
88
90
  else
@@ -126,6 +128,41 @@ module Searchkick
126
128
  type: "custom",
127
129
  tokenizer: "standard",
128
130
  filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
131
+ },
132
+ searchkick_suggest_index: {
133
+ type: "custom",
134
+ tokenizer: "standard",
135
+ filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
136
+ },
137
+ searchkick_text_start_index: {
138
+ type: "custom",
139
+ tokenizer: "keyword",
140
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
141
+ },
142
+ searchkick_text_middle_index: {
143
+ type: "custom",
144
+ tokenizer: "keyword",
145
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
146
+ },
147
+ searchkick_text_end_index: {
148
+ type: "custom",
149
+ tokenizer: "keyword",
150
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
151
+ },
152
+ searchkick_word_start_index: {
153
+ type: "custom",
154
+ tokenizer: "standard",
155
+ filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
156
+ },
157
+ searchkick_word_middle_index: {
158
+ type: "custom",
159
+ tokenizer: "standard",
160
+ filter: ["lowercase", "asciifolding", "searchkick_ngram"]
161
+ },
162
+ searchkick_word_end_index: {
163
+ type: "custom",
164
+ tokenizer: "standard",
165
+ filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
129
166
  }
130
167
  },
131
168
  filter: {
@@ -143,6 +180,16 @@ module Searchkick
143
180
  searchkick_suggest_shingle: {
144
181
  type: "shingle",
145
182
  max_shingle_size: 5
183
+ },
184
+ searchkick_edge_ngram: {
185
+ type: "edgeNGram",
186
+ min_gram: 1,
187
+ max_gram: 50
188
+ },
189
+ searchkick_ngram: {
190
+ type: "nGram",
191
+ min_gram: 1,
192
+ max_gram: 50
146
193
  }
147
194
  },
148
195
  tokenizer: {
@@ -159,7 +206,7 @@ module Searchkick
159
206
  settings.merge!(number_of_shards: 1, number_of_replicas: 0)
160
207
  end
161
208
 
162
- settings.merge!(options[:settings] || {})
209
+ settings.deep_merge!(options[:settings] || {})
163
210
 
164
211
  # synonyms
165
212
  synonyms = options[:synonyms] || []
@@ -200,10 +247,12 @@ module Searchkick
200
247
  }
201
248
  end
202
249
 
203
- # autocomplete and suggest
204
- autocomplete = (options[:autocomplete] || []).map(&:to_s)
205
- suggest = (options[:suggest] || []).map(&:to_s)
206
- (autocomplete + suggest).uniq.each do |field|
250
+ mapping_options = Hash[
251
+ [:autocomplete, :suggest, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end]
252
+ .map{|type| [type, (options[type] || []).map(&:to_s)] }
253
+ ]
254
+
255
+ mapping_options.values.flatten.uniq.each do |field|
207
256
  field_mapping = {
208
257
  type: "multi_field",
209
258
  fields: {
@@ -213,12 +262,13 @@ module Searchkick
213
262
  # http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_fast_vector_highlighter
214
263
  }
215
264
  }
216
- if autocomplete.include?(field)
217
- field_mapping[:fields]["autocomplete"] = {type: "string", index: "analyzed", analyzer: "searchkick_autocomplete_index"}
218
- end
219
- if suggest.include?(field)
220
- field_mapping[:fields]["suggest"] = {type: "string", index: "analyzed", analyzer: "searchkick_suggest_index"}
265
+
266
+ mapping_options.each do |type, fields|
267
+ if fields.include?(field)
268
+ field_mapping[:fields][type] = {type: "string", index: "analyzed", analyzer: "searchkick_#{type}_index"}
269
+ end
221
270
  end
271
+
222
272
  mapping[field] = field_mapping
223
273
  end
224
274
 
@@ -253,7 +303,7 @@ module Searchkick
253
303
  }
254
304
  ]
255
305
  }
256
- }
306
+ }.deep_merge(options[:mappings] || {})
257
307
  end
258
308
 
259
309
  {
@@ -14,7 +14,10 @@ module Searchkick
14
14
  if options[:autocomplete]
15
15
  options[:fields].map{|f| "#{f}.autocomplete" }
16
16
  else
17
- options[:fields].map{|f| "#{f}.analyzed" }
17
+ options[:fields].map do |value|
18
+ k, v = value.is_a?(Hash) ? value.to_a.first : [value, :word]
19
+ "#{k}.#{v == :word ? "analyzed" : v}"
20
+ end
18
21
  end
19
22
  else
20
23
  if options[:autocomplete]
@@ -67,23 +70,37 @@ module Searchkick
67
70
  }
68
71
  }
69
72
  else
70
- shared_options = {
71
- fields: fields,
72
- query: term,
73
- use_dis_max: false,
74
- operator: operator
75
- }
76
- queries = [
77
- {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")},
78
- {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")}
79
- ]
80
- if options[:misspellings] != false
81
- distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1
82
- queries.concat [
83
- {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search")},
84
- {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2")}
85
- ]
73
+ queries = []
74
+ fields.each do |field|
75
+ if field == "_all" or field.end_with?(".analyzed")
76
+ shared_options = {
77
+ fields: [field],
78
+ query: term,
79
+ use_dis_max: false,
80
+ operator: operator
81
+ }
82
+ queries.concat [
83
+ {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")},
84
+ {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")}
85
+ ]
86
+ if options[:misspellings] != false
87
+ distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1
88
+ queries.concat [
89
+ {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search")},
90
+ {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2")}
91
+ ]
92
+ end
93
+ else
94
+ queries << {
95
+ multi_match: {
96
+ fields: [field],
97
+ query: term,
98
+ analyzer: "searchkick_autocomplete_search"
99
+ }
100
+ }
101
+ end
86
102
  end
103
+
87
104
  payload = {
88
105
  dis_max: {
89
106
  queries: queries
@@ -164,6 +181,17 @@ module Searchkick
164
181
  payload[:sort] = order
165
182
  end
166
183
 
184
+ term_filters =
185
+ proc do |field, value|
186
+ if value.is_a?(Array) # in query
187
+ {or: value.map{|v| term_filters.call(field, v) }}
188
+ elsif value.nil?
189
+ {missing: {"field" => field, existence: true, null_value: true}}
190
+ else
191
+ {term: {field => value}}
192
+ end
193
+ end
194
+
167
195
  # where
168
196
  where_filters =
169
197
  proc do |where|
@@ -181,9 +209,7 @@ module Searchkick
181
209
  value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
182
210
  end
183
211
 
184
- if value.is_a?(Array) # in query
185
- filters << {terms: {field => value}}
186
- elsif value.is_a?(Hash)
212
+ if value.is_a?(Hash)
187
213
  if value[:near]
188
214
  filters << {
189
215
  geo_distance: {
@@ -206,11 +232,7 @@ module Searchkick
206
232
 
207
233
  value.each do |op, op_value|
208
234
  if op == :not # not equal
209
- if op_value.is_a?(Array)
210
- filters << {not: {terms: {field => op_value}}}
211
- else
212
- filters << {not: {term: {field => op_value}}}
213
- end
235
+ filters << {not: term_filters.call(field, op_value)}
214
236
  elsif op == :all
215
237
  filters << {terms: {field => op_value, execution: "and"}}
216
238
  else
@@ -231,11 +253,7 @@ module Searchkick
231
253
  end
232
254
  end
233
255
  else
234
- if value.nil?
235
- filters << {missing: {"field" => field, existence: true, null_value: true}}
236
- else
237
- filters << {term: {field => value}}
238
- end
256
+ filters << term_filters.call(field, value)
239
257
  end
240
258
  end
241
259
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -17,4 +17,34 @@ class TestAutocomplete < Minitest::Unit::TestCase
17
17
  assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name]
18
18
  end
19
19
 
20
+ def test_text_start
21
+ store_names ["Where in the World is Carmen San Diego?"]
22
+ assert_search "whe", ["Where in the World is Carmen San Diego?"], fields: [{name: :text_start}]
23
+ end
24
+
25
+ def test_text_middle
26
+ store_names ["Where in the World is Carmen San Diego?"]
27
+ assert_search "n the wor", ["Where in the World is Carmen San Diego?"], fields: [{name: :text_middle}]
28
+ end
29
+
30
+ def test_text_end
31
+ store_names ["Where in the World is Carmen San Diego?"]
32
+ assert_search "ego?", ["Where in the World is Carmen San Diego?"], fields: [{name: :text_end}]
33
+ end
34
+
35
+ def test_word_start
36
+ store_names ["Where in the World is Carmen San Diego?"]
37
+ assert_search "car", ["Where in the World is Carmen San Diego?"], fields: [{name: :word_start}]
38
+ end
39
+
40
+ def test_word_middle
41
+ store_names ["Where in the World is Carmen San Diego?"]
42
+ assert_search "orl", ["Where in the World is Carmen San Diego?"], fields: [{name: :word_middle}]
43
+ end
44
+
45
+ def test_word_end
46
+ store_names ["Where in the World is Carmen San Diego?"]
47
+ assert_search "men", ["Where in the World is Carmen San Diego?"], fields: [{name: :word_end}]
48
+ end
49
+
20
50
  end
data/test/sql_test.rb CHANGED
@@ -67,6 +67,11 @@ class TestSql < Minitest::Unit::TestCase
67
67
  # all
68
68
  assert_search "product", ["Product A"], where: {user_ids: {all: [1, 3]}}
69
69
  assert_search "product", [], where: {user_ids: {all: [1, 2, 3, 4]}}
70
+ # not / exists
71
+ assert_search "product", ["Product C", "Product D"], where: {user_ids: nil}
72
+ assert_search "product", ["Product A", "Product B"], where: {user_ids: {not: nil}}
73
+ assert_search "product", ["Product A", "Product C", "Product D"], where: {user_ids: [3, nil]}
74
+ assert_search "product", ["Product B"], where: {user_ids: {not: [3, nil]}}
70
75
  end
71
76
 
72
77
  def test_where_string
data/test/test_helper.rb CHANGED
@@ -115,7 +115,13 @@ class Product
115
115
  suggest: [:name, :color],
116
116
  conversions: "conversions",
117
117
  personalize: "user_ids",
118
- locations: ["location", "multiple_locations"]
118
+ locations: ["location", "multiple_locations"],
119
+ text_start: [:name],
120
+ text_middle: [:name],
121
+ text_end: [:name],
122
+ word_start: [:name],
123
+ word_middle: [:name],
124
+ word_end: [:name]
119
125
 
120
126
  attr_accessor :conversions, :user_ids
121
127
 
metadata CHANGED
@@ -1,111 +1,111 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.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-12-29 00:00:00.000000000 Z
11
+ date: 2014-01-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tire
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: tire-contrib
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '1.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '1.3'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: minitest
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ~>
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: '4.7'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ~>
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '4.7'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: activerecord
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '>='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '>='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: pg
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - '>='
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - '>='
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  description: Search made easy
@@ -115,8 +115,8 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
- - .gitignore
119
- - .travis.yml
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
120
  - CHANGELOG.md
121
121
  - Gemfile
122
122
  - LICENSE.txt
@@ -158,17 +158,17 @@ require_paths:
158
158
  - lib
159
159
  required_ruby_version: !ruby/object:Gem::Requirement
160
160
  requirements:
161
- - - '>='
161
+ - - ">="
162
162
  - !ruby/object:Gem::Version
163
163
  version: '0'
164
164
  required_rubygems_version: !ruby/object:Gem::Requirement
165
165
  requirements:
166
- - - '>='
166
+ - - ">="
167
167
  - !ruby/object:Gem::Version
168
168
  version: '0'
169
169
  requirements: []
170
170
  rubyforge_project:
171
- rubygems_version: 2.0.0
171
+ rubygems_version: 2.2.0
172
172
  signing_key:
173
173
  specification_version: 4
174
174
  summary: Search made easy