Commit 7999b2b3341f71b602bc8a044a601c28c9f3cfb4

Authored by Andrew Kane
2 parents ba2766c5 cdf7be65

Merge branch 'code_refactor' of https://github.com/doubiz/searchkick

Showing 1 changed file with 266 additions and 238 deletions   Show diff stats
lib/searchkick/query.rb
@@ -149,28 +149,7 @@ module Searchkick @@ -149,28 +149,7 @@ module Searchkick
149 end 149 end
150 150
151 def prepare 151 def prepare
152 - boost_fields = {}  
153 - fields = options[:fields] || searchkick_options[:searchable]  
154 - fields =  
155 - if fields  
156 - if options[:autocomplete]  
157 - fields.map { |f| "#{f}.autocomplete" }  
158 - else  
159 - fields.map do |value|  
160 - k, v = value.is_a?(Hash) ? value.to_a.first : [value, options[:match] || searchkick_options[:match] || :word]  
161 - k2, boost = k.to_s.split("^", 2)  
162 - field = "#{k2}.#{v == :word ? 'analyzed' : v}"  
163 - boost_fields[field] = boost.to_f if boost  
164 - field  
165 - end  
166 - end  
167 - else  
168 - if options[:autocomplete]  
169 - (searchkick_options[:autocomplete] || []).map { |f| "#{f}.autocomplete" }  
170 - else  
171 - ["_all"]  
172 - end  
173 - end 152 + boost_fields, fields = set_fields
174 153
175 operator = options[:operator] || (options[:partial] ? "or" : "and") 154 operator = options[:operator] || (options[:partial] ? "or" : "and")
176 155
@@ -187,7 +166,7 @@ module Searchkick @@ -187,7 +166,7 @@ module Searchkick
187 personalize_field = searchkick_options[:personalize] 166 personalize_field = searchkick_options[:personalize]
188 167
189 all = term == "*" 168 all = term == "*"
190 - facet_limits = {} 169 +
191 170
192 options[:json] ||= options[:body] 171 options[:json] ||= options[:body]
193 if options[:json] 172 if options[:json]
@@ -324,52 +303,11 @@ module Searchkick @@ -324,52 +303,11 @@ module Searchkick
324 custom_filters = [] 303 custom_filters = []
325 multiply_filters = [] 304 multiply_filters = []
326 305
327 - boost_by = options[:boost_by] || {} 306 + set_boost_by(multiply_filters, custom_filters)
328 307
329 - if boost_by.is_a?(Array)  
330 - boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]  
331 - elsif boost_by.is_a?(Hash)  
332 - multiply_by, boost_by = boost_by.partition { |_, v| v[:boost_mode] == "multiply" }.map { |i| Hash[i] }  
333 - end  
334 - boost_by[options[:boost]] = {factor: 1} if options[:boost] 308 + set_boost_where(custom_filters, personalize_field)
335 309
336 - custom_filters.concat boost_filters(boost_by, log: true)  
337 - multiply_filters.concat boost_filters(multiply_by || {})  
338 -  
339 - boost_where = options[:boost_where] || {}  
340 - if options[:user_id] && personalize_field  
341 - boost_where[personalize_field] = options[:user_id]  
342 - end  
343 - if options[:personalize]  
344 - boost_where = boost_where.merge(options[:personalize])  
345 - end  
346 - boost_where.each do |field, value|  
347 - if value.is_a?(Array) && value.first.is_a?(Hash)  
348 - value.each do |value_factor|  
349 - custom_filters << custom_filter(field, value_factor[:value], value_factor[:factor])  
350 - end  
351 - elsif value.is_a?(Hash)  
352 - custom_filters << custom_filter(field, value[:value], value[:factor])  
353 - else  
354 - factor = 1000  
355 - custom_filters << custom_filter(field, value, factor)  
356 - end  
357 - end  
358 -  
359 - boost_by_distance = options[:boost_by_distance]  
360 - if boost_by_distance  
361 - boost_by_distance = {function: :gauss, scale: "5mi"}.merge(boost_by_distance)  
362 - if !boost_by_distance[:field] || !boost_by_distance[:origin]  
363 - raise ArgumentError, "boost_by_distance requires :field and :origin"  
364 - end  
365 - function_params = boost_by_distance.select { |k, _| [:origin, :scale, :offset, :decay].include?(k) }  
366 - function_params[:origin] = location_value(function_params[:origin])  
367 - custom_filters << {  
368 - boost_by_distance[:function] => {  
369 - boost_by_distance[:field] => function_params  
370 - }  
371 - }  
372 - end 310 + set_boost_by_distance(custom_filters) if options[:boost_by_distance]
373 311
374 if custom_filters.any? 312 if custom_filters.any?
375 payload = { 313 payload = {
@@ -399,187 +337,24 @@ module Searchkick @@ -399,187 +337,24 @@ module Searchkick
399 payload[:explain] = options[:explain] if options[:explain] 337 payload[:explain] = options[:explain] if options[:explain]
400 338
401 # order 339 # order
402 - if options[:order]  
403 - order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}  
404 - # TODO id transformation for arrays  
405 - payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? :_id : k, v] }]  
406 - end 340 + set_order(payload) if options[:order]
407 341
408 # filters 342 # filters
409 filters = where_filters(options[:where]) 343 filters = where_filters(options[:where])
410 - if filters.any?  
411 - if options[:facets] || options[:aggs]  
412 - payload[:filter] = {  
413 - and: filters  
414 - }  
415 - else  
416 - # more efficient query if no facets  
417 - payload[:query] = {  
418 - filtered: {  
419 - query: payload[:query],  
420 - filter: {  
421 - and: filters  
422 - }  
423 - }  
424 - }  
425 - end  
426 - end 344 + set_filters(payload, filters) if filters.any?
427 345
428 # facets 346 # facets
429 - if options[:facets]  
430 - facets = options[:facets] || {}  
431 - facets = Hash[facets.map { |f| [f, {}] }] if facets.is_a?(Array) # convert to more advanced syntax  
432 -  
433 - payload[:facets] = {}  
434 - facets.each do |field, facet_options|  
435 - # ask for extra facets due to  
436 - # https://github.com/elasticsearch/elasticsearch/issues/1305  
437 - size = facet_options[:limit] ? facet_options[:limit] + 150 : 1_000  
438 -  
439 - if facet_options[:ranges]  
440 - payload[:facets][field] = {  
441 - range: {  
442 - field.to_sym => facet_options[:ranges]  
443 - }  
444 - }  
445 - elsif facet_options[:stats]  
446 - payload[:facets][field] = {  
447 - terms_stats: {  
448 - key_field: field,  
449 - value_script: below14? ? "doc.score" : "_score",  
450 - size: size  
451 - }  
452 - }  
453 - else  
454 - payload[:facets][field] = {  
455 - terms: {  
456 - field: facet_options[:field] || field,  
457 - size: size  
458 - }  
459 - }  
460 - end  
461 -  
462 - facet_limits[field] = facet_options[:limit] if facet_options[:limit]  
463 -  
464 - # offset is not possible  
465 - # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html  
466 -  
467 - facet_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_facets] == true  
468 - facet_filters = where_filters(facet_options[:where])  
469 - if facet_filters.any?  
470 - payload[:facets][field][:facet_filter] = {  
471 - and: {  
472 - filters: facet_filters  
473 - }  
474 - }  
475 - end  
476 - end  
477 - end 347 + set_facets(payload) if options[:facets]
478 348
479 # aggregations 349 # aggregations
480 - if options[:aggs]  
481 - aggs = options[:aggs]  
482 - payload[:aggs] = {}  
483 -  
484 - aggs = Hash[aggs.map { |f| [f, {}] }] if aggs.is_a?(Array) # convert to more advanced syntax  
485 -  
486 - aggs.each do |field, agg_options|  
487 - size = agg_options[:limit] ? agg_options[:limit] : 1_000  
488 - shared_agg_options = agg_options.slice(:order)  
489 -  
490 - if agg_options[:ranges]  
491 - payload[:aggs][field] = {  
492 - range: {  
493 - field: agg_options[:field] || field,  
494 - ranges: agg_options[:ranges]  
495 - }.merge(shared_agg_options)  
496 - }  
497 - elsif agg_options[:date_ranges]  
498 - payload[:aggs][field] = {  
499 - date_range: {  
500 - field: agg_options[:field] || field,  
501 - ranges: agg_options[:date_ranges]  
502 - }.merge(shared_agg_options)  
503 - }  
504 - else  
505 - payload[:aggs][field] = {  
506 - terms: {  
507 - field: agg_options[:field] || field,  
508 - size: size  
509 - }.merge(shared_agg_options)  
510 - }  
511 - end  
512 -  
513 - where = {}  
514 - where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false  
515 - agg_filters = where_filters(where.merge(agg_options[:where] || {}))  
516 - if agg_filters.any?  
517 - payload[:aggs][field] = {  
518 - filter: {  
519 - bool: {  
520 - must: agg_filters  
521 - }  
522 - },  
523 - aggs: {  
524 - field => payload[:aggs][field]  
525 - }  
526 - }  
527 - end  
528 - end  
529 - end  
530 - 350 + set_aggregations(payload) if options[:aggs]
  351 +
531 # suggestions 352 # suggestions
532 - if options[:suggest]  
533 - suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)  
534 -  
535 - # intersection  
536 - if options[:fields]  
537 - suggest_fields &= options[:fields].map { |v| (v.is_a?(Hash) ? v.keys.first : v).to_s.split("^", 2).first }  
538 - end  
539 -  
540 - if suggest_fields.any?  
541 - payload[:suggest] = {text: term}  
542 - suggest_fields.each do |field|  
543 - payload[:suggest][field] = {  
544 - phrase: {  
545 - field: "#{field}.suggest"  
546 - }  
547 - }  
548 - end  
549 - end  
550 - end 353 + set_suggestions(payload) if options[:suggest]
551 354
552 # highlight 355 # highlight
553 - if options[:highlight]  
554 - payload[:highlight] = {  
555 - fields: Hash[fields.map { |f| [f, {}] }]  
556 - } 356 + set_highlights(payload, fields) if options[:highlight]
557 357
558 - if options[:highlight].is_a?(Hash)  
559 - if (tag = options[:highlight][:tag])  
560 - payload[:highlight][:pre_tags] = [tag]  
561 - payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]  
562 - end  
563 -  
564 - if (fragment_size = options[:highlight][:fragment_size])  
565 - payload[:highlight][:fragment_size] = fragment_size  
566 - end  
567 - if (encoder = options[:highlight][:encoder])  
568 - payload[:highlight][:encoder] = encoder  
569 - end  
570 -  
571 - highlight_fields = options[:highlight][:fields]  
572 - if highlight_fields  
573 - payload[:highlight][:fields] = {}  
574 -  
575 - highlight_fields.each do |name, opts|  
576 - payload[:highlight][:fields]["#{name}.#{@match_suffix}"] = opts || {}  
577 - end  
578 - end  
579 - end  
580 -  
581 - @highlighted_fields = payload[:highlight][:fields].keys  
582 - end  
583 358
584 # An empty array will cause only the _id and _type for each hit to be returned 359 # An empty array will cause only the _id and _type for each hit to be returned
585 # doc for :select - http://www.elasticsearch.org/guide/reference/api/search/fields/ 360 # doc for :select - http://www.elasticsearch.org/guide/reference/api/search/fields/
@@ -606,13 +381,266 @@ module Searchkick @@ -606,13 +381,266 @@ module Searchkick
606 end 381 end
607 382
608 @body = payload 383 @body = payload
609 - @facet_limits = facet_limits 384 + @facet_limits = @facet_limits || {}
610 @page = page 385 @page = page
611 @per_page = per_page 386 @per_page = per_page
612 @padding = padding 387 @padding = padding
613 @load = load 388 @load = load
614 end 389 end
615 390
  391 + def set_fields
  392 + boost_fields = {}
  393 + fields = options[:fields] || searchkick_options[:searchable]
  394 + fields =
  395 + if fields
  396 + if options[:autocomplete]
  397 + fields.map { |f| "#{f}.autocomplete" }
  398 + else
  399 + fields.map do |value|
  400 + k, v = value.is_a?(Hash) ? value.to_a.first : [value, options[:match] || searchkick_options[:match] || :word]
  401 + k2, boost = k.to_s.split("^", 2)
  402 + field = "#{k2}.#{v == :word ? 'analyzed' : v}"
  403 + boost_fields[field] = boost.to_f if boost
  404 + field
  405 + end
  406 + end
  407 + else
  408 + if options[:autocomplete]
  409 + (searchkick_options[:autocomplete] || []).map { |f| "#{f}.autocomplete" }
  410 + else
  411 + ["_all"]
  412 + end
  413 + end
  414 + [boost_fields, fields]
  415 + end
  416 +
  417 + def set_boost_by_distance(custom_filters)
  418 + boost_by_distance = options[:boost_by_distance] || {}
  419 + boost_by_distance = {function: :gauss, scale: "5mi"}.merge(boost_by_distance)
  420 + if !boost_by_distance[:field] || !boost_by_distance[:origin]
  421 + raise ArgumentError, "boost_by_distance requires :field and :origin"
  422 + end
  423 + function_params = boost_by_distance.select { |k, _| [:origin, :scale, :offset, :decay].include?(k) }
  424 + function_params[:origin] = location_value(function_params[:origin])
  425 + custom_filters << {
  426 + boost_by_distance[:function] => {
  427 + boost_by_distance[:field] => function_params
  428 + }
  429 + }
  430 + end
  431 +
  432 + def set_boost_by(multiply_filters, custom_filters)
  433 + boost_by = options[:boost_by] || {}
  434 + if boost_by.is_a?(Array)
  435 + boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]
  436 + elsif boost_by.is_a?(Hash)
  437 + multiply_by, boost_by = boost_by.partition { |_, v| v[:boost_mode] == "multiply" }.map { |i| Hash[i] }
  438 + end
  439 + boost_by[options[:boost]] = {factor: 1} if options[:boost]
  440 +
  441 + custom_filters.concat boost_filters(boost_by, log: true)
  442 + multiply_filters.concat boost_filters(multiply_by || {})
  443 + end
  444 +
  445 + def set_boost_where(custom_filters, personalize_field)
  446 + boost_where = options[:boost_where] || {}
  447 + if options[:user_id] && personalize_field
  448 + boost_where[personalize_field] = options[:user_id]
  449 + end
  450 + if options[:personalize]
  451 + boost_where = boost_where.merge(options[:personalize])
  452 + end
  453 + boost_where.each do |field, value|
  454 + if value.is_a?(Array) && value.first.is_a?(Hash)
  455 + value.each do |value_factor|
  456 + custom_filters << custom_filter(field, value_factor[:value], value_factor[:factor])
  457 + end
  458 + elsif value.is_a?(Hash)
  459 + custom_filters << custom_filter(field, value[:value], value[:factor])
  460 + else
  461 + factor = 1000
  462 + custom_filters << custom_filter(field, value, factor)
  463 + end
  464 + end
  465 + end
  466 +
  467 + def set_suggestions(payload)
  468 + suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)
  469 +
  470 + # intersection
  471 + if options[:fields]
  472 + suggest_fields &= options[:fields].map { |v| (v.is_a?(Hash) ? v.keys.first : v).to_s.split("^", 2).first }
  473 + end
  474 +
  475 + if suggest_fields.any?
  476 + payload[:suggest] = {text: term}
  477 + suggest_fields.each do |field|
  478 + payload[:suggest][field] = {
  479 + phrase: {
  480 + field: "#{field}.suggest"
  481 + }
  482 + }
  483 + end
  484 + end
  485 + end
  486 +
  487 + def set_highlights(payload, fields)
  488 + payload[:highlight] = {
  489 + fields: Hash[fields.map { |f| [f, {}] }]
  490 + }
  491 +
  492 + if options[:highlight].is_a?(Hash)
  493 + if (tag = options[:highlight][:tag])
  494 + payload[:highlight][:pre_tags] = [tag]
  495 + payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
  496 + end
  497 +
  498 + if (fragment_size = options[:highlight][:fragment_size])
  499 + payload[:highlight][:fragment_size] = fragment_size
  500 + end
  501 + if (encoder = options[:highlight][:encoder])
  502 + payload[:highlight][:encoder] = encoder
  503 + end
  504 +
  505 + highlight_fields = options[:highlight][:fields]
  506 + if highlight_fields
  507 + payload[:highlight][:fields] = {}
  508 +
  509 + highlight_fields.each do |name, opts|
  510 + payload[:highlight][:fields]["#{name}.#{@match_suffix}"] = opts || {}
  511 + end
  512 + end
  513 + end
  514 +
  515 + @highlighted_fields = payload[:highlight][:fields].keys
  516 + end
  517 +
  518 + def set_aggregations(payload)
  519 + aggs = options[:aggs]
  520 + payload[:aggs] = {}
  521 +
  522 + aggs = Hash[aggs.map { |f| [f, {}] }] if aggs.is_a?(Array) # convert to more advanced syntax
  523 +
  524 + aggs.each do |field, agg_options|
  525 + size = agg_options[:limit] ? agg_options[:limit] : 1_000
  526 + shared_agg_options = agg_options.slice(:order)
  527 +
  528 + if agg_options[:ranges]
  529 + payload[:aggs][field] = {
  530 + range: {
  531 + field: agg_options[:field] || field,
  532 + ranges: agg_options[:ranges]
  533 + }.merge(shared_agg_options)
  534 + }
  535 + elsif agg_options[:date_ranges]
  536 + payload[:aggs][field] = {
  537 + date_range: {
  538 + field: agg_options[:field] || field,
  539 + ranges: agg_options[:date_ranges]
  540 + }.merge(shared_agg_options)
  541 + }
  542 + else
  543 + payload[:aggs][field] = {
  544 + terms: {
  545 + field: agg_options[:field] || field,
  546 + size: size
  547 + }.merge(shared_agg_options)
  548 + }
  549 + end
  550 +
  551 + where = {}
  552 + where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
  553 + agg_filters = where_filters(where.merge(agg_options[:where] || {}))
  554 + if agg_filters.any?
  555 + payload[:aggs][field] = {
  556 + filter: {
  557 + bool: {
  558 + must: agg_filters
  559 + }
  560 + },
  561 + aggs: {
  562 + field => payload[:aggs][field]
  563 + }
  564 + }
  565 + end
  566 + end
  567 + end
  568 +
  569 + def set_facets(payload)
  570 + facets = options[:facets] || {}
  571 + facets = Hash[facets.map { |f| [f, {}] }] if facets.is_a?(Array) # convert to more advanced syntax
  572 + facet_limits = {}
  573 + payload[:facets] = {}
  574 + facets.each do |field, facet_options|
  575 + # ask for extra facets due to
  576 + # https://github.com/elasticsearch/elasticsearch/issues/1305
  577 + size = facet_options[:limit] ? facet_options[:limit] + 150 : 1_000
  578 +
  579 + if facet_options[:ranges]
  580 + payload[:facets][field] = {
  581 + range: {
  582 + field.to_sym => facet_options[:ranges]
  583 + }
  584 + }
  585 + elsif facet_options[:stats]
  586 + payload[:facets][field] = {
  587 + terms_stats: {
  588 + key_field: field,
  589 + value_script: below14? ? "doc.score" : "_score",
  590 + size: size
  591 + }
  592 + }
  593 + else
  594 + payload[:facets][field] = {
  595 + terms: {
  596 + field: facet_options[:field] || field,
  597 + size: size
  598 + }
  599 + }
  600 + end
  601 +
  602 + facet_limits[field] = facet_options[:limit] if facet_options[:limit]
  603 +
  604 + # offset is not possible
  605 + # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html
  606 +
  607 + facet_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_facets] == true
  608 + facet_filters = where_filters(facet_options[:where])
  609 + if facet_filters.any?
  610 + payload[:facets][field][:facet_filter] = {
  611 + and: {
  612 + filters: facet_filters
  613 + }
  614 + }
  615 + end
  616 + end
  617 + @facet_limits = facet_limits
  618 + end
  619 +
  620 + def set_filters(payload, filters)
  621 + if options[:facets] || options[:aggs]
  622 + payload[:filter] = {
  623 + and: filters
  624 + }
  625 + else
  626 + # more efficient query if no facets
  627 + payload[:query] = {
  628 + filtered: {
  629 + query: payload[:query],
  630 + filter: {
  631 + and: filters
  632 + }
  633 + }
  634 + }
  635 + end
  636 + end
  637 +
  638 + def set_order(payload)
  639 + order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
  640 + # TODO id transformation for arrays
  641 + payload[:sort] = order.is_a?(Array) ? order : Hash[order.map { |k, v| [k.to_s == "id" ? :_id : k, v] }]
  642 + end
  643 +
616 def where_filters(where) 644 def where_filters(where)
617 filters = [] 645 filters = []
618 (where || {}).each do |field, value| 646 (where || {}).each do |field, value|