Commit 668b7948df1d84548690633968de0174c539fc9b

Authored by Andrew Kane
1 parent 278ea36f

Added support for Elasticsearch 5.0 alpha

CHANGELOG.md
1   -## 1.2.2 [unreleased]
  1 +## 1.3.0 [unreleased]
2 2  
  3 +- Added support for Elasticsearch 5.0 alpha
3 4 - Added support for phrase matches
4 5 - Added support for procs for `index_prefix` option
5 6  
... ...
lib/searchkick.rb
... ... @@ -58,7 +58,7 @@ module Searchkick
58 58 @server_version ||= client.info["version"]["number"]
59 59 end
60 60  
61   - def self.below_version?(version)
  61 + def self.server_below?(version)
62 62 Gem::Version.new(server_version) < Gem::Version.new(version)
63 63 end
64 64  
... ...
lib/searchkick/index.rb
... ... @@ -218,6 +218,22 @@ module Searchkick
218 218 settings = options[:settings] || {}
219 219 mappings = options[:mappings]
220 220 else
  221 + below22 = Searchkick.server_below?("2.2.0")
  222 + below50 = Searchkick.server_below?("5.0.0-alpha1")
  223 + default_type = below50 ? "string" : "text"
  224 + default_analyzer = below50 ? :default_index : :default
  225 + keyword_mapping =
  226 + if below50
  227 + {
  228 + type: default_type,
  229 + index: "not_analyzed"
  230 + }
  231 + else
  232 + {
  233 + type: "keyword"
  234 + }
  235 + end
  236 +
221 237 settings = {
222 238 analysis: {
223 239 analyzer: {
... ... @@ -226,7 +242,7 @@ module Searchkick
226 242 tokenizer: "keyword",
227 243 filter: ["lowercase"] + (options[:stem_conversions] == false ? [] : ["searchkick_stemmer"])
228 244 },
229   - default_index: {
  245 + default_analyzer => {
230 246 type: "custom",
231 247 # character filters -> tokenizer -> token filters
232 248 # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
... ... @@ -380,8 +396,8 @@ module Searchkick
380 396 # - Only apply the synonym expansion at index time
381 397 # - Don't have the synonym filter applied search
382 398 # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
383   - settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_synonym")
384   - settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
  399 + settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym")
  400 + settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
385 401  
386 402 %w(word_start word_middle word_end).each do |type|
387 403 settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
... ... @@ -395,8 +411,8 @@ module Searchkick
395 411 synonyms_path: Searchkick.wordnet_path
396 412 }
397 413  
398   - settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_wordnet")
399   - settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_wordnet"
  414 + settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
  415 + settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
400 416  
401 417 %w(word_start word_middle word_end).each do |type|
402 418 settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
... ... @@ -416,7 +432,7 @@ module Searchkick
416 432 mapping[conversions_field] = {
417 433 type: "nested",
418 434 properties: {
419   - query: {type: "string", analyzer: "searchkick_keyword"},
  435 + query: {type: default_type, analyzer: "searchkick_keyword"},
420 436 count: {type: "integer"}
421 437 }
422 438 }
... ... @@ -430,32 +446,39 @@ module Searchkick
430 446 word = options[:word] != false && (!options[:match] || options[:match] == :word)
431 447  
432 448 mapping_options.values.flatten.uniq.each do |field|
433   - field_mapping = {
434   - type: "multi_field",
435   - fields: {}
436   - }
  449 + fields = {}
437 450  
438   - unless mapping_options[:only_analyzed].include?(field)
439   - field_mapping[:fields][field] = {type: "string", index: "not_analyzed"}
  451 + if mapping_options[:only_analyzed].include?(field)
  452 + fields[field] = {type: default_type, index: "no"}
  453 + else
  454 + fields[field] = keyword_mapping
440 455 end
441 456  
442 457 if !options[:searchable] || mapping_options[:searchable].include?(field)
443 458 if word
444   - field_mapping[:fields]["analyzed"] = {type: "string", index: "analyzed"}
  459 + fields["analyzed"] = {type: default_type, index: "analyzed", analyzer: default_analyzer}
445 460  
446 461 if mapping_options[:highlight].include?(field)
447   - field_mapping[:fields]["analyzed"][:term_vector] = "with_positions_offsets"
  462 + fields["analyzed"][:term_vector] = "with_positions_offsets"
448 463 end
449 464 end
450 465  
451   - mapping_options.except(:highlight, :searchable, :only_analyzed).each do |type, fields|
452   - if options[:match] == type || fields.include?(field)
453   - field_mapping[:fields][type] = {type: "string", index: "analyzed", analyzer: "searchkick_#{type}_index"}
  466 + mapping_options.except(:highlight, :searchable, :only_analyzed).each do |type, f|
  467 + if options[:match] == type || f.include?(field)
  468 + fields[type] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{type}_index"}
454 469 end
455 470 end
456 471 end
457 472  
458   - mapping[field] = field_mapping
  473 + mapping[field] =
  474 + if below50
  475 + {
  476 + type: "multi_field",
  477 + fields: fields
  478 + }
  479 + elsif fields[field]
  480 + fields[field].merge(fields: fields.except(field))
  481 + end
459 482 end
460 483  
461 484 (options[:locations] || []).map(&:to_s).each do |field|
... ... @@ -466,7 +489,7 @@ module Searchkick
466 489  
467 490 (options[:unsearchable] || []).map(&:to_s).each do |field|
468 491 mapping[field] = {
469   - type: "string",
  492 + type: default_type,
470 493 index: "no"
471 494 }
472 495 end
... ... @@ -484,21 +507,35 @@ module Searchkick
484 507 # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
485 508 # however, we can include the not_analyzed field in _all
486 509 # and the _all index analyzer will take care of it
487   - "{name}" => {type: "string", index: "not_analyzed", include_in_all: !options[:searchable]}
  510 + "{name}" => keyword_mapping.merge(include_in_all: !options[:searchable])
488 511 }
489 512  
  513 + dynamic_fields["{name}"][:ignore_above] = 256 unless below22
  514 +
490 515 unless options[:searchable]
491 516 if options[:match] && options[:match] != :word
492   - dynamic_fields[options[:match]] = {type: "string", index: "analyzed", analyzer: "searchkick_#{options[:match]}_index"}
  517 + dynamic_fields[options[:match]] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{options[:match]}_index"}
493 518 end
494 519  
495 520 if word
496   - dynamic_fields["analyzed"] = {type: "string", index: "analyzed"}
  521 + dynamic_fields["analyzed"] = {type: default_type, index: "analyzed"}
497 522 end
498 523 end
499 524  
  525 + # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
  526 + multi_field =
  527 + if below50
  528 + {
  529 + type: "multi_field",
  530 + fields: dynamic_fields
  531 + }
  532 + else
  533 + dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
  534 + end
  535 +
500 536 mappings = {
501 537 _default_: {
  538 + _all: {type: default_type, index: "analyzed", analyzer: default_analyzer},
502 539 properties: mapping,
503 540 _routing: routing,
504 541 # https://gist.github.com/kimchy/2898285
... ... @@ -507,11 +544,7 @@ module Searchkick
507 544 string_template: {
508 545 match: "*",
509 546 match_mapping_type: "string",
510   - mapping: {
511   - # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
512   - type: "multi_field",
513   - fields: dynamic_fields
514   - }
  547 + mapping: multi_field
515 548 }
516 549 }
517 550 ]
... ...
lib/searchkick/query.rb
... ... @@ -234,7 +234,6 @@ module Searchkick
234 234 factor = boost_fields[field] || 1
235 235 shared_options = {
236 236 query: term,
237   - operator: operator,
238 237 boost: 10 * factor
239 238 }
240 239  
... ... @@ -246,6 +245,8 @@ module Searchkick
246 245 :match
247 246 end
248 247  
  248 + shared_options[:operator] = operator if match_type == :match || below50?
  249 +
249 250 if field == "_all" || field.end_with?(".analyzed")
250 251 shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
251 252 qs.concat [
... ... @@ -260,7 +261,7 @@ module Searchkick
260 261 qs << shared_options.merge(analyzer: analyzer)
261 262 end
262 263  
263   - if misspellings != false
  264 + if misspellings != false && (match_type == :match || below50?)
264 265 qs.concat qs.map { |q| q.except(:cutoff_frequency).merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: max_expansions, boost: factor).merge(transpositions) }
265 266 end
266 267  
... ... @@ -625,26 +626,44 @@ module Searchkick
625 626  
626 627 def set_filters(payload, filters)
627 628 if options[:facets] || options[:aggs]
628   - payload[:filter] = {
629   - and: filters
630   - }
  629 + if below20?
  630 + payload[:filter] = {
  631 + and: filters
  632 + }
  633 + else
  634 + payload[:post_filter] = {
  635 + bool: {
  636 + filter: filters
  637 + }
  638 + }
  639 + end
631 640 else
632 641 # more efficient query if no facets
633   - payload[:query] = {
634   - filtered: {
635   - query: payload[:query],
636   - filter: {
637   - and: filters
  642 + if below20?
  643 + payload[:query] = {
  644 + filtered: {
  645 + query: payload[:query],
  646 + filter: {
  647 + and: filters
  648 + }
638 649 }
639 650 }
640   - }
  651 + else
  652 + payload[:query] = {
  653 + bool: {
  654 + must: payload[:query],
  655 + filter: filters
  656 + }
  657 + }
  658 + end
641 659 end
642 660 end
643 661  
  662 + # TODO id transformation for arrays
644 663 def set_order(payload)
645 664 order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
646   - # TODO id transformation for arrays
647   - payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? :_id : k, v] }]
  665 + id_field = below50? ? :_id : :_uid
  666 + payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? id_field : k, v] }]
648 667 end
649 668  
650 669 def where_filters(where)
... ... @@ -654,7 +673,11 @@ module Searchkick
654 673  
655 674 if field == :or
656 675 value.each do |or_clause|
657   - filters << {or: or_clause.map { |or_statement| {and: where_filters(or_statement)} }}
  676 + if below50?
  677 + filters << {or: or_clause.map { |or_statement| {and: where_filters(or_statement)} }}
  678 + else
  679 + filters << {bool: {should: or_clause.map { |or_statement| {bool: {filter: where_filters(or_statement)}} }}}
  680 + end
658 681 end
659 682 else
660 683 # expand ranges
... ... @@ -688,7 +711,11 @@ module Searchkick
688 711 when :regexp # support for regexp queries without using a regexp ruby object
689 712 filters << {regexp: {field => {value: op_value}}}
690 713 when :not # not equal
691   - filters << {not: {filter: term_filters(field, op_value)}}
  714 + if below50?
  715 + filters << {not: {filter: term_filters(field, op_value)}}
  716 + else
  717 + filters << {bool: {must_not: term_filters(field, op_value)}}
  718 + end
692 719 when :all
693 720 op_value.each do |value|
694 721 filters << term_filters(field, value)
... ... @@ -728,12 +755,20 @@ module Searchkick
728 755 def term_filters(field, value)
729 756 if value.is_a?(Array) # in query
730 757 if value.any?(&:nil?)
731   - {or: [term_filters(field, nil), term_filters(field, value.compact)]}
  758 + if below50?
  759 + {or: [term_filters(field, nil), term_filters(field, value.compact)]}
  760 + else
  761 + {bool: {should: [term_filters(field, nil), term_filters(field, value.compact)]}}
  762 + end
732 763 else
733 764 {in: {field => value}}
734 765 end
735 766 elsif value.nil?
736   - {missing: {"field" => field, existence: true, null_value: true}}
  767 + if below50?
  768 + {missing: {field: field, existence: true, null_value: true}}
  769 + else
  770 + {bool: {must_not: {exists: {field: field}}}}
  771 + end
737 772 elsif value.is_a?(Regexp)
738 773 {regexp: {field => {value: value.source}}}
739 774 else
... ... @@ -742,12 +777,19 @@ module Searchkick
742 777 end
743 778  
744 779 def custom_filter(field, value, factor)
745   - {
746   - filter: {
747   - and: where_filters(field => value)
748   - },
749   - boost_factor: factor
750   - }
  780 + if below50?
  781 + {
  782 + filter: {
  783 + and: where_filters(field => value)
  784 + },
  785 + boost_factor: factor
  786 + }
  787 + else
  788 + {
  789 + filter: where_filters(field => value),
  790 + weight: factor
  791 + }
  792 + end
751 793 end
752 794  
753 795 def boost_filters(boost_by, options = {})
... ... @@ -781,15 +823,19 @@ module Searchkick
781 823 end
782 824  
783 825 def below12?
784   - Searchkick.below_version?("1.2.0")
  826 + Searchkick.server_below?("1.2.0")
785 827 end
786 828  
787 829 def below14?
788   - Searchkick.below_version?("1.4.0")
  830 + Searchkick.server_below?("1.4.0")
789 831 end
790 832  
791 833 def below20?
792   - Searchkick.below_version?("2.0.0")
  834 + Searchkick.server_below?("2.0.0")
  835 + end
  836 +
  837 + def below50?
  838 + Searchkick.server_below?("5.0.0-alpha1")
793 839 end
794 840 end
795 841 end
... ...
test/match_test.rb
... ... @@ -110,14 +110,14 @@ class MatchTest &lt; Minitest::Test
110 110 end
111 111  
112 112 def test_misspelling_zucchini_transposition
113   - skip unless elasticsearch_below14?
  113 + skip if elasticsearch_below14?
114 114 store_names ["zucchini"]
115 115 assert_search "zuccihni", ["zucchini"]
116 116 assert_search "zuccihni", [], misspellings: {transpositions: false}
117 117 end
118 118  
119 119 def test_misspelling_lasagna
120   - skip unless elasticsearch_below14?
  120 + skip if elasticsearch_below14?
121 121 store_names ["lasagna"]
122 122 assert_search "lasanga", ["lasagna"], misspellings: {transpositions: true}
123 123 assert_search "lasgana", ["lasagna"], misspellings: {transpositions: true}
... ... @@ -126,7 +126,7 @@ class MatchTest &lt; Minitest::Test
126 126 end
127 127  
128 128 def test_misspelling_lasagna_pasta
129   - skip unless elasticsearch_below14?
  129 + skip if elasticsearch_below14?
130 130 store_names ["lasagna pasta"]
131 131 assert_search "lasanga", ["lasagna pasta"], misspellings: {transpositions: true}
132 132 assert_search "lasanga pasta", ["lasagna pasta"], misspellings: {transpositions: true}
... ...
test/order_test.rb
... ... @@ -28,9 +28,15 @@ class OrderTest &lt; Minitest::Test
28 28 end
29 29  
30 30 def test_order_ignore_unmapped
  31 + skip unless elasticsearch_below50?
31 32 assert_order "product", [], order: {not_mapped: {ignore_unmapped: true}}
32 33 end
33 34  
  35 + def test_order_unmapped_type
  36 + skip if elasticsearch_below50?
  37 + assert_order "product", [], order: {not_mapped: {unmapped_type: "long"}}
  38 + end
  39 +
34 40 def test_order_array
35 41 store [{name: "San Francisco", latitude: 37.7833, longitude: -122.4167}]
36 42 assert_order "francisco", ["San Francisco"], order: [{_geo_distance: {location: "0,0"}}]
... ...
test/sql_test.rb
... ... @@ -77,6 +77,7 @@ class SqlTest &lt; Minitest::Test
77 77 # select
78 78  
79 79 def test_select
  80 + skip unless elasticsearch_below50?
80 81 store [{name: "Product A", store_id: 1}]
81 82 result = Product.search("product", load: false, select: [:name, :store_id]).first
82 83 assert_equal %w(id name store_id), result.keys.reject { |k| k.start_with?("_") }.sort
... ... @@ -85,12 +86,14 @@ class SqlTest &lt; Minitest::Test
85 86 end
86 87  
87 88 def test_select_array
  89 + skip unless elasticsearch_below50?
88 90 store [{name: "Product A", user_ids: [1, 2]}]
89 91 result = Product.search("product", load: false, select: [:user_ids]).first
90 92 assert_equal [1, 2], result.user_ids
91 93 end
92 94  
93 95 def test_select_single_field
  96 + skip unless elasticsearch_below50?
94 97 store [{name: "Product A", store_id: 1}]
95 98 result = Product.search("product", load: false, select: :name).first
96 99 assert_equal %w(id name), result.keys.reject { |k| k.start_with?("_") }.sort
... ... @@ -99,6 +102,7 @@ class SqlTest &lt; Minitest::Test
99 102 end
100 103  
101 104 def test_select_all
  105 + skip unless elasticsearch_below50?
102 106 store [{name: "Product A", user_ids: [1, 2]}]
103 107 hit = Product.search("product", select: true).hits.first
104 108 assert_equal hit["_source"]["name"], "Product A"
... ... @@ -106,6 +110,7 @@ class SqlTest &lt; Minitest::Test
106 110 end
107 111  
108 112 def test_select_none
  113 + skip unless elasticsearch_below50?
109 114 store [{name: "Product A", user_ids: [1, 2]}]
110 115 hit = Product.search("product", select: []).hits.first
111 116 assert_nil hit["_source"]
... ...
test/test_helper.rb
... ... @@ -21,12 +21,16 @@ I18n.config.enforce_available_locales = true
21 21 ActiveJob::Base.logger = nil if defined?(ActiveJob)
22 22 ActiveSupport::LogSubscriber.logger = Logger.new(STDOUT) if ENV["NOTIFICATIONS"]
23 23  
  24 +def elasticsearch_below50?
  25 + Searchkick.server_below?("5.0.0-alpha1")
  26 +end
  27 +
24 28 def elasticsearch_below20?
25   - Searchkick.below_version?("2.0.0")
  29 + Searchkick.server_below?("2.0.0")
26 30 end
27 31  
28 32 def elasticsearch_below14?
29   - Searchkick.server_version.starts_with?("1.4.0")
  33 + Searchkick.server_below?("1.4.0")
30 34 end
31 35  
32 36 def mongoid2?
... ... @@ -292,7 +296,7 @@ class Store
292 296 mappings: {
293 297 store: {
294 298 properties: {
295   - name: {type: "string", analyzer: "keyword"}
  299 + name: elasticsearch_below50? ? {type: "string", analyzer: "keyword"} : {type: "keyword"}
296 300 }
297 301 }
298 302 }
... ...