# 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].map do |sub_block| 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