Commit 01de136583eda91799d59da59ea14bdcdea14f33

Authored by Andrew Kane
1 parent d5312b89
Exists in conversions_v2

Added conversions_v2 option

README.md
... ... @@ -669,7 +669,7 @@ Add conversion data with:
669 669 class Product < ApplicationRecord
670 670 has_many :searches, class_name: "Searchjoy::Search", as: :convertable
671 671  
672   - searchkick conversions: [:conversions] # name of field
  672 + searchkick conversions_v2: [:conversions] # name of field
673 673  
674 674 def search_data
675 675 {
... ... @@ -2101,6 +2101,38 @@ And there’s a [new option](#default-scopes) for models with default scopes.
2101 2101  
2102 2102 Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md#500-2022-02-21) for the full list of changes.
2103 2103  
  2104 +## Upgrading Conversions
  2105 +
  2106 +Upgrade conversions without downtime. Add `conversions_v2` to your model and an additional field to `search_data`:
  2107 +
  2108 +```ruby
  2109 +class Product < ApplicationRecord
  2110 + searchkick conversions: [:conversions], conversions_v2: [:conversions_v2]
  2111 +
  2112 + def search_data
  2113 + conversions = searches.group(:query).distinct.count(:user_id)
  2114 + {
  2115 + conversions: conversions,
  2116 + conversions_v2: conversions
  2117 + }
  2118 + end
  2119 +end
  2120 +```
  2121 +
  2122 +Reindex, then remove `conversions`:
  2123 +
  2124 +```ruby
  2125 +class Product < ApplicationRecord
  2126 + searchkick conversions_v2: [:conversions_v2]
  2127 +
  2128 + def search_data
  2129 + {
  2130 + conversions_v2: searches.group(:query).distinct.count(:user_id)
  2131 + }
  2132 + end
  2133 +end
  2134 +```
  2135 +
2104 2136 ## History
2105 2137  
2106 2138 View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md).
... ...
lib/searchkick/index.rb
... ... @@ -291,6 +291,11 @@ module Searchkick
291 291 end
292 292  
293 293 # private
  294 + def conversions_v2_fields
  295 + @conversions_v2_fields ||= Array(options[:conversions_v2]).map(&:to_s)
  296 + end
  297 +
  298 + # private
294 299 def suggest_fields
295 300 @suggest_fields ||= Array(options[:suggest]).map(&:to_s)
296 301 end
... ...
lib/searchkick/index_options.rb
... ... @@ -357,6 +357,16 @@ module Searchkick
357 357 }
358 358 end
359 359  
  360 + Array(options[:conversions_v2]).each do |conversions_field|
  361 + mapping[conversions_field] = {
  362 + type: "rank_features"
  363 + }
  364 + end
  365 +
  366 + if (Array(options[:conversions_v2]).map(&:to_s) & Array(options[:conversions]).map(&:to_s)).any?
  367 + raise ArgumentError, "Must have separate conversions fields"
  368 + end
  369 +
360 370 mapping_options =
361 371 [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable]
362 372 .to_h { |type| [type, (options[type] || []).map(&:to_s)] }
... ...
lib/searchkick/model.rb
... ... @@ -3,7 +3,7 @@ module Searchkick
3 3 def searchkick(**options)
4 4 options = Searchkick.model_options.merge(options)
5 5  
6   - unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields,
  6 + unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :conversions_v2, :deep_paging, :default_fields,
7 7 :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language,
8 8 :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity,
9 9 :special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end,
... ...
lib/searchkick/query.rb
... ... @@ -18,7 +18,7 @@ module Searchkick
18 18  
19 19 def initialize(klass, term = "*", **options)
20 20 unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost,
21   - :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :explain,
  21 + :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_v2, :conversions_term, :debug, :emoji, :exclude, :explain,
22 22 :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load,
23 23 :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile,
24 24 :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where]
... ... @@ -431,6 +431,7 @@ module Searchkick
431 431 }
432 432  
433 433 should.concat(set_conversions)
  434 + should.concat(set_conversions_v2)
434 435 end
435 436  
436 437 query = payload
... ... @@ -642,6 +643,25 @@ module Searchkick
642 643 end
643 644 end
644 645  
  646 + def set_conversions_v2
  647 + conversions_fields = Array(options[:conversions_v2] || searchkick_options[:conversions_v2]).map(&:to_s)
  648 + # disable if searchkick_options[:conversions] to make it easy to upgrade without downtime
  649 + if conversions_fields.present? && options[:conversions_v2] != false && !(options[:conversions_v2].nil? && searchkick_options[:conversions])
  650 + conversions_fields.map do |conversions_field|
  651 + {
  652 + rank_feature: {
  653 + field: "#{conversions_field}.#{options[:conversions_term] || term}",
  654 + log: {
  655 + scaling_factor: 1
  656 + }
  657 + }
  658 + }
  659 + end
  660 + else
  661 + []
  662 + end
  663 + end
  664 +
645 665 def set_exclude(field, analyzer)
646 666 Array(options[:exclude]).map do |phrase|
647 667 {
... ...
lib/searchkick/record_data.rb
... ... @@ -58,6 +58,12 @@ module Searchkick
58 58 end
59 59 end
60 60  
  61 + index.conversions_v2_fields.each do |conversions_field|
  62 + unless source[conversions_field] || source[conversions_field.to_sym]
  63 + source[conversions_field] ||= {}
  64 + end
  65 + end
  66 +
61 67 # hack to prevent generator field doesn't exist error
62 68 if !partial_reindex
63 69 index.suggest_fields.each do |field|
... ...
test/conversions_test.rb
... ... @@ -16,6 +16,15 @@ class ConversionsTest &lt; Minitest::Test
16 16 assert_equal_scores "tomato", conversions: false
17 17 end
18 18  
  19 + def test_conversions_v2
  20 + store [
  21 + {name: "Tomato A", conversions_v2: {"tomato" => 1}},
  22 + {name: "Tomato B", conversions_v2: {"tomato" => 2}},
  23 + {name: "Tomato C", conversions_v2: {"tomato" => 3}}
  24 + ]
  25 + assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"], conversions_v2: [:conversions_v2]
  26 + end
  27 +
19 28 def test_multiple_conversions
20 29 store [
21 30 {name: "Speaker A", conversions_a: {"speaker" => 1}, conversions_b: {"speaker" => 6}},
... ...
test/models/product.rb
... ... @@ -10,6 +10,7 @@ class Product
10 10 ],
11 11 suggest: [:name, :color],
12 12 conversions: [:conversions],
  13 + conversions_v2: [:conversions_v2],
13 14 locations: [:location, :multiple_locations],
14 15 text_start: [:name],
15 16 text_middle: [:name],
... ... @@ -22,7 +23,7 @@ class Product
22 23 similarity: "BM25",
23 24 match: ENV["MATCH"] ? ENV["MATCH"].to_sym : nil
24 25  
25   - attr_accessor :conversions, :user_ids, :aisle, :details
  26 + attr_accessor :conversions, :conversions_v2, :user_ids, :aisle, :details
26 27  
27 28 class << self
28 29 attr_accessor :dynamic_data
... ... @@ -33,6 +34,7 @@ class Product
33 34  
34 35 serializable_hash.except("id", "_id").merge(
35 36 conversions: conversions,
  37 + conversions_v2: conversions_v2,
36 38 user_ids: user_ids,
37 39 location: {lat: latitude, lon: longitude},
38 40 multiple_locations: [{lat: latitude, lon: longitude}, {lat: 0, lon: 0}],
... ...