Commit cdf7be657ba72418a7207dbf25f52f70bf435ba5

Authored by Omar Mowafi
1 parent c0c8a77e

Refactored Searchkick::Query#prepare

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