searchkick 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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