Commit 7ce0440ab4f2fdd02a52b2bcf42fb4f90e668667
1 parent
4054147c
Exists in
master
and in
17 other branches
Added support for Elasticsearch 7
Showing
27 changed files
with
219 additions
and
203 deletions
Show diff stats
.travis.yml
... | ... | @@ -14,30 +14,24 @@ cache: |
14 | 14 | directories: |
15 | 15 | - $HOME/elasticsearch |
16 | 16 | env: |
17 | - - ELASTICSEARCH_VERSION=6.7.0 | |
17 | + - ELASTICSEARCH_VERSION=7.0.0 | |
18 | 18 | jdk: openjdk10 |
19 | 19 | matrix: |
20 | 20 | include: |
21 | 21 | - gemfile: Gemfile |
22 | 22 | - gemfile: test/gemfiles/activerecord51.gemfile |
23 | - env: ELASTICSEARCH_VERSION=6.0.0 | |
23 | + env: ELASTICSEARCH_VERSION=7.0.0 | |
24 | 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 | 28 | services: |
30 | 29 | - mongodb |
31 | 30 | - redis-server |
32 | - - gemfile: test/gemfiles/mongoid5.gemfile | |
31 | + - gemfile: test/gemfiles/mongoid6.gemfile | |
33 | 32 | services: |
34 | 33 | - mongodb |
35 | 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 | 35 | notifications: |
42 | 36 | email: |
43 | 37 | on_success: never | ... | ... |
CHANGELOG.md
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 | 11 | ## 3.1.3 |
2 | 12 | |
3 | 13 | - Added support for endless ranges | ... | ... |
README.md
... | ... | @@ -53,7 +53,7 @@ Add this line to your application’s Gemfile: |
53 | 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 | 58 | Add searchkick to models you want to search. |
59 | 59 | |
... | ... | @@ -312,13 +312,13 @@ A few languages require plugins: |
312 | 312 | |
313 | 313 | ```ruby |
314 | 314 | class Product < ApplicationRecord |
315 | - searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]] | |
315 | + searchkick synonyms: [["burger", "hamburger"], ["sneakers", "shoes"]] | |
316 | 316 | end |
317 | 317 | ``` |
318 | 318 | |
319 | 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 | 323 | To read synonyms from a file, use: |
324 | 324 | |
... | ... | @@ -796,8 +796,6 @@ Script support |
796 | 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 | 799 | Date histogram |
802 | 800 | |
803 | 801 | ```ruby |
... | ... | @@ -924,9 +922,7 @@ You can also index and search geo shapes. |
924 | 922 | |
925 | 923 | ```ruby |
926 | 924 | class Restaurant < ApplicationRecord |
927 | - searchkick geo_shape: { | |
928 | - bounds: {tree: "geohash", precision: "1km"} | |
929 | - } | |
925 | + searchkick geo_shape: [:bounds] | |
930 | 926 | |
931 | 927 | def search_data |
932 | 928 | attributes.merge( |
... | ... | @@ -959,12 +955,6 @@ Not touching the query shape |
959 | 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 | 958 | ## Inheritance |
969 | 959 | |
970 | 960 | Searchkick supports single table inheritance. |
... | ... | @@ -1496,21 +1486,15 @@ Then use `products` and `coupons` as typical results. |
1496 | 1486 | |
1497 | 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 | 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 | 1499 | ```ruby |
1516 | 1500 | indices_boost: {Category => 2, Product => 1} |
... | ... | @@ -1657,7 +1641,7 @@ Product.search "milk", includes: [:brand, :stores] |
1657 | 1641 | Eager load different associations by model |
1658 | 1642 | |
1659 | 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 | 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 | 1884 | |
1901 | 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 | 1892 | ## Elasticsearch 5 to 6 Upgrade |
1904 | 1893 | |
1905 | 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 | 78 | Gem::Version.new(server_version.split("-")[0]) < Gem::Version.new(version.split("-")[0]) |
79 | 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 | 89 | def self.search(term = "*", model: nil, **options, &block) |
82 | 90 | options = options.dup |
83 | 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 | 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 | 111 | end |
92 | 112 | end |
93 | 113 | ... | ... |
lib/searchkick/index.rb
... | ... | @@ -17,7 +17,7 @@ module Searchkick |
17 | 17 | end |
18 | 18 | |
19 | 19 | def delete |
20 | - if !Searchkick.server_below?("6.0.0") && alias_exists? | |
20 | + if alias_exists? | |
21 | 21 | # can't call delete directly on aliases in ES 6 |
22 | 22 | indices = client.indices.get_alias(name: name).keys |
23 | 23 | client.indices.delete index: indices |
... | ... | @@ -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 | 72 | end |
73 | 73 | |
74 | 74 | def promote(new_name, update_refresh_interval: false) | ... | ... |
lib/searchkick/index_options.rb
... | ... | @@ -4,15 +4,13 @@ module Searchkick |
4 | 4 | options = @options |
5 | 5 | language = options[:language] |
6 | 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 | 8 | if options[:mappings] && !options[:merge_mappings] |
11 | 9 | settings = options[:settings] || {} |
12 | 10 | mappings = options[:mappings] |
13 | 11 | else |
14 | - below60 = Searchkick.server_below?("6.0.0") | |
15 | 12 | below62 = Searchkick.server_below?("6.2.0") |
13 | + below70 = Searchkick.server_below?("7.0.0") | |
16 | 14 | |
17 | 15 | default_type = "text" |
18 | 16 | default_analyzer = :searchkick_index |
... | ... | @@ -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 | 145 | stem = options[:stem] |
157 | 146 | |
158 | 147 | case language |
... | ... | @@ -279,8 +268,7 @@ module Searchkick |
279 | 268 | # - Only apply the synonym expansion at index time |
280 | 269 | # - Don't have the synonym filter applied search |
281 | 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 | 273 | %w(word_start word_middle word_end).each do |type| |
286 | 274 | settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym") |
... | ... | @@ -391,10 +379,6 @@ module Searchkick |
391 | 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 | 382 | if options.key?(:filterable) |
399 | 383 | dynamic_fields["{name}"] = {type: default_type, index: index_false_value} |
400 | 384 | end |
... | ... | @@ -413,25 +397,24 @@ module Searchkick |
413 | 397 | multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}")) |
414 | 398 | |
415 | 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 | 418 | end |
436 | 419 | |
437 | 420 | mappings = mappings.symbolize_keys.deep_merge((options[:mappings] || {}).symbolize_keys) | ... | ... |
lib/searchkick/query.rb
... | ... | @@ -18,7 +18,7 @@ module Searchkick |
18 | 18 | unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost, |
19 | 19 | :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :execute, :explain, |
20 | 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 | 22 | :request_params, :routing, :scope_results, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where] |
23 | 23 | raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any? |
24 | 24 | |
... | ... | @@ -39,6 +39,7 @@ module Searchkick |
39 | 39 | @misspellings = false |
40 | 40 | @misspellings_below = nil |
41 | 41 | @highlighted_fields = nil |
42 | + @index_mapping = nil | |
42 | 43 | |
43 | 44 | prepare |
44 | 45 | end |
... | ... | @@ -56,9 +57,18 @@ module Searchkick |
56 | 57 | end |
57 | 58 | |
58 | 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 | 67 | index = |
60 | 68 | if options[:index_name] |
61 | 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 | 72 | elsif searchkick_index |
63 | 73 | searchkick_index.name |
64 | 74 | else |
... | ... | @@ -116,8 +126,8 @@ module Searchkick |
116 | 126 | misspellings: @misspellings, |
117 | 127 | term: term, |
118 | 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 | 133 | if options[:debug] |
... | ... | @@ -166,7 +176,7 @@ module Searchkick |
166 | 176 | end |
167 | 177 | |
168 | 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 | 180 | end |
171 | 181 | |
172 | 182 | private |
... | ... | @@ -377,7 +387,7 @@ module Searchkick |
377 | 387 | queries_to_add.concat(q2) |
378 | 388 | end |
379 | 389 | |
380 | - queries.concat(queries_to_add) | |
390 | + queries << queries_to_add | |
381 | 391 | |
382 | 392 | if options[:exclude] |
383 | 393 | must_not.concat(set_exclude(exclude_field, exclude_analyzer)) |
... | ... | @@ -392,9 +402,10 @@ module Searchkick |
392 | 402 | |
393 | 403 | should = [] |
394 | 404 | else |
405 | + # higher score for matching more fields | |
395 | 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 | 674 | def set_boost_by_indices(payload) |
664 | 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 | 680 | end |
681 | 681 | |
682 | 682 | payload[:indices_boost] = indices_boost |
... | ... | @@ -713,7 +713,7 @@ module Searchkick |
713 | 713 | def set_highlights(payload, fields) |
714 | 714 | payload[:highlight] = { |
715 | 715 | fields: Hash[fields.map { |f| [f, {}] }], |
716 | - fragment_size: below60? ? 30000 : 0 | |
716 | + fragment_size: 0 | |
717 | 717 | } |
718 | 718 | |
719 | 719 | if options[:highlight].is_a?(Hash) |
... | ... | @@ -824,7 +824,7 @@ module Searchkick |
824 | 824 | # TODO id transformation for arrays |
825 | 825 | def set_order(payload) |
826 | 826 | order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc} |
827 | - id_field = below60? ? :_uid : :_id | |
827 | + id_field = :_id | |
828 | 828 | payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }] |
829 | 829 | end |
830 | 830 | |
... | ... | @@ -1021,16 +1021,12 @@ module Searchkick |
1021 | 1021 | k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "") |
1022 | 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 | 1024 | def below61? |
1033 | 1025 | Searchkick.server_below?("6.1.0") |
1034 | 1026 | end |
1027 | + | |
1028 | + def below70? | |
1029 | + Searchkick.server_below?("7.0.0") | |
1030 | + end | |
1035 | 1031 | end |
1036 | 1032 | end | ... | ... |
lib/searchkick/record_data.rb
... | ... | @@ -34,18 +34,13 @@ module Searchkick |
34 | 34 | index.klass_document_type(record.class, ignore_type) |
35 | 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 | 37 | def record_data |
43 | 38 | data = { |
44 | 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 | 44 | data |
50 | 45 | end |
51 | 46 | ... | ... |
lib/searchkick/results.rb
... | ... | @@ -25,9 +25,16 @@ module Searchkick |
25 | 25 | # results can have different types |
26 | 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 | 38 | end |
32 | 39 | |
33 | 40 | missing_ids = [] |
... | ... | @@ -35,7 +42,7 @@ module Searchkick |
35 | 42 | # sort |
36 | 43 | results = |
37 | 44 | hits.map do |hit| |
38 | - result = results[hit["_type"]][hit["_id"].to_s] | |
45 | + result = results[hit["_index"]][hit["_id"].to_s] | |
39 | 46 | if result && !(options[:load].is_a?(Hash) && options[:load][:dumpable]) |
40 | 47 | if (hit["highlight"] || options[:highlight]) && !result.respond_to?(:search_highlights) |
41 | 48 | highlights = hit_highlights(hit) |
... | ... | @@ -132,7 +139,13 @@ module Searchkick |
132 | 139 | end |
133 | 140 | |
134 | 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 | 149 | end |
137 | 150 | alias_method :total_entries, :total_count |
138 | 151 | ... | ... |
searchkick.gemspec
... | ... | @@ -16,10 +16,10 @@ Gem::Specification.new do |spec| |
16 | 16 | spec.files = Dir["*.{md,txt}", "{lib}/**/*"] |
17 | 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 | 23 | spec.add_dependency "hashie" |
24 | 24 | |
25 | 25 | spec.add_development_dependency "bundler" | ... | ... |
test/aggs_test.rb
... | ... | @@ -20,8 +20,7 @@ class AggsTest < Minitest::Test |
20 | 20 | end |
21 | 21 | |
22 | 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 | 24 | assert_equal %w(red green blue), agg["buckets"].map { |b| b["key"] } |
26 | 25 | end |
27 | 26 | |
... | ... | @@ -37,8 +36,7 @@ class AggsTest < Minitest::Test |
37 | 36 | |
38 | 37 | def test_script |
39 | 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 | 40 | assert_equal ({"Color: blue" => 1, "Color: green" => 1, "Color: red" => 1}), buckets_as_hash(agg) |
43 | 41 | end |
44 | 42 | ... | ... |
test/boost_test.rb
... | ... | @@ -67,6 +67,7 @@ class BoostTest < Minitest::Test |
67 | 67 | end |
68 | 68 | |
69 | 69 | def test_conversions_weight |
70 | + Product.reindex | |
70 | 71 | store [ |
71 | 72 | {name: "Product Boost", orders_count: 20}, |
72 | 73 | {name: "Product Conversions", conversions: {"product" => 10}} |
... | ... | @@ -229,6 +230,6 @@ class BoostTest < Minitest::Test |
229 | 230 | store_names ["Rex"], Animal |
230 | 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 | 234 | end |
234 | 235 | end | ... | ... |
test/ci/install_elasticsearch.sh
... | ... | @@ -5,15 +5,13 @@ set -e |
5 | 5 | CACHE_DIR=$HOME/elasticsearch/$ELASTICSEARCH_VERSION |
6 | 6 | |
7 | 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 | 10 | else |
13 | 11 | URL=https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-$ELASTICSEARCH_VERSION.tar.gz |
14 | 12 | fi |
15 | 13 | |
16 | - wget $URL | |
14 | + wget -O elasticsearch-$ELASTICSEARCH_VERSION.tar.gz $URL | |
17 | 15 | tar xvfz elasticsearch-$ELASTICSEARCH_VERSION.tar.gz |
18 | 16 | mv elasticsearch-$ELASTICSEARCH_VERSION $CACHE_DIR |
19 | 17 | else | ... | ... |
test/errors_test.rb
... | ... | @@ -4,13 +4,15 @@ class ErrorsTest < Minitest::Test |
4 | 4 | def test_bulk_import_raises_error |
5 | 5 | valid_dog = Product.create(name: "2016-01-02") |
6 | 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 | 16 | index.store valid_dog |
15 | 17 | assert_raises(Searchkick::ImportError) do |
16 | 18 | index.bulk_index [valid_dog, invalid_dog] | ... | ... |
test/gemfiles/activerecord42.gemfile
test/gemfiles/mongoid5.gemfile
test/geo_shape_test.rb
... | ... | @@ -32,6 +32,9 @@ class GeoShapeTest < Minitest::Test |
32 | 32 | end |
33 | 33 | |
34 | 34 | def test_circle |
35 | + # https://github.com/elastic/elasticsearch/issues/39237 | |
36 | + skip unless Searchkick.server_below?("6.6.0") | |
37 | + | |
35 | 38 | assert_search "*", ["Region A"], { |
36 | 39 | where: { |
37 | 40 | territory: { |
... | ... | @@ -142,6 +145,9 @@ class GeoShapeTest < Minitest::Test |
142 | 145 | end |
143 | 146 | |
144 | 147 | def test_contains |
148 | + # CONTAINS query relation not supported | |
149 | + skip unless Searchkick.server_below?("6.6.0") | |
150 | + | |
145 | 151 | assert_search "*", ["Region C"], { |
146 | 152 | where: { |
147 | 153 | territory: { | ... | ... |
test/inheritance_test.rb
... | ... | @@ -77,6 +77,20 @@ class InheritanceTest < Minitest::Test |
77 | 77 | def test_multiple_indices |
78 | 78 | store_names ["Product A"] |
79 | 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 | 95 | end |
82 | 96 | end | ... | ... |
test/models/product.rb
... | ... | @@ -2,11 +2,8 @@ class Product |
2 | 2 | searchkick \ |
3 | 3 | synonyms: [ |
4 | 4 | ["clorox", "bleach"], |
5 | - ["scallion", "greenonion"], | |
6 | - ["saran wrap", "plastic wrap"], | |
7 | - ["qtip", "cottonswab"], | |
8 | 5 | ["burger", "hamburger"], |
9 | - ["bandaid", "bandag"], | |
6 | + ["bandaid", "bandages"], | |
10 | 7 | ["UPPERCASE", "lowercase"], |
11 | 8 | "lightbulb => led,lightbulb", |
12 | 9 | "lightbulb => halogenlamp" | ... | ... |
test/models/region.rb
test/models/store.rb
1 | 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 | 9 | searchkick \ |
3 | 10 | routing: true, |
4 | 11 | merge_mappings: true, |
5 | - mappings: { | |
6 | - store: { | |
7 | - properties: { | |
8 | - name: {type: "keyword"} | |
9 | - } | |
10 | - } | |
11 | - } | |
12 | + mappings: mappings | |
12 | 13 | |
13 | 14 | def search_document_id |
14 | 15 | id | ... | ... |
test/multi_indices_test.rb
... | ... | @@ -7,16 +7,44 @@ class MultiIndicesTest < Minitest::Test |
7 | 7 | assert_search_multi "product", ["Product A", "Product B"] |
8 | 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 | 42 | end |
15 | 43 | |
16 | 44 | private |
17 | 45 | |
18 | 46 | def assert_search_multi(term, expected, options = {}) |
19 | - options[:index_name] = [Product, Speaker] | |
47 | + options[:models] = [Product, Speaker] | |
20 | 48 | options[:fields] = [:name] |
21 | 49 | assert_search(term, expected, options, Searchkick) |
22 | 50 | end | ... | ... |
test/query_test.rb
... | ... | @@ -4,18 +4,11 @@ class QueryTest < Minitest::Test |
4 | 4 | def test_basic |
5 | 5 | store_names ["Milk", "Apple"] |
6 | 6 | query = Product.search("milk", execute: false) |
7 | - # query.body = {query: {match_all: {}}} | |
8 | - # query.body = {query: {match: {name: "Apple"}}} | |
9 | 7 | query.body[:query] = {match_all: {}} |
10 | 8 | assert_equal ["Apple", "Milk"], query.map(&:name).sort |
11 | 9 | assert_equal ["Apple", "Milk"], query.execute.map(&:name).sort |
12 | 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 | 12 | def test_with_uneffective_min_score |
20 | 13 | store_names ["Milk", "Milk2"] |
21 | 14 | assert_search "milk", ["Milk", "Milk2"], body_options: {min_score: 0.0001} | ... | ... |
test/routing_test.rb
... | ... | @@ -7,8 +7,11 @@ class RoutingTest < Minitest::Test |
7 | 7 | end |
8 | 8 | |
9 | 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 | 15 | end |
13 | 16 | |
14 | 17 | def test_routing_correct_node | ... | ... |
test/sql_test.rb
... | ... | @@ -37,6 +37,7 @@ class SqlTest < Minitest::Test |
37 | 37 | end |
38 | 38 | |
39 | 39 | def test_fields_both_match |
40 | + # have same score due to dismax | |
40 | 41 | store [ |
41 | 42 | {name: "Blue A", color: "red"}, |
42 | 43 | {name: "Blue B", color: "light blue"} |
... | ... | @@ -172,7 +173,7 @@ class SqlTest < Minitest::Test |
172 | 173 | store_names ["Store A"], Store |
173 | 174 | |
174 | 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 | 178 | assert_equal 2, result.length |
178 | 179 | ... | ... |
test/suggest_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | 3 | class SuggestTest < Minitest::Test |
4 | + def setup | |
5 | + super | |
6 | + Product.reindex | |
7 | + end | |
8 | + | |
4 | 9 | def test_basic |
5 | 10 | store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"] |
6 | 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 < Minitest::Test |
6 | 6 | assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"] |
7 | 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 | 9 | def test_burger_buns |
15 | 10 | store_names ["Hamburger Buns"] |
16 | 11 | assert_search "burger buns", ["Hamburger Buns"] |
... | ... | @@ -21,24 +16,14 @@ class SynonymsTest < Minitest::Test |
21 | 16 | assert_search "bandaids", ["Band-Aid", "Kroger 12-Pack Bandages"] |
22 | 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 | 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 | 22 | end |
38 | 23 | |
39 | 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 | 27 | end |
43 | 28 | |
44 | 29 | def test_word_start | ... | ... |