searchkick 1.0.3 → 1.1.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: 12242ee03581e51bcb73eb5301309a0ba85b0c8b
4
- data.tar.gz: 94f6e444569e05c8d1a56054da8f079cdf43bf50
3
+ metadata.gz: 35f9202e63447e7c182bd2a34737e7a519888e91
4
+ data.tar.gz: 19a7a8d24dd5ff687687e235c64971cb979f42f2
5
5
  SHA512:
6
- metadata.gz: 56a50ceacda73f6aee353c22b84191d721afe481808ed69beb7d79601d4e6cd57deed76ac8f70f4ccc37d753ee4d16758b5b269359cbd79ec363180290c4ee99
7
- data.tar.gz: 71953014b0b9233519f0088da6d3246a2f45fa3babff843dcce2143b0b9f24b8ddeb0523418b5b764485785921c11a2ad6f723b028cd84d420162bba15bf4438
6
+ metadata.gz: 762a4b1e6ce4a31e2a3d75ac235954222f26c3d5361d0bb8fd1504aeaf3f294c0f569a15208d1e2e38428715bdaa1bdc879356b2040bff5bb313f763e3091ccc
7
+ data.tar.gz: 1f7c6ea8338b6fe7479a22a55d523a753903fd703f142522806b83926be3cf3b20d10b1b2c39eb507657af97f48e84372d1c6cb102e10fe6a05ea40031eefe16
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.1.0
2
+
3
+ - Added `below` option to misspellings to improve performance
4
+ - Fixed synonyms for `word_*` partial matches
5
+ - Added `searchable` option
6
+ - Added `similarity` option
7
+ - Added `match` option
8
+ - Added `word` option
9
+ - Added highlighted fields to `load: false`
10
+
1
11
  ## 1.0.3
2
12
 
3
13
  - Added support for Elasticsearch 2.1
data/README.md CHANGED
@@ -127,7 +127,7 @@ Searches return a `Searchkick::Results` object. This responds like an array to m
127
127
  results = Product.search("milk")
128
128
  results.size
129
129
  results.any?
130
- results.each { ... }
130
+ results.each { |result| ... }
131
131
  ```
132
132
 
133
133
  Get total results
@@ -227,7 +227,7 @@ end
227
227
  And to search (after you reindex):
228
228
 
229
229
  ```ruby
230
- Product.search "back", fields: [{name: :word_start}]
230
+ Product.search "back", fields: [:name], match: :word_start
231
231
  ```
232
232
 
233
233
  Available options are:
@@ -242,16 +242,10 @@ Available options are:
242
242
  :text_end
243
243
  ```
244
244
 
245
- To boost fields, use:
246
-
247
- ```ruby
248
- fields: [{"name^2" => :word_start}] # better interface on the way
249
- ```
250
-
251
245
  ### Exact Matches
252
246
 
253
247
  ```ruby
254
- User.search "hi@searchkick.org", fields: [{email: :exact}, :name]
248
+ User.search params[:q], fields: [{email: :exact}, :name]
255
249
  ```
256
250
 
257
251
  ### Language
@@ -309,7 +303,15 @@ You can change this with:
309
303
  Product.search "zucini", misspellings: {edit_distance: 2} # zucchini
310
304
  ```
311
305
 
312
- Or turn off misspellings with:
306
+ To improve performance for correctly spelled queries (which should be a majority for most applications), Searchkick can first perform a search without misspellings, and if there are few results, perform another with them. [master]
307
+
308
+ ```ruby
309
+ Product.search "zuchini", misspellings: {below: 5}
310
+ ```
311
+
312
+ If there are fewer than 5 results, a 2nd search is performed for misspellings.
313
+
314
+ Turn off misspellings with:
313
315
 
314
316
  ```ruby
315
317
  Product.search "zuchini", misspellings: false # no zucchini
@@ -498,14 +500,14 @@ First, specify which fields use this feature. This is necessary since autocompl
498
500
 
499
501
  ```ruby
500
502
  class City < ActiveRecord::Base
501
- searchkick text_start: [:name]
503
+ searchkick match: :word_start
502
504
  end
503
505
  ```
504
506
 
505
507
  Reindex and search with:
506
508
 
507
509
  ```ruby
508
- City.search "san fr", fields: [{name: :text_start}]
510
+ City.search "san fr", fields: [:name]
509
511
  ```
510
512
 
511
513
  Typically, you want to use a JavaScript library like [typeahead.js](http://twitter.github.io/typeahead.js/) or [jQuery UI](http://jqueryui.com/autocomplete/).
@@ -517,11 +519,9 @@ First, add a route and controller action.
517
519
  ```ruby
518
520
  # app/controllers/cities_controller.rb
519
521
  class CitiesController < ApplicationController
520
-
521
522
  def autocomplete
522
- render json: City.search(params[:query], fields: [{name: :text_start}], limit: 10).map(&:name)
523
+ render json: City.search(params[:query], fields: [:name], limit: 10).map(&:name)
523
524
  end
524
-
525
525
  end
526
526
  ```
527
527
 
@@ -546,7 +546,7 @@ Then add the search box and JavaScript code to a view.
546
546
 
547
547
  ```ruby
548
548
  class Product < ActiveRecord::Base
549
- searchkick suggest: ["name"] # fields to generate suggestions
549
+ searchkick suggest: [:name] # fields to generate suggestions
550
550
  end
551
551
  ```
552
552
 
@@ -1147,6 +1147,14 @@ class Product < ActiveRecord::Base
1147
1147
  end
1148
1148
  ```
1149
1149
 
1150
+ Use [Okapi BM25](https://www.elastic.co/guide/en/elasticsearch/guide/current/pluggable-similarites.html) for ranking [master]
1151
+
1152
+ ```ruby
1153
+ class Product < ActiveRecord::Base
1154
+ searchkick similarity: "BM25"
1155
+ end
1156
+ ```
1157
+
1150
1158
  Change import batch size
1151
1159
 
1152
1160
  ```ruby
@@ -353,6 +353,10 @@ module Searchkick
353
353
  settings.merge!(number_of_shards: 1, number_of_replicas: 0)
354
354
  end
355
355
 
356
+ if options[:similarity]
357
+ settings[:similarity] = {default: {type: options[:similarity]}}
358
+ end
359
+
356
360
  settings.deep_merge!(options[:settings] || {})
357
361
 
358
362
  # synonyms
@@ -376,6 +380,10 @@ module Searchkick
376
380
  # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
377
381
  settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_synonym")
378
382
  settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
383
+
384
+ %w(word_start word_middle word_end).each do |type|
385
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
386
+ end
379
387
  end
380
388
 
381
389
  if options[:wordnet]
@@ -387,6 +395,10 @@ module Searchkick
387
395
 
388
396
  settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_wordnet")
389
397
  settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_wordnet"
398
+
399
+ %w(word_start word_middle word_end).each do |type|
400
+ settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
401
+ end
390
402
  end
391
403
 
392
404
  if options[:special_characters] == false
@@ -409,29 +421,34 @@ module Searchkick
409
421
  end
410
422
 
411
423
  mapping_options = Hash[
412
- [:autocomplete, :suggest, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight]
424
+ [:autocomplete, :suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable]
413
425
  .map { |type| [type, (options[type] || []).map(&:to_s)] }
414
426
  ]
415
427
 
428
+ word = options[:word] != false && (!options[:match] || options[:match] == :word)
429
+
416
430
  mapping_options.values.flatten.uniq.each do |field|
417
431
  field_mapping = {
418
432
  type: "multi_field",
419
433
  fields: {
420
- field => {type: "string", index: "not_analyzed"},
421
- "analyzed" => {type: "string", index: "analyzed"}
422
- # term_vector: "with_positions_offsets" for fast / correct highlighting
423
- # http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_fast_vector_highlighter
434
+ field => {type: "string", index: "not_analyzed"}
424
435
  }
425
436
  }
426
437
 
427
- mapping_options.except(:highlight).each do |type, fields|
428
- if fields.include?(field)
429
- field_mapping[:fields][type] = {type: "string", index: "analyzed", analyzer: "searchkick_#{type}_index"}
438
+ if !options[:searchable] || mapping_options[:searchable].include?(field)
439
+ if word
440
+ field_mapping[:fields]["analyzed"] = {type: "string", index: "analyzed"}
441
+
442
+ if mapping_options[:highlight].include?(field)
443
+ field_mapping[:fields]["analyzed"][:term_vector] = "with_positions_offsets"
444
+ end
430
445
  end
431
- end
432
446
 
433
- if mapping_options[:highlight].include?(field)
434
- field_mapping[:fields]["analyzed"][:term_vector] = "with_positions_offsets"
447
+ mapping_options.except(:highlight, :searchable).each do |type, fields|
448
+ if options[:match] == type || fields.include?(field)
449
+ field_mapping[:fields][type] = {type: "string", index: "analyzed", analyzer: "searchkick_#{type}_index"}
450
+ end
451
+ end
435
452
  end
436
453
 
437
454
  mapping[field] = field_mapping
@@ -455,6 +472,24 @@ module Searchkick
455
472
  routing = {required: true, path: options[:routing].to_s}
456
473
  end
457
474
 
475
+ dynamic_fields = {
476
+ # analyzed field must be the default field for include_in_all
477
+ # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
478
+ # however, we can include the not_analyzed field in _all
479
+ # and the _all index analyzer will take care of it
480
+ "{name}" => {type: "string", index: "not_analyzed", include_in_all: !options[:searchable]}
481
+ }
482
+
483
+ unless options[:searchable]
484
+ if options[:match] && options[:match] != :word
485
+ dynamic_fields[options[:match]] = {type: "string", index: "analyzed", analyzer: "searchkick_#{options[:match]}_index"}
486
+ end
487
+
488
+ if word
489
+ dynamic_fields["analyzed"] = {type: "string", index: "analyzed"}
490
+ end
491
+ end
492
+
458
493
  mappings = {
459
494
  _default_: {
460
495
  properties: mapping,
@@ -468,14 +503,7 @@ module Searchkick
468
503
  mapping: {
469
504
  # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
470
505
  type: "multi_field",
471
- fields: {
472
- # analyzed field must be the default field for include_in_all
473
- # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
474
- # however, we can include the not_analyzed field in _all
475
- # and the _all index analyzer will take care of it
476
- "{name}" => {type: "string", index: "not_analyzed"},
477
- "analyzed" => {type: "string", index: "analyzed"}
478
- }
506
+ fields: dynamic_fields
479
507
  }
480
508
  }
481
509
  }
@@ -2,16 +2,16 @@
2
2
 
3
3
  module Searchkick
4
4
  class Query
5
- def execute_with_instrumentation
5
+ def execute_search_with_instrumentation
6
6
  event = {
7
7
  name: "#{searchkick_klass.name} Search",
8
8
  query: params
9
9
  }
10
10
  ActiveSupport::Notifications.instrument("search.searchkick", event) do
11
- execute_without_instrumentation
11
+ execute_search_without_instrumentation
12
12
  end
13
13
  end
14
- alias_method_chain :execute, :instrumentation
14
+ alias_method_chain :execute_search, :instrumentation
15
15
  end
16
16
 
17
17
  class Index
@@ -22,15 +22,114 @@ module Searchkick
22
22
  @klass = klass
23
23
  @term = term
24
24
  @options = options
25
+ @match_suffix = options[:match] || searchkick_options[:match] || "analyzed"
25
26
 
27
+ prepare
28
+ end
29
+
30
+ def searchkick_index
31
+ klass.searchkick_index
32
+ end
33
+
34
+ def searchkick_options
35
+ klass.searchkick_options
36
+ end
37
+
38
+ def searchkick_klass
39
+ klass.searchkick_klass
40
+ end
41
+
42
+ def params
43
+ params = {
44
+ index: options[:index_name] || searchkick_index.name,
45
+ body: body
46
+ }
47
+ params.merge!(type: @type) if @type
48
+ params.merge!(routing: @routing) if @routing
49
+ params
50
+ end
51
+
52
+ def execute
53
+ @execute ||= begin
54
+ begin
55
+ response = execute_search
56
+ if @misspellings_below && response["hits"]["total"] < @misspellings_below
57
+ prepare
58
+ response = execute_search
59
+ end
60
+ rescue => e # TODO rescue type
61
+ status_code = e.message[1..3].to_i
62
+ if status_code == 404
63
+ raise MissingIndexError, "Index missing - run #{searchkick_klass.name}.reindex"
64
+ elsif status_code == 500 && (
65
+ e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") ||
66
+ e.message.include?("No query registered for [multi_match]") ||
67
+ e.message.include?("[match] query does not support [cutoff_frequency]]") ||
68
+ e.message.include?("No query registered for [function_score]]")
69
+ )
70
+
71
+ raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 1.0 or greater"
72
+ elsif status_code == 400
73
+ if e.message.include?("[multi_match] analyzer [searchkick_search] not found")
74
+ raise InvalidQueryError, "Bad mapping - run #{searchkick_klass.name}.reindex"
75
+ else
76
+ raise InvalidQueryError, e.message
77
+ end
78
+ else
79
+ raise e
80
+ end
81
+ end
82
+
83
+ # apply facet limit in client due to
84
+ # https://github.com/elasticsearch/elasticsearch/issues/1305
85
+ @facet_limits.each do |field, limit|
86
+ field = field.to_s
87
+ facet = response["facets"][field]
88
+ response["facets"][field]["terms"] = facet["terms"].first(limit)
89
+ response["facets"][field]["other"] = facet["total"] - facet["terms"].sum { |term| term["count"] }
90
+ end
91
+
92
+ opts = {
93
+ page: @page,
94
+ per_page: @per_page,
95
+ padding: @padding,
96
+ load: @load,
97
+ includes: options[:include] || options[:includes],
98
+ json: !options[:json].nil?,
99
+ match_suffix: @match_suffix,
100
+ highlighted_fields: @highlighted_fields || []
101
+ }
102
+ Searchkick::Results.new(searchkick_klass, response, opts)
103
+ end
104
+ end
105
+
106
+ def to_curl
107
+ query = params
108
+ type = query[:type]
109
+ index = query[:index].is_a?(Array) ? query[:index].join(",") : query[:index]
110
+
111
+ # no easy way to tell which host the client will use
112
+ host = Searchkick.client.transport.hosts.first
113
+ credentials = (host[:user] || host[:password]) ? "#{host[:user]}:#{host[:password]}@" : nil
114
+ "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -d '#{query[:body].to_json}'"
115
+ end
116
+
117
+ private
118
+
119
+ def execute_search
120
+ Searchkick.client.search(params)
121
+ end
122
+
123
+ def prepare
26
124
  boost_fields = {}
125
+ fields = options[:fields] || searchkick_options[:searchable]
27
126
  fields =
28
- if options[:fields]
127
+ if fields
29
128
  if options[:autocomplete]
30
- options[:fields].map { |f| "#{f}.autocomplete" }
129
+ fields.map { |f| "#{f}.autocomplete" }
31
130
  else
32
- options[:fields].map do |value|
33
- k, v = value.is_a?(Hash) ? value.to_a.first : [value, :word]
131
+ fields.map do |value|
132
+ k, v = value.is_a?(Hash) ? value.to_a.first : [value, options[:match] || searchkick_options[:match] || :word]
34
133
  k2, boost = k.to_s.split("^", 2)
35
134
  field = "#{k2}.#{v == :word ? 'analyzed' : v}"
36
135
  boost_fields[field] = boost.to_f if boost
@@ -93,6 +192,36 @@ module Searchkick
93
192
  }
94
193
  else
95
194
  queries = []
195
+
196
+ misspellings =
197
+ if options.key?(:misspellings)
198
+ options[:misspellings]
199
+ elsif options.key?(:mispellings)
200
+ options[:mispellings] # why not?
201
+ else
202
+ true
203
+ end
204
+
205
+ if misspellings.is_a?(Hash) && misspellings[:below] && !@misspellings_below
206
+ @misspellings_below = misspellings[:below].to_i
207
+ misspellings = false
208
+ end
209
+
210
+ if misspellings != false
211
+ edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
212
+ transpositions =
213
+ if misspellings.is_a?(Hash) && misspellings.key?(:transpositions)
214
+ {fuzzy_transpositions: misspellings[:transpositions]}
215
+ elsif below14?
216
+ {}
217
+ else
218
+ {fuzzy_transpositions: true}
219
+ end
220
+ prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
221
+ default_max_expansions = @misspellings_below ? 20 : 3
222
+ max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || default_max_expansions
223
+ end
224
+
96
225
  fields.each do |field|
97
226
  qs = []
98
227
 
@@ -103,29 +232,6 @@ module Searchkick
103
232
  boost: 10 * factor
104
233
  }
105
234
 
106
- misspellings =
107
- if options.key?(:misspellings)
108
- options[:misspellings]
109
- elsif options.key?(:mispellings)
110
- options[:mispellings] # why not?
111
- else
112
- true
113
- end
114
-
115
- if misspellings != false
116
- edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
117
- transpositions =
118
- if misspellings.is_a?(Hash) && misspellings.key?(:transpositions)
119
- {fuzzy_transpositions: misspellings[:transpositions]}
120
- elsif below14?
121
- {}
122
- else
123
- {fuzzy_transpositions: true}
124
- end
125
- prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
126
- max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || 3
127
- end
128
-
129
235
  if field == "_all" || field.end_with?(".analyzed")
130
236
  shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
131
237
  qs.concat [
@@ -436,10 +542,12 @@ module Searchkick
436
542
  payload[:highlight][:fields] = {}
437
543
 
438
544
  highlight_fields.each do |name, opts|
439
- payload[:highlight][:fields]["#{name}.analyzed"] = opts || {}
545
+ payload[:highlight][:fields]["#{name}.#{@match_suffix}"] = opts || {}
440
546
  end
441
547
  end
442
548
  end
549
+
550
+ @highlighted_fields = payload[:highlight][:fields].keys
443
551
  end
444
552
 
445
553
  # An empty array will cause only the _id and _type for each hit to be returned
@@ -466,89 +574,6 @@ module Searchkick
466
574
  @load = load
467
575
  end
468
576
 
469
- def searchkick_index
470
- klass.searchkick_index
471
- end
472
-
473
- def searchkick_options
474
- klass.searchkick_options
475
- end
476
-
477
- def searchkick_klass
478
- klass.searchkick_klass
479
- end
480
-
481
- def params
482
- params = {
483
- index: options[:index_name] || searchkick_index.name,
484
- body: body
485
- }
486
- params.merge!(type: @type) if @type
487
- params.merge!(routing: @routing) if @routing
488
- params
489
- end
490
-
491
- def execute
492
- @execute ||= begin
493
- begin
494
- response = Searchkick.client.search(params)
495
- rescue => e # TODO rescue type
496
- status_code = e.message[1..3].to_i
497
- if status_code == 404
498
- raise MissingIndexError, "Index missing - run #{searchkick_klass.name}.reindex"
499
- elsif status_code == 500 && (
500
- e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") ||
501
- e.message.include?("No query registered for [multi_match]") ||
502
- e.message.include?("[match] query does not support [cutoff_frequency]]") ||
503
- e.message.include?("No query registered for [function_score]]")
504
- )
505
-
506
- raise UnsupportedVersionError, "This version of Searchkick requires Elasticsearch 1.0 or greater"
507
- elsif status_code == 400
508
- if e.message.include?("[multi_match] analyzer [searchkick_search] not found")
509
- raise InvalidQueryError, "Bad mapping - run #{searchkick_klass.name}.reindex"
510
- else
511
- raise InvalidQueryError, e.message
512
- end
513
- else
514
- raise e
515
- end
516
- end
517
-
518
- # apply facet limit in client due to
519
- # https://github.com/elasticsearch/elasticsearch/issues/1305
520
- @facet_limits.each do |field, limit|
521
- field = field.to_s
522
- facet = response["facets"][field]
523
- response["facets"][field]["terms"] = facet["terms"].first(limit)
524
- response["facets"][field]["other"] = facet["total"] - facet["terms"].sum { |term| term["count"] }
525
- end
526
-
527
- opts = {
528
- page: @page,
529
- per_page: @per_page,
530
- padding: @padding,
531
- load: @load,
532
- includes: options[:include] || options[:includes],
533
- json: !options[:json].nil?
534
- }
535
- Searchkick::Results.new(searchkick_klass, response, opts)
536
- end
537
- end
538
-
539
- def to_curl
540
- query = params
541
- type = query[:type]
542
- index = query[:index].is_a?(Array) ? query[:index].join(",") : query[:index]
543
-
544
- # no easy way to tell which host the client will use
545
- host = Searchkick.client.transport.hosts.first
546
- credentials = (host[:user] || host[:password]) ? "#{host[:user]}:#{host[:password]}@" : nil
547
- "curl #{host[:protocol]}://#{credentials}#{host[:host]}:#{host[:port]}/#{CGI.escape(index)}#{type ? "/#{type.map { |t| CGI.escape(t) }.join(',')}" : ''}/_search?pretty -d '#{query[:body].to_json}'"
548
- end
549
-
550
- private
551
-
552
577
  def where_filters(where)
553
578
  filters = []
554
579
  (where || {}).each do |field, value|
@@ -42,6 +42,14 @@ module Searchkick
42
42
  else
43
43
  hit.except("fields").merge(hit["fields"])
44
44
  end
45
+
46
+ if hit["highlight"]
47
+ highlight = Hash[hit["highlight"].map { |k, v| [base_field(k), v.first] }]
48
+ options[:highlighted_fields].map{ |k| base_field(k) }.each do |k|
49
+ result["highlighted_#{k}"] ||= (highlight[k] || result[k])
50
+ end
51
+ end
52
+
45
53
  result["id"] ||= result["_id"] # needed for legacy reasons
46
54
  Hashie::Mash.new(result)
47
55
  end
@@ -65,7 +73,7 @@ module Searchkick
65
73
  each_with_hit.map do |model, hit|
66
74
  details = {}
67
75
  if hit["highlight"]
68
- details[:highlight] = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.analyzed\z/, "")).to_sym, v.first] }]
76
+ details[:highlight] = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, v.first] }]
69
77
  end
70
78
  [model, details]
71
79
  end
@@ -189,5 +197,9 @@ module Searchkick
189
197
  raise "Not sure how to load records"
190
198
  end
191
199
  end
200
+
201
+ def base_field(k)
202
+ k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
203
+ end
192
204
  end
193
205
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "1.0.3"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
+ gem install bundler
4
+
3
5
  sudo apt-get purge elasticsearch
4
6
  wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.7.3.deb
5
7
  sudo dpkg -i elasticsearch-1.7.3.deb
@@ -27,7 +27,8 @@ class HighlightTest < Minitest::Test
27
27
 
28
28
  def test_field_options
29
29
  store_names ["Two Door Cinema Club are a Northern Irish indie rock band"]
30
- assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 20}}}).with_details.first[1][:highlight][:name]
30
+ fragment_size = ENV["MATCH"] == "word_start" ? 26 : 20
31
+ assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: fragment_size}}}).with_details.first[1][:highlight][:name]
31
32
  end
32
33
 
33
34
  def test_multiple_words
@@ -36,6 +37,7 @@ class HighlightTest < Minitest::Test
36
37
  end
37
38
 
38
39
  def test_json
40
+ skip if ENV["MATCH"] == "word_start"
39
41
  store_names ["Two Door Cinema Club"]
40
42
  json = {
41
43
  query: {
data/test/match_test.rb CHANGED
@@ -198,6 +198,13 @@ class MatchTest < Minitest::Test
198
198
  assert_search "almond", []
199
199
  end
200
200
 
201
+ def test_unsearchable_where
202
+ store [
203
+ {name: "Unsearchable", description: "Almond"}
204
+ ]
205
+ assert_search "*", ["Unsearchable"], where: {description: "Almond"}
206
+ end
207
+
201
208
  def test_emoji
202
209
  skip unless defined?(EmojiParser)
203
210
  store_names ["Banana"]
@@ -33,4 +33,14 @@ class MisspellingsTest < Minitest::Test
33
33
  ]
34
34
  assert_search "red blue", ["red", "blue", "cyan", "magenta"], operator: "or", fields: ["color"], misspellings: false
35
35
  end
36
+
37
+ def test_misspellings_below_unmet
38
+ store_names ["abc", "abd", "aee"]
39
+ assert_search "abc", ["abc", "abd"], misspellings: {below: 2}
40
+ end
41
+
42
+ def test_misspellings_below_met
43
+ store_names ["abc", "abd", "aee"]
44
+ assert_search "abc", ["abc"], misspellings: {below: 1}
45
+ end
36
46
  end
@@ -41,6 +41,11 @@ class SynonymsTest < Minitest::Test
41
41
  assert_search "scallions", ["Green Onions"]
42
42
  end
43
43
 
44
+ def test_word_start
45
+ store_names ["Clorox Bleach", "Kroger Bleach"]
46
+ assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"], fields: [{name: :word_start}]
47
+ end
48
+
44
49
  def test_wordnet
45
50
  skip unless ENV["TEST_WORDNET"]
46
51
  store_names ["Creature", "Beast", "Dragon"], Animal
data/test/test_helper.rb CHANGED
@@ -216,7 +216,9 @@ class Product
216
216
  word_middle: [:name],
217
217
  word_end: [:name],
218
218
  highlight: [:name],
219
- unsearchable: [:description]
219
+ # unsearchable: [:description],
220
+ searchable: [:name, :color],
221
+ match: ENV["MATCH"] ? ENV["MATCH"].to_sym : nil
220
222
 
221
223
  attr_accessor :conversions, :user_ids, :aisle
222
224
 
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: 1.0.3
4
+ version: 1.1.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: 2015-11-28 00:00:00.000000000 Z
11
+ date: 2015-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -174,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
174
  version: '0'
175
175
  requirements: []
176
176
  rubyforge_project:
177
- rubygems_version: 2.4.5.1
177
+ rubygems_version: 2.4.5
178
178
  signing_key:
179
179
  specification_version: 4
180
180
  summary: Searchkick learns what your users are looking for. As more people search,
@@ -216,3 +216,4 @@ test_files:
216
216
  - test/synonyms_test.rb
217
217
  - test/test_helper.rb
218
218
  - test/where_test.rb
219
+ has_rdoc: