Commit 0d315179440a52af38b8936bb0628fddada09fac

Authored by Rajesh Kumar
1 parent bd12de70

Support conversion boosting on multiple fields

@@ -1199,6 +1199,34 @@ class Product < ActiveRecord::Base @@ -1199,6 +1199,34 @@ class Product < ActiveRecord::Base
1199 end 1199 end
1200 ``` 1200 ```
1201 1201
  1202 +Multiple conversion fields
  1203 +
  1204 +```ruby
  1205 +class Product < ActiveRecord::Base
  1206 + has_many :searches, class_name: "Searchjoy::Search"
  1207 +
  1208 + # searchkick also supports multiple "conversions" fields
  1209 + searchkick conversions: ["unique_user_conversions", "total_conversions"]
  1210 +
  1211 + def search_data
  1212 + {
  1213 + name: name,
  1214 + unique_user_conversions: searches.group(:query).uniq.count(:user_id)
  1215 + # {"ice cream" => 234, "chocolate" => 67, "cream" => 2}
  1216 + total_conversions: searches.group(:query).count
  1217 + # {"ice cream" => 412, "chocolate" => 117, "cream" => 6}
  1218 + }
  1219 + end
  1220 +end
  1221 +```
  1222 +and during query time:
  1223 +
  1224 +```ruby
  1225 +Product.search("banana") # boost by both fields (default)
  1226 +Product.search("banana", {conversions: "total_conversions"}) # only boost by total_conversions
  1227 +Product.search("banana", {conversions: false}) # no conversion boosting
  1228 +```
  1229 +
1202 Change timeout 1230 Change timeout
1203 1231
1204 ```ruby 1232 ```ruby
lib/searchkick/index.rb
@@ -430,7 +430,7 @@ module Searchkick @@ -430,7 +430,7 @@ module Searchkick
430 mapping = {} 430 mapping = {}
431 431
432 # conversions 432 # conversions
433 - if (conversions_field = options[:conversions]) 433 + Array(options[:conversions]).each do |conversions_field|
434 mapping[conversions_field] = { 434 mapping[conversions_field] = {
435 type: "nested", 435 type: "nested",
436 properties: { 436 properties: {
@@ -602,9 +602,10 @@ module Searchkick @@ -602,9 +602,10 @@ module Searchkick
602 source = source.inject({}) { |memo, (k, v)| memo[k.to_s] = v; memo }.except("_id") 602 source = source.inject({}) { |memo, (k, v)| memo[k.to_s] = v; memo }.except("_id")
603 603
604 # conversions 604 # conversions
605 - conversions_field = options[:conversions]  
606 - if conversions_field && source[conversions_field]  
607 - source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} } 605 + Array(options[:conversions]).each do |conversions_field|
  606 + if source[conversions_field]
  607 + source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
  608 + end
608 end 609 end
609 610
610 # hack to prevent generator field doesn't exist error 611 # hack to prevent generator field doesn't exist error
lib/searchkick/query.rb
@@ -162,8 +162,8 @@ module Searchkick @@ -162,8 +162,8 @@ module Searchkick
162 # model and eagar loading 162 # model and eagar loading
163 load = options[:load].nil? ? true : options[:load] 163 load = options[:load].nil? ? true : options[:load]
164 164
165 - conversions_field = searchkick_options[:conversions]  
166 - personalize_field = searchkick_options[:personalize] 165 + conversions_fields = Array(options[:conversions] || searchkick_options[:conversions])
  166 + personalize_field = searchkick_options[:personalize]
167 167
168 all = term == "*" 168 all = term == "*"
169 169
@@ -275,34 +275,38 @@ module Searchkick @@ -275,34 +275,38 @@ module Searchkick
275 } 275 }
276 end 276 end
277 277
278 - if conversions_field && options[:conversions] != false  
279 - # wrap payload in a bool query  
280 - script_score =  
281 - if below12?  
282 - {script_score: {script: "doc['count'].value"}}  
283 - else  
284 - {field_value_factor: {field: "#{conversions_field}.count"}}  
285 - end 278 + if conversions_fields.present? && options[:conversions] != false
  279 + shoulds = []
  280 + conversions_fields.each do |conversions_field|
  281 + # wrap payload in a bool query
  282 + script_score =
  283 + if below12?
  284 + {script_score: {script: "doc['count'].value"}}
  285 + else
  286 + {field_value_factor: {field: "#{conversions_field}.count"}}
  287 + end
286 288
  289 + shoulds << {
  290 + nested: {
  291 + path: conversions_field,
  292 + score_mode: "sum",
  293 + query: {
  294 + function_score: {
  295 + boost_mode: "replace",
  296 + query: {
  297 + match: {
  298 + "#{conversions_field}.query" => term
  299 + }
  300 + }
  301 + }.merge(script_score)
  302 + }
  303 + }
  304 + }
  305 + end
287 payload = { 306 payload = {
288 bool: { 307 bool: {
289 must: payload, 308 must: payload,
290 - should: {  
291 - nested: {  
292 - path: conversions_field,  
293 - score_mode: "sum",  
294 - query: {  
295 - function_score: {  
296 - boost_mode: "replace",  
297 - query: {  
298 - match: {  
299 - "#{conversions_field}.query" => term  
300 - }  
301 - }  
302 - }.merge(script_score)  
303 - }  
304 - }  
305 - } 309 + should: shoulds
306 } 310 }
307 } 311 }
308 end 312 end
test/boost_test.rb
@@ -10,6 +10,24 @@ class BoostTest &lt; Minitest::Test @@ -10,6 +10,24 @@ class BoostTest &lt; Minitest::Test
10 {name: "Tomato C", conversions: {"tomato" => 3}} 10 {name: "Tomato C", conversions: {"tomato" => 3}}
11 ] 11 ]
12 assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"] 12 assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"]
  13 + assert_equal_scores "tomato", {conversions: false}
  14 + end
  15 +
  16 + def test_multiple_conversions
  17 + skip if elasticsearch_below14?
  18 +
  19 + store [
  20 + {name: "Speaker A", conversions_a: {"speaker" => 1}, conversions_b: {"speaker" => 6}},
  21 + {name: "Speaker B", conversions_a: {"speaker" => 2}, conversions_b: {"speaker" => 5}},
  22 + {name: "Speaker C", conversions_a: {"speaker" => 3}, conversions_b: {"speaker" => 4}},
  23 + ], Speaker
  24 +
  25 + assert_equal_scores "speaker", {conversions: false}, Speaker
  26 + assert_equal_scores "speaker", {}, Speaker
  27 + assert_equal_scores "speaker", {conversions: ["conversions_a", "conversions_b"]}, Speaker
  28 + assert_equal_scores "speaker", {conversions: ["conversions_b", "conversions_a"]}, Speaker
  29 + assert_order "speaker", ["Speaker C", "Speaker B", "Speaker A"], {conversions: "conversions_a"}, Speaker
  30 + assert_order "speaker", ["Speaker A", "Speaker B", "Speaker C"], {conversions: "conversions_b"}, Speaker
13 end 31 end
14 32
15 def test_conversions_stemmed 33 def test_conversions_stemmed
test/test_helper.rb
@@ -93,6 +93,12 @@ if defined?(Mongoid) @@ -93,6 +93,12 @@ if defined?(Mongoid)
93 field :name 93 field :name
94 end 94 end
95 95
  96 + class Speaker
  97 + include Mongoid::Document
  98 +
  99 + field :name
  100 + end
  101 +
96 class Animal 102 class Animal
97 include Mongoid::Document 103 include Mongoid::Document
98 104
@@ -137,6 +143,13 @@ elsif defined?(NoBrainer) @@ -137,6 +143,13 @@ elsif defined?(NoBrainer)
137 field :name, type: String 143 field :name, type: String
138 end 144 end
139 145
  146 + class Speaker
  147 + include NoBrainer::Document
  148 +
  149 + field :id, type: Object
  150 + field :name, type: String
  151 + end
  152 +
140 class Animal 153 class Animal
141 include NoBrainer::Document 154 include NoBrainer::Document
142 155
@@ -221,6 +234,10 @@ else @@ -221,6 +234,10 @@ else
221 t.string :name 234 t.string :name
222 end 235 end
223 236
  237 + ActiveRecord::Migration.create_table :speakers do |t|
  238 + t.string :name
  239 + end
  240 +
224 ActiveRecord::Migration.create_table :animals do |t| 241 ActiveRecord::Migration.create_table :animals do |t|
225 t.string :name 242 t.string :name
226 t.string :type 243 t.string :type
@@ -233,6 +250,9 @@ else @@ -233,6 +250,9 @@ else
233 has_many :products 250 has_many :products
234 end 251 end
235 252
  253 + class Speaker < ActiveRecord::Base
  254 + end
  255 +
236 class Animal < ActiveRecord::Base 256 class Animal < ActiveRecord::Base
237 end 257 end
238 258
@@ -310,6 +330,20 @@ class Store @@ -310,6 +330,20 @@ class Store
310 end 330 end
311 end 331 end
312 332
  333 +class Speaker
  334 + searchkick \
  335 + conversions: ["conversions_a", "conversions_b"]
  336 +
  337 + attr_accessor :conversions_a, :conversions_b
  338 +
  339 + def search_data
  340 + serializable_hash.except("id").merge(
  341 + conversions_a: conversions_a,
  342 + conversions_b: conversions_b,
  343 + )
  344 + end
  345 +end
  346 +
313 class Animal 347 class Animal
314 searchkick \ 348 searchkick \
315 autocomplete: [:name], 349 autocomplete: [:name],
@@ -325,12 +359,14 @@ Product.create!(name: &quot;Set mapping&quot;) @@ -325,12 +359,14 @@ Product.create!(name: &quot;Set mapping&quot;)
325 359
326 Store.reindex 360 Store.reindex
327 Animal.reindex 361 Animal.reindex
  362 +Speaker.reindex
328 363
329 class Minitest::Test 364 class Minitest::Test
330 def setup 365 def setup
331 Product.destroy_all 366 Product.destroy_all
332 Store.destroy_all 367 Store.destroy_all
333 Animal.destroy_all 368 Animal.destroy_all
  369 + Speaker.destroy_all
334 end 370 end
335 371
336 protected 372 protected
@@ -355,6 +391,10 @@ class Minitest::Test @@ -355,6 +391,10 @@ class Minitest::Test
355 assert_equal expected, klass.search(term, options).map(&:name) 391 assert_equal expected, klass.search(term, options).map(&:name)
356 end 392 end
357 393
  394 + def assert_equal_scores(term, options = {}, klass = Product)
  395 + assert_equal 1, klass.search(term, options).hits.map { |a| a['_score'] }.uniq.size
  396 + end
  397 +
358 def assert_first(term, expected, options = {}, klass = Product) 398 def assert_first(term, expected, options = {}, klass = Product)
359 assert_equal expected, klass.search(term, options).map(&:name).first 399 assert_equal expected, klass.search(term, options).map(&:name).first
360 end 400 end