Commit 668b7948df1d84548690633968de0174c539fc9b
1 parent
278ea36f
Exists in
master
and in
21 other branches
Added support for Elasticsearch 5.0 alpha
Showing
8 changed files
with
156 additions
and
61 deletions
Show diff stats
CHANGELOG.md
lib/searchkick.rb
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 < 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 < 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 < 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 < 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 < 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 < 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 < 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 | } | ... | ... |