Commit 7ce0440ab4f2fdd02a52b2bcf42fb4f90e668667

Authored by Andrew Kane
1 parent 4054147c

Added support for Elasticsearch 7

@@ -14,30 +14,24 @@ cache: @@ -14,30 +14,24 @@ cache:
14 directories: 14 directories:
15 - $HOME/elasticsearch 15 - $HOME/elasticsearch
16 env: 16 env:
17 - - ELASTICSEARCH_VERSION=6.7.0 17 + - ELASTICSEARCH_VERSION=7.0.0
18 jdk: openjdk10 18 jdk: openjdk10
19 matrix: 19 matrix:
20 include: 20 include:
21 - gemfile: Gemfile 21 - gemfile: Gemfile
22 - gemfile: test/gemfiles/activerecord51.gemfile 22 - gemfile: test/gemfiles/activerecord51.gemfile
23 - env: ELASTICSEARCH_VERSION=6.0.0 23 + env: ELASTICSEARCH_VERSION=7.0.0
24 - gemfile: test/gemfiles/activerecord50.gemfile 24 - gemfile: test/gemfiles/activerecord50.gemfile
25 - env: ELASTICSEARCH_VERSION=5.6.10  
26 - - gemfile: test/gemfiles/activerecord42.gemfile  
27 - env: ELASTICSEARCH_VERSION=5.0.1  
28 - - gemfile: test/gemfiles/mongoid6.gemfile 25 + env: ELASTICSEARCH_VERSION=6.7.0
  26 + - gemfile: test/gemfiles/mongoid7.gemfile
  27 + env: ELASTICSEARCH_VERSION=6.0.0
29 services: 28 services:
30 - mongodb 29 - mongodb
31 - redis-server 30 - redis-server
32 - - gemfile: test/gemfiles/mongoid5.gemfile 31 + - gemfile: test/gemfiles/mongoid6.gemfile
33 services: 32 services:
34 - mongodb 33 - mongodb
35 - redis-server 34 - redis-server
36 - - gemfile: Gemfile  
37 - env: ELASTICSEARCH_VERSION=7.0.0-rc1  
38 - allow_failures:  
39 - - gemfile: Gemfile  
40 - env: ELASTICSEARCH_VERSION=7.0.0-rc1  
41 notifications: 35 notifications:
42 email: 36 email:
43 on_success: never 37 on_success: never
  1 +## 4.0.0 [unreleased]
  2 +
  3 +- Added support for Elasticsearch 7
  4 +- Added `models` option
  5 +
  6 +Breaking changes
  7 +
  8 +- Removed support for Elasticsearch 5
  9 +- Removed support for multi-word synonyms (they no longer work with shingles)
  10 +
1 ## 3.1.3 11 ## 3.1.3
2 12
3 - Added support for endless ranges 13 - Added support for endless ranges
@@ -53,7 +53,7 @@ Add this line to your application’s Gemfile: @@ -53,7 +53,7 @@ Add this line to your application’s Gemfile:
53 gem 'searchkick' 53 gem 'searchkick'
54 ``` 54 ```
55 55
56 -The latest version works with Elasticsearch 5 and 6. For Elasticsearch 2, use version 2.5.0 and [this readme](https://github.com/ankane/searchkick/blob/v2.5.0/README.md). 56 +The latest version works with Elasticsearch 6 and 7. For Elasticsearch 5, use version 3.1.3 and [this readme](https://github.com/ankane/searchkick/blob/v3.1.3/README.md).
57 57
58 Add searchkick to models you want to search. 58 Add searchkick to models you want to search.
59 59
@@ -312,13 +312,13 @@ A few languages require plugins: @@ -312,13 +312,13 @@ A few languages require plugins:
312 312
313 ```ruby 313 ```ruby
314 class Product < ApplicationRecord 314 class Product < ApplicationRecord
315 - searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]] 315 + searchkick synonyms: [["burger", "hamburger"], ["sneakers", "shoes"]]
316 end 316 end
317 ``` 317 ```
318 318
319 Call `Product.reindex` after changing synonyms. 319 Call `Product.reindex` after changing synonyms.
320 320
321 -Synonyms cannot be more than two words at the moment. 321 +Synonyms cannot be multiple words at the moment.
322 322
323 To read synonyms from a file, use: 323 To read synonyms from a file, use:
324 324
@@ -796,8 +796,6 @@ Script support @@ -796,8 +796,6 @@ Script support
796 Product.search "*", aggs: {color: {script: {source: "'Color: ' + _value"}}} 796 Product.search "*", aggs: {color: {script: {source: "'Color: ' + _value"}}}
797 ``` 797 ```
798 798
799 -**Note:** Use `inline` instead of `source` before Elasticsearch 5.6  
800 -  
801 Date histogram 799 Date histogram
802 800
803 ```ruby 801 ```ruby
@@ -924,9 +922,7 @@ You can also index and search geo shapes. @@ -924,9 +922,7 @@ You can also index and search geo shapes.
924 922
925 ```ruby 923 ```ruby
926 class Restaurant < ApplicationRecord 924 class Restaurant < ApplicationRecord
927 - searchkick geo_shape: {  
928 - bounds: {tree: "geohash", precision: "1km"}  
929 - } 925 + searchkick geo_shape: [:bounds]
930 926
931 def search_data 927 def search_data
932 attributes.merge( 928 attributes.merge(
@@ -959,12 +955,6 @@ Not touching the query shape @@ -959,12 +955,6 @@ Not touching the query shape
959 Restaurant.search "burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}} 955 Restaurant.search "burger", where: {bounds: {geo_shape: {type: "envelope", relation: "disjoint", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}
960 ``` 956 ```
961 957
962 -Containing the query shape  
963 -  
964 -```ruby  
965 -Restaurant.search "fries", where: {bounds: {geo_shape: {type: "envelope", relation: "contains", coordinates: [{lat: 38, lon: -123}, {lat: 37, lon: -122}]}}}  
966 -```  
967 -  
968 ## Inheritance 958 ## Inheritance
969 959
970 Searchkick supports single table inheritance. 960 Searchkick supports single table inheritance.
@@ -1496,21 +1486,15 @@ Then use `products` and `coupons` as typical results. @@ -1496,21 +1486,15 @@ Then use `products` and `coupons` as typical results.
1496 1486
1497 **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors. 1487 **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors.
1498 1488
1499 -## Multiple Indices 1489 +## Multiple Models
1500 1490
1501 -Search across multiple models/indices with: 1491 +Search across multiple models with:
1502 1492
1503 ```ruby 1493 ```ruby
1504 -Searchkick.search "milk", index_name: [Product, Category] 1494 +Searchkick.search "milk", models: [Product, Category]
1505 ``` 1495 ```
1506 1496
1507 -Specify conditions for different indices  
1508 -  
1509 -```ruby  
1510 -where: {_or: [{_type: "product", in_stock: true}, {_type: "category", active: true}]}  
1511 -```  
1512 -  
1513 -Boost specific indices with: 1497 +Boost specific models with:
1514 1498
1515 ```ruby 1499 ```ruby
1516 indices_boost: {Category => 2, Product => 1} 1500 indices_boost: {Category => 2, Product => 1}
@@ -1657,7 +1641,7 @@ Product.search &quot;milk&quot;, includes: [:brand, :stores] @@ -1657,7 +1641,7 @@ Product.search &quot;milk&quot;, includes: [:brand, :stores]
1657 Eager load different associations by model 1641 Eager load different associations by model
1658 1642
1659 ```ruby 1643 ```ruby
1660 -Searchkick.search("*", index_name: [Product, Store], model_includes: {Product => [:store], Store => [:product]}) 1644 +Searchkick.search("*", models: [Product, Store], model_includes: {Product => [:store], Store => [:product]})
1661 ``` 1645 ```
1662 1646
1663 Run additional scopes on results 1647 Run additional scopes on results
@@ -1900,6 +1884,11 @@ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenan @@ -1900,6 +1884,11 @@ Check out [this great post](https://www.tiagoamaro.com.br/2014/12/11/multi-tenan
1900 1884
1901 See [how to upgrade to Searchkick 3](docs/Searchkick-3-Upgrade.md) 1885 See [how to upgrade to Searchkick 3](docs/Searchkick-3-Upgrade.md)
1902 1886
  1887 +## Elasticsearch 6 to 7 Upgrade
  1888 +
  1889 +1. Install Searchkick 4
  1890 +2. Upgrade your Elasticsearch cluster
  1891 +
1903 ## Elasticsearch 5 to 6 Upgrade 1892 ## Elasticsearch 5 to 6 Upgrade
1904 1893
1905 Elasticsearch 6 removes the ability to reindex with the `_all` field. Before you upgrade, we recommend disabling this field manually and specifying default fields on your models. 1894 Elasticsearch 6 removes the ability to reindex with the `_all` field. Before you upgrade, we recommend disabling this field manually and specifying default fields on your models.
lib/searchkick.rb
@@ -78,16 +78,36 @@ module Searchkick @@ -78,16 +78,36 @@ module Searchkick
78 Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0]) 78 Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0])
79 end 79 end
80 80
  81 + # memoize for performance
  82 + def self.server_below7?
  83 + unless defined?(@server_below7)
  84 + @server_below7 = server_below?("7.0.0")
  85 + end
  86 + @server_below7
  87 + end
  88 +
81 def self.search(term = "*", model: nil, **options, &block) 89 def self.search(term = "*", model: nil, **options, &block)
82 options = options.dup 90 options = options.dup
83 klass = model 91 klass = model
84 92
85 - # make Searchkick.search(index_name: [Product]) and Product.search equivalent 93 + # convert index_name into models if possible
  94 + # this should allow for easier upgrade
  95 + if options[:index_name] && !options[:models] && Array(options[:index_name]).all? { |v| v.respond_to?(:searchkick_index) }
  96 + options[:models] = options.delete(:index_name)
  97 + end
  98 +
  99 + # make Searchkick.search(models: [Product]) and Product.search equivalent
86 unless klass 100 unless klass
87 - index_name = Array(options[:index_name])  
88 - if index_name.size == 1 && index_name.first.respond_to?(:searchkick_index)  
89 - klass = index_name.first  
90 - options.delete(:index_name) 101 + models = Array(options[:models])
  102 + if models.size == 1
  103 + klass = models.first
  104 + options.delete(:models)
  105 + end
  106 + end
  107 +
  108 + if klass
  109 + if (options[:models] && Array(options[:models]) != [klass]) || Array(options[:index_name]).any? { |v| v.respond_to?(:searchkick_index) && v != klass }
  110 + raise ArgumentError, "Use Searchkick.search to search multiple models"
91 end 111 end
92 end 112 end
93 113
lib/searchkick/index.rb
@@ -17,7 +17,7 @@ module Searchkick @@ -17,7 +17,7 @@ module Searchkick
17 end 17 end
18 18
19 def delete 19 def delete
20 - if !Searchkick.server_below?("6.0.0") && alias_exists? 20 + if alias_exists?
21 # can't call delete directly on aliases in ES 6 21 # can't call delete directly on aliases in ES 6
22 indices = client.indices.get_alias(name: name).keys 22 indices = client.indices.get_alias(name: name).keys
23 client.indices.delete index: indices 23 client.indices.delete index: indices
@@ -68,7 +68,7 @@ module Searchkick @@ -68,7 +68,7 @@ module Searchkick
68 } 68 }
69 ) 69 )
70 70
71 - response["hits"]["total"] 71 + Searchkick::Results.new(nil, response).total_count
72 end 72 end
73 73
74 def promote(new_name, update_refresh_interval: false) 74 def promote(new_name, update_refresh_interval: false)
lib/searchkick/index_options.rb
@@ -4,15 +4,13 @@ module Searchkick @@ -4,15 +4,13 @@ module Searchkick
4 options = @options 4 options = @options
5 language = options[:language] 5 language = options[:language]
6 language = language.call if language.respond_to?(:call) 6 language = language.call if language.respond_to?(:call)
7 - index_type = options[:_type]  
8 - index_type = index_type.call if index_type.respond_to?(:call)  
9 7
10 if options[:mappings] && !options[:merge_mappings] 8 if options[:mappings] && !options[:merge_mappings]
11 settings = options[:settings] || {} 9 settings = options[:settings] || {}
12 mappings = options[:mappings] 10 mappings = options[:mappings]
13 else 11 else
14 - below60 = Searchkick.server_below?("6.0.0")  
15 below62 = Searchkick.server_below?("6.2.0") 12 below62 = Searchkick.server_below?("6.2.0")
  13 + below70 = Searchkick.server_below?("7.0.0")
16 14
17 default_type = "text" 15 default_type = "text"
18 default_analyzer = :searchkick_index 16 default_analyzer = :searchkick_index
@@ -144,15 +142,6 @@ module Searchkick @@ -144,15 +142,6 @@ module Searchkick
144 } 142 }
145 } 143 }
146 144
147 - if below60  
148 - # ES docs say standard token filter does nothing in ES 5  
149 - # (and therefore isn't needed at at), but tests say otherwise  
150 - # https://www.elastic.co/guide/en/elasticsearch/reference/5.0/analysis-standard-tokenfilter.html  
151 - [default_analyzer, :searchkick_search, :searchkick_search2].each do |analyzer|  
152 - settings[:analysis][:analyzer][analyzer][:filter].unshift("standard")  
153 - end  
154 - end  
155 -  
156 stem = options[:stem] 145 stem = options[:stem]
157 146
158 case language 147 case language
@@ -279,8 +268,7 @@ module Searchkick @@ -279,8 +268,7 @@ module Searchkick
279 # - Only apply the synonym expansion at index time 268 # - Only apply the synonym expansion at index time
280 # - Don't have the synonym filter applied search 269 # - Don't have the synonym filter applied search
281 # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general. 270 # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
282 - settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym") if below60  
283 - settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym" 271 + settings[:analysis][:analyzer][default_analyzer][:filter].insert(2, "searchkick_synonym")
284 272
285 %w(word_start word_middle word_end).each do |type| 273 %w(word_start word_middle word_end).each do |type|
286 settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym") 274 settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
@@ -391,10 +379,6 @@ module Searchkick @@ -391,10 +379,6 @@ module Searchkick
391 "{name}" => keyword_mapping 379 "{name}" => keyword_mapping
392 } 380 }
393 381
394 - if below60 && all  
395 - dynamic_fields["{name}"][:include_in_all] = !options[:searchable]  
396 - end  
397 -  
398 if options.key?(:filterable) 382 if options.key?(:filterable)
399 dynamic_fields["{name}"] = {type: default_type, index: index_false_value} 383 dynamic_fields["{name}"] = {type: default_type, index: index_false_value}
400 end 384 end
@@ -413,25 +397,24 @@ module Searchkick @@ -413,25 +397,24 @@ module Searchkick
413 multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}")) 397 multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
414 398
415 mappings = { 399 mappings = {
416 - index_type => {  
417 - properties: mapping,  
418 - _routing: routing,  
419 - # https://gist.github.com/kimchy/2898285  
420 - dynamic_templates: [  
421 - {  
422 - string_template: {  
423 - match: "*",  
424 - match_mapping_type: "string",  
425 - mapping: multi_field  
426 - } 400 + properties: mapping,
  401 + _routing: routing,
  402 + # https://gist.github.com/kimchy/2898285
  403 + dynamic_templates: [
  404 + {
  405 + string_template: {
  406 + match: "*",
  407 + match_mapping_type: "string",
  408 + mapping: multi_field
427 } 409 }
428 - ]  
429 - } 410 + }
  411 + ]
430 } 412 }
431 413
432 - if below60  
433 - all_enabled = all && (!options[:searchable] || options[:searchable].to_a.map(&:to_s).include?("_all"))  
434 - mappings[index_type][:_all] = all_enabled ? analyzed_field_options : {enabled: false} 414 + if below70
  415 + index_type = options[:_type]
  416 + index_type = index_type.call if index_type.respond_to?(:call)
  417 + mappings = {index_type => mappings}
435 end 418 end
436 419
437 mappings = mappings.symbolize_keys.deep_merge((options[:mappings] || {}).symbolize_keys) 420 mappings = mappings.symbolize_keys.deep_merge((options[:mappings] || {}).symbolize_keys)
lib/searchkick/query.rb
@@ -18,7 +18,7 @@ module Searchkick @@ -18,7 +18,7 @@ module Searchkick
18 unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost, 18 unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
19 :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain, 19 :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain,
20 :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load, 20 :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
21 - :match, :misspellings, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile, 21 + :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
22 :request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where] 22 :request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
23 raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any? 23 raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
24 24
@@ -39,6 +39,7 @@ module Searchkick @@ -39,6 +39,7 @@ module Searchkick
39 @misspellings = false 39 @misspellings = false
40 @misspellings_below = nil 40 @misspellings_below = nil
41 @highlighted_fields = nil 41 @highlighted_fields = nil
  42 + @index_mapping = nil
42 43
43 prepare 44 prepare
44 end 45 end
@@ -56,9 +57,18 @@ module Searchkick @@ -56,9 +57,18 @@ module Searchkick
56 end 57 end
57 58
58 def params 59 def params
  60 + if options[:models]
  61 + @index_mapping = {}
  62 + Array(options[:models]).each do |model|
  63 + @index_mapping[model.searchkick_index.name] = model
  64 + end
  65 + end
  66 +
59 index = 67 index =
60 if options[:index_name] 68 if options[:index_name]
61 Array(options[:index_name]).map { |v| v.respond_to?(:searchkick_index) ? v.searchkick_index.name : v }.join(",") 69 Array(options[:index_name]).map { |v| v.respond_to?(:searchkick_index) ? v.searchkick_index.name : v }.join(",")
  70 + elsif options[:models]
  71 + @index_mapping.keys.join(",")
62 elsif searchkick_index 72 elsif searchkick_index
63 searchkick_index.name 73 searchkick_index.name
64 else 74 else
@@ -116,8 +126,8 @@ module Searchkick @@ -116,8 +126,8 @@ module Searchkick
116 misspellings: @misspellings, 126 misspellings: @misspellings,
117 term: term, 127 term: term,
118 scope_results: options[:scope_results], 128 scope_results: options[:scope_results],
119 - index_name: options[:index_name],  
120 - total_entries: options[:total_entries] 129 + total_entries: options[:total_entries],
  130 + index_mapping: @index_mapping
121 } 131 }
122 132
123 if options[:debug] 133 if options[:debug]
@@ -166,7 +176,7 @@ module Searchkick @@ -166,7 +176,7 @@ module Searchkick
166 end 176 end
167 177
168 def retry_misspellings?(response) 178 def retry_misspellings?(response)
169 - @misspellings_below && response["hits"]["total"] < @misspellings_below 179 + @misspellings_below && Searchkick::Results.new(searchkick_klass, response).total_count < @misspellings_below
170 end 180 end
171 181
172 private 182 private
@@ -377,7 +387,7 @@ module Searchkick @@ -377,7 +387,7 @@ module Searchkick
377 queries_to_add.concat(q2) 387 queries_to_add.concat(q2)
378 end 388 end
379 389
380 - queries.concat(queries_to_add) 390 + queries << queries_to_add
381 391
382 if options[:exclude] 392 if options[:exclude]
383 must_not.concat(set_exclude(exclude_field, exclude_analyzer)) 393 must_not.concat(set_exclude(exclude_field, exclude_analyzer))
@@ -392,9 +402,10 @@ module Searchkick @@ -392,9 +402,10 @@ module Searchkick
392 402
393 should = [] 403 should = []
394 else 404 else
  405 + # higher score for matching more fields
395 payload = { 406 payload = {
396 - dis_max: {  
397 - queries: queries 407 + bool: {
  408 + should: queries.map { |qs| {dis_max: {queries: qs}} }
398 } 409 }
399 } 410 }
400 411
@@ -663,20 +674,9 @@ module Searchkick @@ -663,20 +674,9 @@ module Searchkick
663 def set_boost_by_indices(payload) 674 def set_boost_by_indices(payload)
664 return unless options[:indices_boost] 675 return unless options[:indices_boost]
665 676
666 - if below52?  
667 - indices_boost = options[:indices_boost].each_with_object({}) do |(key, boost), memo|  
668 - index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key  
669 - # try to use index explicitly instead of alias: https://github.com/elasticsearch/elasticsearch/issues/4756  
670 - index_by_alias = Searchkick.client.indices.get_alias(index: index).keys.first  
671 - memo[index_by_alias || index] = boost  
672 - end  
673 - else  
674 - # array format supports alias resolution  
675 - # https://github.com/elastic/elasticsearch/pull/21393  
676 - indices_boost = options[:indices_boost].map do |key, boost|  
677 - index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key  
678 - {index => boost}  
679 - end 677 + indices_boost = options[:indices_boost].map do |key, boost|
  678 + index = key.respond_to?(:searchkick_index) ? key.searchkick_index.name : key
  679 + {index => boost}
680 end 680 end
681 681
682 payload[:indices_boost] = indices_boost 682 payload[:indices_boost] = indices_boost
@@ -713,7 +713,7 @@ module Searchkick @@ -713,7 +713,7 @@ module Searchkick
713 def set_highlights(payload, fields) 713 def set_highlights(payload, fields)
714 payload[:highlight] = { 714 payload[:highlight] = {
715 fields: Hash[fields.map { |f| [f, {}] }], 715 fields: Hash[fields.map { |f| [f, {}] }],
716 - fragment_size: below60? ? 30000 : 0 716 + fragment_size: 0
717 } 717 }
718 718
719 if options[:highlight].is_a?(Hash) 719 if options[:highlight].is_a?(Hash)
@@ -824,7 +824,7 @@ module Searchkick @@ -824,7 +824,7 @@ module Searchkick
824 # TODO id transformation for arrays 824 # TODO id transformation for arrays
825 def set_order(payload) 825 def set_order(payload)
826 order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc} 826 order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
827 - id_field = below60? ? :_uid : :_id 827 + id_field = :_id
828 payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }] 828 payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
829 end 829 end
830 830
@@ -1021,16 +1021,12 @@ module Searchkick @@ -1021,16 +1021,12 @@ module Searchkick
1021 k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "") 1021 k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
1022 end 1022 end
1023 1023
1024 - def below52?  
1025 - Searchkick.server_below?("5.2.0")  
1026 - end  
1027 -  
1028 - def below60?  
1029 - Searchkick.server_below?("6.0.0")  
1030 - end  
1031 -  
1032 def below61? 1024 def below61?
1033 Searchkick.server_below?("6.1.0") 1025 Searchkick.server_below?("6.1.0")
1034 end 1026 end
  1027 +
  1028 + def below70?
  1029 + Searchkick.server_below?("7.0.0")
  1030 + end
1035 end 1031 end
1036 end 1032 end
lib/searchkick/record_data.rb
@@ -34,18 +34,13 @@ module Searchkick @@ -34,18 +34,13 @@ module Searchkick
34 index.klass_document_type(record.class, ignore_type) 34 index.klass_document_type(record.class, ignore_type)
35 end 35 end
36 36
37 - # memoize  
38 - def self.routing_key  
39 - @routing_key ||= Searchkick.server_below?("6.0.0") ? :_routing : :routing  
40 - end  
41 -  
42 def record_data 37 def record_data
43 data = { 38 data = {
44 _index: index.name, 39 _index: index.name,
45 - _id: search_id,  
46 - _type: document_type 40 + _id: search_id
47 } 41 }
48 - data[self.class.routing_key] = record.search_routing if record.respond_to?(:search_routing) 42 + data[:_type] = document_type if Searchkick.server_below7?
  43 + data[:routing] = record.search_routing if record.respond_to?(:search_routing)
49 data 44 data
50 end 45 end
51 46
lib/searchkick/results.rb
@@ -25,9 +25,16 @@ module Searchkick @@ -25,9 +25,16 @@ module Searchkick
25 # results can have different types 25 # results can have different types
26 results = {} 26 results = {}
27 27
28 - hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits|  
29 - klass = (!options[:index_name] && @klass) || type.camelize.constantize  
30 - results[type] = results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s } 28 + hits.group_by { |hit, _| hit["_index"] }.each do |index, grouped_hits|
  29 + klass =
  30 + if @klass
  31 + @klass
  32 + else
  33 + index_alias = index.split("_")[0..-2].join("_")
  34 + (options[:index_mapping] || {})[index_alias]
  35 + end
  36 + raise Searchkick::Error, "Unknown model for index: #{index}" unless klass
  37 + results[index] = results_query(klass, grouped_hits).to_a.index_by { |r| r.id.to_s }
31 end 38 end
32 39
33 missing_ids = [] 40 missing_ids = []
@@ -35,7 +42,7 @@ module Searchkick @@ -35,7 +42,7 @@ module Searchkick
35 # sort 42 # sort
36 results = 43 results =
37 hits.map do |hit| 44 hits.map do |hit|
38 - result = results[hit["_type"]][hit["_id"].to_s] 45 + result = results[hit["_index"]][hit["_id"].to_s]
39 if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable]) 46 if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable])
40 if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights) 47 if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights)
41 highlights = hit_highlights(hit) 48 highlights = hit_highlights(hit)
@@ -132,7 +139,13 @@ module Searchkick @@ -132,7 +139,13 @@ module Searchkick
132 end 139 end
133 140
134 def total_count 141 def total_count
135 - options[:total_entries] || response["hits"]["total"] 142 + if options[:total_entries]
  143 + options[:total_entries]
  144 + elsif response["hits"]["total"].is_a?(Hash)
  145 + response["hits"]["total"]["value"]
  146 + else
  147 + response["hits"]["total"]
  148 + end
136 end 149 end
137 alias_method :total_entries, :total_count 150 alias_method :total_entries, :total_count
138 151
searchkick.gemspec
@@ -16,10 +16,10 @@ Gem::Specification.new do |spec| @@ -16,10 +16,10 @@ Gem::Specification.new do |spec|
16 spec.files = Dir["*.{md,txt}", "{lib}/**/*"] 16 spec.files = Dir["*.{md,txt}", "{lib}/**/*"]
17 spec.require_path = "lib" 17 spec.require_path = "lib"
18 18
19 - spec.required_ruby_version = ">= 2.2" 19 + spec.required_ruby_version = ">= 2.4"
20 20
21 - spec.add_dependency "activemodel", ">= 4.2"  
22 - spec.add_dependency "elasticsearch", ">= 5" 21 + spec.add_dependency "activemodel", ">= 5"
  22 + spec.add_dependency "elasticsearch", ">= 6"
23 spec.add_dependency "hashie" 23 spec.add_dependency "hashie"
24 24
25 spec.add_development_dependency "bundler" 25 spec.add_development_dependency "bundler"
test/aggs_test.rb
@@ -20,8 +20,7 @@ class AggsTest &lt; Minitest::Test @@ -20,8 +20,7 @@ class AggsTest &lt; Minitest::Test
20 end 20 end
21 21
22 def test_order 22 def test_order
23 - order_key = Searchkick.server_below?("6.0") ? "_term" : "_key"  
24 - agg = Product.search("Product", aggs: {color: {order: {order_key => "desc"}}}).aggs["color"] 23 + agg = Product.search("Product", aggs: {color: {order: {_key: "desc"}}}).aggs["color"]
25 assert_equal %w(red green blue), agg["buckets"].map { |b| b["key"] } 24 assert_equal %w(red green blue), agg["buckets"].map { |b| b["key"] }
26 end 25 end
27 26
@@ -37,8 +36,7 @@ class AggsTest &lt; Minitest::Test @@ -37,8 +36,7 @@ class AggsTest &lt; Minitest::Test
37 36
38 def test_script 37 def test_script
39 source = "'Color: ' + _value" 38 source = "'Color: ' + _value"
40 - script = Searchkick.server_below?("5.6") ? {inline: source} : {source: source}  
41 - agg = Product.search("Product", aggs: {color: {script: script}}).aggs["color"] 39 + agg = Product.search("Product", aggs: {color: {script: {source: source}}}).aggs["color"]
42 assert_equal ({"Color: blue" => 1, "Color: green" => 1, "Color: red" => 1}), buckets_as_hash(agg) 40 assert_equal ({"Color: blue" => 1, "Color: green" => 1, "Color: red" => 1}), buckets_as_hash(agg)
43 end 41 end
44 42
test/boost_test.rb
@@ -67,6 +67,7 @@ class BoostTest &lt; Minitest::Test @@ -67,6 +67,7 @@ class BoostTest &lt; Minitest::Test
67 end 67 end
68 68
69 def test_conversions_weight 69 def test_conversions_weight
  70 + Product.reindex
70 store [ 71 store [
71 {name: "Product Boost", orders_count: 20}, 72 {name: "Product Boost", orders_count: 20},
72 {name: "Product Conversions", conversions: {"product" => 10}} 73 {name: "Product Conversions", conversions: {"product" => 10}}
@@ -229,6 +230,6 @@ class BoostTest &lt; Minitest::Test @@ -229,6 +230,6 @@ class BoostTest &lt; Minitest::Test
229 store_names ["Rex"], Animal 230 store_names ["Rex"], Animal
230 store_names ["Rexx"], Product 231 store_names ["Rexx"], Product
231 232
232 - assert_order "Rex", ["Rexx", "Rex"], {index_name: [Animal, Product], indices_boost: {Animal => 1, Product => 200}, fields: [:name]}, Store 233 + assert_order "Rex", ["Rexx", "Rex"], {models: [Animal, Product], indices_boost: {Animal => 1, Product => 200}, fields: [:name]}, Searchkick
233 end 234 end
234 end 235 end
test/ci/install_elasticsearch.sh
@@ -5,15 +5,13 @@ set -e @@ -5,15 +5,13 @@ set -e
5 CACHE_DIR=$HOME/elasticsearch/$ELASTICSEARCH_VERSION 5 CACHE_DIR=$HOME/elasticsearch/$ELASTICSEARCH_VERSION
6 6
7 if [ ! -d "$CACHE_DIR" ]; then 7 if [ ! -d "$CACHE_DIR" ]; then
8 - if [[ $ELASTICSEARCH_VERSION == 1* ]]; then  
9 - URL=https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-$ELASTICSEARCH_VERSION.tar.gz  
10 - elif [[ $ELASTICSEARCH_VERSION == 2* ]]; then  
11 - URL=https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/$ELASTICSEARCH_VERSION/elasticsearch-$ELASTICSEARCH_VERSION.tar.gz 8 + if [[ $ELASTICSEARCH_VERSION == 7* ]]; then
  9 + URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-$ELASTICSEARCH_VERSION-linux-x86_64.tar.gz
12 else 10 else
13 URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-$ELASTICSEARCH_VERSION.tar.gz 11 URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-$ELASTICSEARCH_VERSION.tar.gz
14 fi 12 fi
15 13
16 - wget $URL 14 + wget -O elasticsearch-$ELASTICSEARCH_VERSION.tar.gz $URL
17 tar xvfz elasticsearch-$ELASTICSEARCH_VERSION.tar.gz 15 tar xvfz elasticsearch-$ELASTICSEARCH_VERSION.tar.gz
18 mv elasticsearch-$ELASTICSEARCH_VERSION $CACHE_DIR 16 mv elasticsearch-$ELASTICSEARCH_VERSION $CACHE_DIR
19 else 17 else
test/errors_test.rb
@@ -4,13 +4,15 @@ class ErrorsTest &lt; Minitest::Test @@ -4,13 +4,15 @@ class ErrorsTest &lt; Minitest::Test
4 def test_bulk_import_raises_error 4 def test_bulk_import_raises_error
5 valid_dog = Product.create(name: "2016-01-02") 5 valid_dog = Product.create(name: "2016-01-02")
6 invalid_dog = Product.create(name: "Ol' One-Leg") 6 invalid_dog = Product.create(name: "Ol' One-Leg")
7 - index = Searchkick::Index.new "dogs", mappings: {  
8 - dog: {  
9 - properties: {  
10 - name: {type: "date"}  
11 - } 7 + mapping = {
  8 + properties: {
  9 + name: {type: "date"}
12 } 10 }
13 } 11 }
  12 + mapping = {product: mapping} if Searchkick.server_below?("7.0.0")
  13 + index = Searchkick::Index.new "dogs", mappings: mapping
  14 + index.delete if index.exists?
  15 + index.create_index
14 index.store valid_dog 16 index.store valid_dog
15 assert_raises(Searchkick::ImportError) do 17 assert_raises(Searchkick::ImportError) do
16 index.bulk_index [valid_dog, invalid_dog] 18 index.bulk_index [valid_dog, invalid_dog]
test/gemfiles/activerecord42.gemfile
@@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
1 -source 'https://rubygems.org'  
2 -  
3 -# Specify your gem's dependencies in searchkick.gemspec  
4 -gemspec path: "../../"  
5 -  
6 -gem "sqlite3", "~> 1.3.0"  
7 -gem "activerecord", "~> 4.2.0"  
test/gemfiles/mongoid5.gemfile
@@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
1 -source 'https://rubygems.org'  
2 -  
3 -# Specify your gem's dependencies in searchkick.gemspec  
4 -gemspec path: "../../"  
5 -  
6 -gem "mongoid", "~> 5.0.0"  
7 -gem "activejob"  
test/geo_shape_test.rb
@@ -32,6 +32,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -32,6 +32,9 @@ class GeoShapeTest &lt; Minitest::Test
32 end 32 end
33 33
34 def test_circle 34 def test_circle
  35 + # https://github.com/elastic/elasticsearch/issues/39237
  36 + skip unless Searchkick.server_below?("6.6.0")
  37 +
35 assert_search "*", ["Region A"], { 38 assert_search "*", ["Region A"], {
36 where: { 39 where: {
37 territory: { 40 territory: {
@@ -142,6 +145,9 @@ class GeoShapeTest &lt; Minitest::Test @@ -142,6 +145,9 @@ class GeoShapeTest &lt; Minitest::Test
142 end 145 end
143 146
144 def test_contains 147 def test_contains
  148 + # CONTAINS query relation not supported
  149 + skip unless Searchkick.server_below?("6.6.0")
  150 +
145 assert_search "*", ["Region C"], { 151 assert_search "*", ["Region C"], {
146 where: { 152 where: {
147 territory: { 153 territory: {
test/inheritance_test.rb
@@ -77,6 +77,20 @@ class InheritanceTest &lt; Minitest::Test @@ -77,6 +77,20 @@ class InheritanceTest &lt; Minitest::Test
77 def test_multiple_indices 77 def test_multiple_indices
78 store_names ["Product A"] 78 store_names ["Product A"]
79 store_names ["Product B"], Animal 79 store_names ["Product B"], Animal
80 - assert_search "product", ["Product A", "Product B"], index_name: [Product.searchkick_index.name, Animal.searchkick_index.name], conversions: false 80 + assert_search "product", ["Product A", "Product B"], {models: [Product, Animal], conversions: false}, Searchkick
  81 + assert_search "product", ["Product A", "Product B"], {index_name: [Product, Animal], conversions: false}, Searchkick
  82 + end
  83 +
  84 + def test_index_name_model
  85 + store_names ["Product A"]
  86 + assert_equal ["Product A"], Searchkick.search("product", index_name: [Product]).map(&:name)
  87 + end
  88 +
  89 + def test_index_name_string
  90 + store_names ["Product A"]
  91 + error = assert_raises Searchkick::Error do
  92 + Searchkick.search("product", index_name: [Product.searchkick_index.name]).map(&:name)
  93 + end
  94 + assert_includes error.message, "Unknown model"
81 end 95 end
82 end 96 end
test/models/product.rb
@@ -2,11 +2,8 @@ class Product @@ -2,11 +2,8 @@ class Product
2 searchkick \ 2 searchkick \
3 synonyms: [ 3 synonyms: [
4 ["clorox", "bleach"], 4 ["clorox", "bleach"],
5 - ["scallion", "greenonion"],  
6 - ["saran wrap", "plastic wrap"],  
7 - ["qtip", "cottonswab"],  
8 ["burger", "hamburger"], 5 ["burger", "hamburger"],
9 - ["bandaid", "bandag"], 6 + ["bandaid", "bandages"],
10 ["UPPERCASE", "lowercase"], 7 ["UPPERCASE", "lowercase"],
11 "lightbulb => led,lightbulb", 8 "lightbulb => led,lightbulb",
12 "lightbulb => halogenlamp" 9 "lightbulb => halogenlamp"
test/models/region.rb
1 class Region 1 class Region
2 searchkick \ 2 searchkick \
3 - geo_shape: {  
4 - territory: {tree: "quadtree", precision: "10km"}  
5 - } 3 + geo_shape: [:territory]
6 4
7 attr_accessor :territory 5 attr_accessor :territory
8 6
test/models/store.rb
1 class Store 1 class Store
  2 + mappings = {
  3 + properties: {
  4 + name: {type: "keyword"}
  5 + }
  6 + }
  7 + mappings = {store: mappings} if Searchkick.server_below?("7.0.0")
  8 +
2 searchkick \ 9 searchkick \
3 routing: true, 10 routing: true,
4 merge_mappings: true, 11 merge_mappings: true,
5 - mappings: {  
6 - store: {  
7 - properties: {  
8 - name: {type: "keyword"}  
9 - }  
10 - }  
11 - } 12 + mappings: mappings
12 13
13 def search_document_id 14 def search_document_id
14 id 15 id
test/multi_indices_test.rb
@@ -7,16 +7,44 @@ class MultiIndicesTest &lt; Minitest::Test @@ -7,16 +7,44 @@ class MultiIndicesTest &lt; Minitest::Test
7 assert_search_multi "product", ["Product A", "Product B"] 7 assert_search_multi "product", ["Product A", "Product B"]
8 end 8 end
9 9
10 - def test_where  
11 - store [{name: "Product A", color: "red"}, {name: "Product B", color: "blue"}]  
12 - store_names ["Product C"], Speaker  
13 - assert_search_multi "product", ["Product A", "Product C"], where: {_or: [{_type: "product", color: "red"}, {_type: "speaker"}]} 10 + def test_index_name
  11 + store_names ["Product A"]
  12 + assert_equal ["Product A"], Product.search("product", index_name: Product.searchkick_index.name).map(&:name)
  13 + assert_equal ["Product A"], Product.search("product", index_name: Product).map(&:name)
  14 + assert_equal [], Product.search("product", index_name: Speaker.searchkick_index.name, conversions: false).map(&:name)
  15 + end
  16 +
  17 + def test_models_and_index_name
  18 + store_names ["Product A"]
  19 + store_names ["Product B"], Speaker
  20 + assert_equal ["Product A"], Searchkick.search("product", models: [Product, Store], index_name: Product.searchkick_index.name).map(&:name)
  21 + error = assert_raises(Searchkick::Error) do
  22 + Searchkick.search("product", models: [Product, Store], index_name: Speaker.searchkick_index.name).map(&:name)
  23 + end
  24 + assert_includes error.message, "Unknown model"
  25 + # legacy
  26 + assert_equal ["Product A"], Searchkick.search("product", index_name: [Product, Store]).map(&:name)
  27 + end
  28 +
  29 + def test_model_with_another_model
  30 + error = assert_raises(ArgumentError) do
  31 + Product.search(models: [Store])
  32 + end
  33 + assert_includes error.message, "Use Searchkick.search"
  34 + end
  35 +
  36 + def test_model_with_another_model_in_index_name
  37 + error = assert_raises(ArgumentError) do
  38 + # legacy protection
  39 + Product.search(index_name: [Store, "another"])
  40 + end
  41 + assert_includes error.message, "Use Searchkick.search"
14 end 42 end
15 43
16 private 44 private
17 45
18 def assert_search_multi(term, expected, options = {}) 46 def assert_search_multi(term, expected, options = {})
19 - options[:index_name] = [Product, Speaker] 47 + options[:models] = [Product, Speaker]
20 options[:fields] = [:name] 48 options[:fields] = [:name]
21 assert_search(term, expected, options, Searchkick) 49 assert_search(term, expected, options, Searchkick)
22 end 50 end
test/query_test.rb
@@ -4,18 +4,11 @@ class QueryTest &lt; Minitest::Test @@ -4,18 +4,11 @@ class QueryTest &lt; Minitest::Test
4 def test_basic 4 def test_basic
5 store_names ["Milk", "Apple"] 5 store_names ["Milk", "Apple"]
6 query = Product.search("milk", execute: false) 6 query = Product.search("milk", execute: false)
7 - # query.body = {query: {match_all: {}}}  
8 - # query.body = {query: {match: {name: "Apple"}}}  
9 query.body[:query] = {match_all: {}} 7 query.body[:query] = {match_all: {}}
10 assert_equal ["Apple", "Milk"], query.map(&:name).sort 8 assert_equal ["Apple", "Milk"], query.map(&:name).sort
11 assert_equal ["Apple", "Milk"], query.execute.map(&:name).sort 9 assert_equal ["Apple", "Milk"], query.execute.map(&:name).sort
12 end 10 end
13 11
14 - def test_with_effective_min_score  
15 - store_names ["Milk", "Milk2"]  
16 - assert_search "milk", ["Milk"], body_options: {min_score: 1}  
17 - end  
18 -  
19 def test_with_uneffective_min_score 12 def test_with_uneffective_min_score
20 store_names ["Milk", "Milk2"] 13 store_names ["Milk", "Milk2"]
21 assert_search "milk", ["Milk", "Milk2"], body_options: {min_score: 0.0001} 14 assert_search "milk", ["Milk", "Milk2"], body_options: {min_score: 0.0001}
test/routing_test.rb
@@ -7,8 +7,11 @@ class RoutingTest &lt; Minitest::Test @@ -7,8 +7,11 @@ class RoutingTest &lt; Minitest::Test
7 end 7 end
8 8
9 def test_routing_mappings 9 def test_routing_mappings
10 - index_options = Store.searchkick_index.index_options  
11 - assert_equal index_options[:mappings][:store][:_routing], required: true 10 + mappings = Store.searchkick_index.index_options[:mappings]
  11 + if Searchkick.server_below?("7.0.0")
  12 + mappings = mappings[:store]
  13 + end
  14 + assert_equal mappings[:_routing], required: true
12 end 15 end
13 16
14 def test_routing_correct_node 17 def test_routing_correct_node
test/sql_test.rb
@@ -37,6 +37,7 @@ class SqlTest &lt; Minitest::Test @@ -37,6 +37,7 @@ class SqlTest &lt; Minitest::Test
37 end 37 end
38 38
39 def test_fields_both_match 39 def test_fields_both_match
  40 + # have same score due to dismax
40 store [ 41 store [
41 {name: "Blue A", color: "red"}, 42 {name: "Blue A", color: "red"},
42 {name: "Blue B", color: "light blue"} 43 {name: "Blue B", color: "light blue"}
@@ -172,7 +173,7 @@ class SqlTest &lt; Minitest::Test @@ -172,7 +173,7 @@ class SqlTest &lt; Minitest::Test
172 store_names ["Store A"], Store 173 store_names ["Store A"], Store
173 174
174 associations = {Product => [:store], Store => [:products]} 175 associations = {Product => [:store], Store => [:products]}
175 - result = Searchkick.search("*", index_name: [Product, Store], model_includes: associations) 176 + result = Searchkick.search("*", models: [Product, Store], model_includes: associations)
176 177
177 assert_equal 2, result.length 178 assert_equal 2, result.length
178 179
test/suggest_test.rb
1 require_relative "test_helper" 1 require_relative "test_helper"
2 2
3 class SuggestTest < Minitest::Test 3 class SuggestTest < Minitest::Test
  4 + def setup
  5 + super
  6 + Product.reindex
  7 + end
  8 +
4 def test_basic 9 def test_basic
5 store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"] 10 store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
6 assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark", fields: [:name] 11 assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark", fields: [:name]
test/synonyms_test.rb
@@ -6,11 +6,6 @@ class SynonymsTest &lt; Minitest::Test @@ -6,11 +6,6 @@ class SynonymsTest &lt; Minitest::Test
6 assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"] 6 assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"]
7 end 7 end
8 8
9 - def test_saran_wrap  
10 - store_names ["Saran Wrap", "Kroger Plastic Wrap"]  
11 - assert_search "saran wrap", ["Saran Wrap", "Kroger Plastic Wrap"]  
12 - end  
13 -  
14 def test_burger_buns 9 def test_burger_buns
15 store_names ["Hamburger Buns"] 10 store_names ["Hamburger Buns"]
16 assert_search "burger buns", ["Hamburger Buns"] 11 assert_search "burger buns", ["Hamburger Buns"]
@@ -21,24 +16,14 @@ class SynonymsTest &lt; Minitest::Test @@ -21,24 +16,14 @@ class SynonymsTest &lt; Minitest::Test
21 assert_search "bandaids", ["Band-Aid", "Kroger 12-Pack Bandages"] 16 assert_search "bandaids", ["Band-Aid", "Kroger 12-Pack Bandages"]
22 end 17 end
23 18
24 - def test_qtips  
25 - store_names ["Q Tips", "Kroger Cotton Swabs"]  
26 - assert_search "q tips", ["Q Tips", "Kroger Cotton Swabs"]  
27 - end  
28 -  
29 def test_reverse 19 def test_reverse
30 - store_names ["Scallions"]  
31 - assert_search "green onions", ["Scallions"]  
32 - end  
33 -  
34 - def test_exact  
35 - store_names ["Green Onions", "Yellow Onions"]  
36 - assert_search "scallion", ["Green Onions"] 20 + store_names ["Hamburger"]
  21 + assert_search "burger", ["Hamburger"]
37 end 22 end
38 23
39 def test_stemmed 24 def test_stemmed
40 - store_names ["Green Onions", "Yellow Onions"]  
41 - assert_search "scallions", ["Green Onions"] 25 + store_names ["Burger"]
  26 + assert_search "hamburgers", ["Burger"]
42 end 27 end
43 28
44 def test_word_start 29 def test_word_start