From 01de136583eda91799d59da59ea14bdcdea14f33 Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Tue, 8 Mar 2022 10:50:21 -0500 Subject: [PATCH] Added conversions_v2 option --- README.md | 34 +++++++++++++++++++++++++++++++++- lib/searchkick/index.rb | 5 +++++ lib/searchkick/index_options.rb | 10 ++++++++++ lib/searchkick/model.rb | 2 +- lib/searchkick/query.rb | 22 +++++++++++++++++++++- lib/searchkick/record_data.rb | 6 ++++++ test/conversions_test.rb | 9 +++++++++ test/models/product.rb | 4 +++- 8 files changed, 88 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aaa0330..5b79230 100644 --- a/README.md +++ b/README.md @@ -669,7 +669,7 @@ Add conversion data with: class Product < ApplicationRecord has_many :searches, class_name: "Searchjoy::Search", as: :convertable - searchkick conversions: [:conversions] # name of field + searchkick conversions_v2: [:conversions] # name of field def search_data { @@ -2101,6 +2101,38 @@ And there’s a [new option](#default-scopes) for models with default scopes. Check out the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md#500-2022-02-21) for the full list of changes. +## Upgrading Conversions + +Upgrade conversions without downtime. Add `conversions_v2` to your model and an additional field to `search_data`: + +```ruby +class Product < ApplicationRecord + searchkick conversions: [:conversions], conversions_v2: [:conversions_v2] + + def search_data + conversions = searches.group(:query).distinct.count(:user_id) + { + conversions: conversions, + conversions_v2: conversions + } + end +end +``` + +Reindex, then remove `conversions`: + +```ruby +class Product < ApplicationRecord + searchkick conversions_v2: [:conversions_v2] + + def search_data + { + conversions_v2: searches.group(:query).distinct.count(:user_id) + } + end +end +``` + ## History View the [changelog](https://github.com/ankane/searchkick/blob/master/CHANGELOG.md). diff --git a/lib/searchkick/index.rb b/lib/searchkick/index.rb index 1bc3744..c90fad2 100644 --- a/lib/searchkick/index.rb +++ b/lib/searchkick/index.rb @@ -291,6 +291,11 @@ module Searchkick end # private + def conversions_v2_fields + @conversions_v2_fields ||= Array(options[:conversions_v2]).map(&:to_s) + end + + # private def suggest_fields @suggest_fields ||= Array(options[:suggest]).map(&:to_s) end diff --git a/lib/searchkick/index_options.rb b/lib/searchkick/index_options.rb index f4194cf..f533a1d 100644 --- a/lib/searchkick/index_options.rb +++ b/lib/searchkick/index_options.rb @@ -357,6 +357,16 @@ module Searchkick } end + Array(options[:conversions_v2]).each do |conversions_field| + mapping[conversions_field] = { + type: "rank_features" + } + end + + if (Array(options[:conversions_v2]).map(&:to_s) & Array(options[:conversions]).map(&:to_s)).any? + raise ArgumentError, "Must have separate conversions fields" + end + mapping_options = [:suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :filterable] .to_h { |type| [type, (options[type] || []).map(&:to_s)] } diff --git a/lib/searchkick/model.rb b/lib/searchkick/model.rb index c7daef1..210b8f3 100644 --- a/lib/searchkick/model.rb +++ b/lib/searchkick/model.rb @@ -3,7 +3,7 @@ module Searchkick def searchkick(**options) options = Searchkick.model_options.merge(options) - unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :deep_paging, :default_fields, + unknown_keywords = options.keys - [:_all, :_type, :batch_size, :callbacks, :case_sensitive, :conversions, :conversions_v2, :deep_paging, :default_fields, :filterable, :geo_shape, :highlight, :ignore_above, :index_name, :index_prefix, :inheritance, :language, :locations, :mappings, :match, :merge_mappings, :routing, :searchable, :search_synonyms, :settings, :similarity, :special_characters, :stem, :stemmer, :stem_conversions, :stem_exclusion, :stemmer_override, :suggest, :synonyms, :text_end, diff --git a/lib/searchkick/query.rb b/lib/searchkick/query.rb index dee2ca9..58ae60c 100644 --- a/lib/searchkick/query.rb +++ b/lib/searchkick/query.rb @@ -18,7 +18,7 @@ module Searchkick def initialize(klass, term = "*", **options) unknown_keywords = options.keys - [:aggs, :block, :body, :body_options, :boost, - :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_term, :debug, :emoji, :exclude, :explain, + :boost_by, :boost_by_distance, :boost_by_recency, :boost_where, :conversions, :conversions_v2, :conversions_term, :debug, :emoji, :exclude, :explain, :fields, :highlight, :includes, :index_name, :indices_boost, :limit, :load, :match, :misspellings, :models, :model_includes, :offset, :operator, :order, :padding, :page, :per_page, :profile, :request_params, :routing, :scope_results, :scroll, :select, :similar, :smart_aggs, :suggest, :total_entries, :track, :type, :where] @@ -431,6 +431,7 @@ module Searchkick } should.concat(set_conversions) + should.concat(set_conversions_v2) end query = payload @@ -642,6 +643,25 @@ module Searchkick end end + def set_conversions_v2 + conversions_fields = Array(options[:conversions_v2] || searchkick_options[:conversions_v2]).map(&:to_s) + # disable if searchkick_options[:conversions] to make it easy to upgrade without downtime + if conversions_fields.present? && options[:conversions_v2] != false && !(options[:conversions_v2].nil? && searchkick_options[:conversions]) + conversions_fields.map do |conversions_field| + { + rank_feature: { + field: "#{conversions_field}.#{options[:conversions_term] || term}", + log: { + scaling_factor: 1 + } + } + } + end + else + [] + end + end + def set_exclude(field, analyzer) Array(options[:exclude]).map do |phrase| { diff --git a/lib/searchkick/record_data.rb b/lib/searchkick/record_data.rb index b70a663..19ffe79 100644 --- a/lib/searchkick/record_data.rb +++ b/lib/searchkick/record_data.rb @@ -58,6 +58,12 @@ module Searchkick end end + index.conversions_v2_fields.each do |conversions_field| + unless source[conversions_field] || source[conversions_field.to_sym] + source[conversions_field] ||= {} + end + end + # hack to prevent generator field doesn't exist error if !partial_reindex index.suggest_fields.each do |field| diff --git a/test/conversions_test.rb b/test/conversions_test.rb index e96c7ef..1864e35 100644 --- a/test/conversions_test.rb +++ b/test/conversions_test.rb @@ -16,6 +16,15 @@ class ConversionsTest < Minitest::Test assert_equal_scores "tomato", conversions: false end + def test_conversions_v2 + store [ + {name: "Tomato A", conversions_v2: {"tomato" => 1}}, + {name: "Tomato B", conversions_v2: {"tomato" => 2}}, + {name: "Tomato C", conversions_v2: {"tomato" => 3}} + ] + assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"], conversions_v2: [:conversions_v2] + end + def test_multiple_conversions store [ {name: "Speaker A", conversions_a: {"speaker" => 1}, conversions_b: {"speaker" => 6}}, diff --git a/test/models/product.rb b/test/models/product.rb index b6179df..2470a52 100644 --- a/test/models/product.rb +++ b/test/models/product.rb @@ -10,6 +10,7 @@ class Product ], suggest: [:name, :color], conversions: [:conversions], + conversions_v2: [:conversions_v2], locations: [:location, :multiple_locations], text_start: [:name], text_middle: [:name], @@ -22,7 +23,7 @@ class Product similarity: "BM25", match: ENV["MATCH"] ? ENV["MATCH"].to_sym : nil - attr_accessor :conversions, :user_ids, :aisle, :details + attr_accessor :conversions, :conversions_v2, :user_ids, :aisle, :details class << self attr_accessor :dynamic_data @@ -33,6 +34,7 @@ class Product serializable_hash.except("id", "_id").merge( conversions: conversions, + conversions_v2: conversions_v2, user_ids: user_ids, location: {lat: latitude, lon: longitude}, multiple_locations: [{lat: latitude, lon: longitude}, {lat: 0, lon: 0}], -- libgit2 0.21.0