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 149 end
150 150  
151 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 154 operator = options[:operator] || (options[:partial] ? "or" : "and")
176 155  
... ... @@ -187,7 +166,7 @@ module Searchkick
187 166 personalize_field = searchkick_options[:personalize]
188 167  
189 168 all = term == "*"
190   - facet_limits = {}
  169 +
191 170  
192 171 options[:json] ||= options[:body]
193 172 if options[:json]
... ... @@ -324,52 +303,11 @@ module Searchkick
324 303 custom_filters = []
325 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 312 if custom_filters.any?
375 313 payload = {
... ... @@ -399,187 +337,24 @@ module Searchkick
399 337 payload[:explain] = options[:explain] if options[:explain]
400 338  
401 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 342 # filters
409 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 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 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 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 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 359 # An empty array will cause only the _id and _type for each hit to be returned
585 360 # doc for :select - http://www.elasticsearch.org/guide/reference/api/search/fields/
... ... @@ -606,13 +381,266 @@ module Searchkick
606 381 end
607 382  
608 383 @body = payload
609   - @facet_limits = facet_limits
  384 + @facet_limits = @facet_limits || {}
610 385 @page = page
611 386 @per_page = per_page
612 387 @padding = padding
613 388 @load = load
614 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 644 def where_filters(where)
617 645 filters = []
618 646 (where || {}).each do |field, value|
... ...