Commit 0d315179440a52af38b8936bb0628fddada09fac

Authored by Rajesh Kumar
1 parent bd12de70

Support conversion boosting on multiple fields

README.md
... ... @@ -1199,6 +1199,34 @@ class Product < ActiveRecord::Base
1199 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 1230 Change timeout
1203 1231  
1204 1232 ```ruby
... ...
lib/searchkick/index.rb
... ... @@ -430,7 +430,7 @@ module Searchkick
430 430 mapping = {}
431 431  
432 432 # conversions
433   - if (conversions_field = options[:conversions])
  433 + Array(options[:conversions]).each do |conversions_field|
434 434 mapping[conversions_field] = {
435 435 type: "nested",
436 436 properties: {
... ... @@ -602,9 +602,10 @@ module Searchkick
602 602 source = source.inject({}) { |memo, (k, v)| memo[k.to_s] = v; memo }.except("_id")
603 603  
604 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 609 end
609 610  
610 611 # hack to prevent generator field doesn't exist error
... ...
lib/searchkick/query.rb
... ... @@ -162,8 +162,8 @@ module Searchkick
162 162 # model and eagar loading
163 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 168 all = term == "*"
169 169  
... ... @@ -275,34 +275,38 @@ module Searchkick
275 275 }
276 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 306 payload = {
288 307 bool: {
289 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 312 end
... ...
test/boost_test.rb
... ... @@ -10,6 +10,24 @@ class BoostTest &lt; Minitest::Test
10 10 {name: "Tomato C", conversions: {"tomato" => 3}}
11 11 ]
12 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 31 end
14 32  
15 33 def test_conversions_stemmed
... ...
test/test_helper.rb
... ... @@ -93,6 +93,12 @@ if defined?(Mongoid)
93 93 field :name
94 94 end
95 95  
  96 + class Speaker
  97 + include Mongoid::Document
  98 +
  99 + field :name
  100 + end
  101 +
96 102 class Animal
97 103 include Mongoid::Document
98 104  
... ... @@ -137,6 +143,13 @@ elsif defined?(NoBrainer)
137 143 field :name, type: String
138 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 153 class Animal
141 154 include NoBrainer::Document
142 155  
... ... @@ -221,6 +234,10 @@ else
221 234 t.string :name
222 235 end
223 236  
  237 + ActiveRecord::Migration.create_table :speakers do |t|
  238 + t.string :name
  239 + end
  240 +
224 241 ActiveRecord::Migration.create_table :animals do |t|
225 242 t.string :name
226 243 t.string :type
... ... @@ -233,6 +250,9 @@ else
233 250 has_many :products
234 251 end
235 252  
  253 + class Speaker < ActiveRecord::Base
  254 + end
  255 +
236 256 class Animal < ActiveRecord::Base
237 257 end
238 258  
... ... @@ -310,6 +330,20 @@ class Store
310 330 end
311 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 347 class Animal
314 348 searchkick \
315 349 autocomplete: [:name],
... ... @@ -325,12 +359,14 @@ Product.create!(name: &quot;Set mapping&quot;)
325 359  
326 360 Store.reindex
327 361 Animal.reindex
  362 +Speaker.reindex
328 363  
329 364 class Minitest::Test
330 365 def setup
331 366 Product.destroy_all
332 367 Store.destroy_all
333 368 Animal.destroy_all
  369 + Speaker.destroy_all
334 370 end
335 371  
336 372 protected
... ... @@ -355,6 +391,10 @@ class Minitest::Test
355 391 assert_equal expected, klass.search(term, options).map(&:name)
356 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 398 def assert_first(term, expected, options = {}, klass = Product)
359 399 assert_equal expected, klass.search(term, options).map(&:name).first
360 400 end
... ...