Commit 0d315179440a52af38b8936bb0628fddada09fac
1 parent
bd12de70
Exists in
master
and in
21 other branches
Support conversion boosting on multiple fields
Showing
5 changed files
with
121 additions
and
30 deletions
Show diff stats
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 < 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: "Set mapping") |
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 | ... | ... |