123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Selector::SearchIndex < Selector::Base
- def get
- result = {
- size: options[:limit] || SearchIndexBackend::DEFAULT_QUERY_OPTIONS[:limit],
- }
- query = run(selector, 0)
- if query.present?
- result[:query] = query
- end
- result = query_aggs_range(result)
- query_sort(result)
- end
- def query_sort(query)
- if options[:aggs_interval].present? && options[:aggs_interval][:field].present? && options[:aggs_interval][:interval].blank?
- query_sort_by_aggs_interval(query)
- else
- query_sort_by_index(query)
- end
- query
- end
- def query_sort_by_index(query)
- query[:sort] = SearchIndexBackend.search_by_index_sort(index: target_class.to_s, sort_by: options[:sort_by], order_by: options[:order_by])
- query
- end
- def query_sort_by_aggs_interval(query)
- query[:sort] = [
- {
- options[:aggs_interval][:field] => {
- order: 'desc',
- }
- },
- '_score'
- ]
- query
- end
- def query_aggs_range(query)
- return query if options[:aggs_interval].blank?
- query = query_aggs_interval(query)
- query[:query] = {
- bool: {
- must: [
- {
- range: {
- options[:aggs_interval][:field] => {
- from: options[:aggs_interval][:from],
- to: options[:aggs_interval][:to],
- },
- },
- },
- query[:query],
- ],
- },
- }
- query
- end
- def query_aggs_interval(query)
- return query if options[:aggs_interval][:interval].blank?
- query[:size] = 0
- query[:aggs] = {
- time_buckets: {
- date_histogram: {
- field: options[:aggs_interval][:field],
- calendar_interval: options[:aggs_interval][:interval],
- }
- }
- }
- query_aggs_interval_timezone(query)
- end
- def query_aggs_interval_timezone(query)
- return query if options[:aggs_interval][:timezone].blank?
- query[:aggs][:time_buckets][:date_histogram][:time_zone] = options[:aggs_interval][:timezone]
- query
- end
- def run(block, level)
- if block.key?(:conditions)
- block_query = []
- block[:conditions].each do |sub_block|
- block_query << run(sub_block, level + 1)
- end
- block_query = block_query.compact
- return if block_query.blank?
- operator = :must
- case block[:operator]
- when 'NOT'
- operator = :must_not
- when 'OR'
- operator = :should
- end
- {
- bool: {
- operator => block_query
- }
- }
- else
- condition_query(block)
- end
- end
- def condition_query(block_condition)
- query_must = []
- query_must_not = []
- current_user = options[:current_user]
- current_user_id = UserInfo.current_user_id
- if current_user
- current_user_id = current_user.id
- end
- relative_map = {
- day: 'd',
- year: 'y',
- month: 'M',
- week: 'w',
- hour: 'h',
- minute: 'm',
- }
- operators_is_isnot = ['is', 'is not']
- data = block_condition.clone
- key = data[:name]
- table, key_tmp = key.split('.')
- if key_tmp.blank?
- key_tmp = table
- table = target_name
- end
- wildcard_or_term = 'term'
- if data[:value].is_a?(Array)
- wildcard_or_term = 'terms'
- end
- t = {}
- # use .keyword in case of compare exact values
- if ['is', 'is not', 'is any of', 'is none of', 'starts with one of', 'ends with one of'].include?(data[:operator])
- case data[:pre_condition]
- when 'not_set'
- data[:value] = if key_tmp.match?(%r{^(created_by|updated_by|owner|customer|user)_id})
- 1
- end
- when 'current_user.id'
- raise "Use current_user.id in selector, but no current_user is set #{data.inspect}" if !current_user_id
- data[:value] = []
- wildcard_or_term = 'terms'
- if key_tmp == 'out_of_office_replacement_id'
- data[:value].push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
- else
- data[:value].push current_user_id
- end
- when 'current_user.organization_id'
- raise "Use current_user.id in selector, but no current_user is set #{data.inspect}" if !current_user_id
- user = User.find_by(id: current_user_id)
- data[:value] = user.organization_id
- end
- if data[:value].is_a?(Array)
- data[:value].each do |value|
- next if !value.is_a?(String) || value !~ %r{[A-z]}
- key_tmp += '.keyword'
- break
- end
- elsif data[:value].is_a?(String) && %r{[A-z]}.match?(data[:value])
- key_tmp += '.keyword'
- end
- end
- # use .keyword and wildcard search in cases where query contains non A-z chars
- value_is_string = Array.wrap(data[:value]).any? { |v| v.is_a?(String) && v.match?(%r{[A-z]}) }
- if ['contains', 'contains not', 'starts with one of', 'ends with one of'].include?(data[:operator]) && value_is_string
- wildcard_or_term = 'wildcard'
- if !key_tmp.ends_with?('.keyword')
- key_tmp += '.keyword'
- end
- if data[:value].is_a?(Array)
- or_condition = {
- bool: {
- should: [],
- }
- }
- data[:value].each do |value|
- t = {}
- t[wildcard_or_term] = {}
- t[wildcard_or_term][key_tmp] = if data[:operator] == 'starts with one of'
- "#{value}*"
- elsif data[:operator] == 'ends with one of'
- "*#{value}"
- else
- "*#{value}*"
- end
- or_condition[:bool][:should] << t
- end
- data[:value] = or_condition
- else
- data[:value] = "*#{data[:value]}*"
- end
- end
- if table != target_name
- key_tmp = "#{table}.#{key_tmp}"
- end
- # for pre condition not_set we want to check if values are defined for the object by exists
- if data[:pre_condition] == 'not_set' && operators_is_isnot.include?(data[:operator]) && data[:value].nil?
- t['exists'] = {
- field: key_tmp,
- }
- case data[:operator]
- when 'is'
- query_must_not.push t
- when 'is not'
- query_must.push t
- end
- elsif data[:value].is_a?(Hash) && data[:value][:bool].present?
- query_must.push data[:value]
- # is/is not/contains/contains not
- elsif ['is', 'is not', 'contains', 'contains not', 'is any of', 'is none of'].include?(data[:operator])
- t[wildcard_or_term] = {}
- t[wildcard_or_term][key_tmp] = data[:value]
- case data[:operator]
- when 'is', 'contains', 'is any of'
- query_must.push t
- when 'is not', 'contains not', 'is none of'
- query_must_not.push t
- end
- elsif ['contains all', 'contains one', 'contains all not', 'contains one not'].include?(data[:operator])
- values = data[:value]
- if data[:value].is_a?(String)
- values = values.split(',').map(&:strip)
- end
- t[:query_string] = {}
- case data[:operator]
- when 'contains all'
- t[:query_string][:query] = "#{key_tmp}:(\"#{values.join('" AND "')}\")"
- query_must.push t
- when 'contains one not'
- t[:query_string][:query] = "#{key_tmp}:(\"#{values.join('" OR "')}\")"
- query_must_not.push t
- when 'contains one'
- t[:query_string][:query] = "#{key_tmp}:(\"#{values.join('" OR "')}\")"
- query_must.push t
- when 'contains all not'
- t[:query_string][:query] = "#{key_tmp}:(\"#{values.join('" AND "')}\")"
- query_must_not.push t
- end
- # within last/within next (relative)
- elsif ['within last (relative)', 'within next (relative)'].include?(data[:operator])
- range = relative_map[data[:range].to_sym]
- if range.blank?
- raise "Invalid relative_map for range '#{data[:range]}'."
- end
- t[:range] = {}
- t[:range][key_tmp] = {}
- if data[:operator] == 'within last (relative)'
- t[:range][key_tmp][:gte] = "now-#{data[:value]}#{range}"
- else
- t[:range][key_tmp][:lt] = "now+#{data[:value]}#{range}"
- end
- query_must.push t
- # before/after (relative)
- elsif ['before (relative)', 'after (relative)'].include?(data[:operator])
- range = relative_map[data[:range].to_sym]
- if range.blank?
- raise "Invalid relative_map for range '#{data[:range]}'."
- end
- t[:range] = {}
- t[:range][key_tmp] = {}
- if data[:operator] == 'before (relative)'
- t[:range][key_tmp][:lt] = "now-#{data[:value]}#{range}"
- else
- t[:range][key_tmp][:gt] = "now+#{data[:value]}#{range}"
- end
- query_must.push t
- # till/from (relative)
- elsif ['till (relative)', 'from (relative)'].include?(data[:operator])
- range = relative_map[data[:range].to_sym]
- if range.blank?
- raise "Invalid relative_map for range '#{data[:range]}'."
- end
- t[:range] = {}
- t[:range][key_tmp] = {}
- if data[:operator] == 'till (relative)'
- t[:range][key_tmp][:lt] = "now+#{data[:value]}#{range}"
- else
- t[:range][key_tmp][:gt] = "now-#{data[:value]}#{range}"
- end
- query_must.push t
- # before/after (absolute)
- elsif ['before (absolute)', 'after (absolute)'].include?(data[:operator])
- t[:range] = {}
- t[:range][key_tmp] = {}
- if data[:operator] == 'before (absolute)'
- t[:range][key_tmp][:lt] = (data[:value])
- else
- t[:range][key_tmp][:gt] = (data[:value])
- end
- query_must.push t
- elsif data[:operator] == 'today'
- t[:range] = {}
- t[:range][key_tmp] = {}
- t[:range][key_tmp][:gte] = "#{Time.zone.today}T00:00:00Z"
- t[:range][key_tmp][:lte] = "#{Time.zone.today}T23:59:59Z"
- query_must.push t
- else
- raise "unknown operator '#{data[:operator]}' for #{key}"
- end
- data = {
- bool: {},
- }
- if query_must.present?
- data[:bool][:must] = query_must
- end
- if query_must_not.present?
- data[:bool][:must_not] = query_must_not
- end
- data
- end
- end
|