Search query extension refactoring to use params and not arguments.

This commit is contained in:
Martin Edenhofer 2018-11-06 17:11:10 +01:00
parent 9ef3cb62c0
commit c2f4755a6c
9 changed files with 127 additions and 62 deletions

View file

@ -205,21 +205,21 @@ class FormController < ApplicationController
return false if !SearchIndexBackend.enabled? return false if !SearchIndexBackend.enabled?
form_limit_by_ip_per_hour = Setting.get('form_ticket_create_by_ip_per_hour') || 20 form_limit_by_ip_per_hour = Setting.get('form_ticket_create_by_ip_per_hour') || 20
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1h", form_limit_by_ip_per_hour, 'Ticket') result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1h", 'Ticket', limit: form_limit_by_ip_per_hour)
if result.count >= form_limit_by_ip_per_hour.to_i if result.count >= form_limit_by_ip_per_hour.to_i
response_access_deny response_access_deny
return true return true
end end
form_limit_by_ip_per_day = Setting.get('form_ticket_create_by_ip_per_day') || 240 form_limit_by_ip_per_day = Setting.get('form_ticket_create_by_ip_per_day') || 240
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1d", form_limit_by_ip_per_day, 'Ticket') result = SearchIndexBackend.search("preferences.form.remote_ip:'#{request.remote_ip}' AND created_at:>now-1d", 'Ticket', limit: form_limit_by_ip_per_day)
if result.count >= form_limit_by_ip_per_day.to_i if result.count >= form_limit_by_ip_per_day.to_i
response_access_deny response_access_deny
return true return true
end end
form_limit_per_day = Setting.get('form_ticket_create_per_day') || 5000 form_limit_per_day = Setting.get('form_ticket_create_per_day') || 5000
result = SearchIndexBackend.search('preferences.form.remote_ip:* AND created_at:>now-1d', form_limit_per_day, 'Ticket') result = SearchIndexBackend.search('preferences.form.remote_ip:* AND created_at:>now-1d', 'Ticket', limit: form_limit_per_day)
if result.count >= form_limit_per_day.to_i if result.count >= form_limit_per_day.to_i
response_access_deny response_access_deny
return true return true

View file

@ -64,7 +64,7 @@ class SearchController < ApplicationController
# do only one query to index search backend # do only one query to index search backend
if objects_with_direct_search_index.present? if objects_with_direct_search_index.present?
items = SearchIndexBackend.search(query, limit, objects_with_direct_search_index) items = SearchIndexBackend.search(query, objects_with_direct_search_index, limit: limit)
items.each do |item| items.each do |item|
require_dependency item[:type].to_filename require_dependency item[:type].to_filename
local_class = Kernel.const_get(item[:type]) local_class = Kernel.const_get(item[:type])

View file

@ -64,7 +64,7 @@ returns
# try search index backend # try search index backend
if SearchIndexBackend.enabled? if SearchIndexBackend.enabled?
items = SearchIndexBackend.search(query, limit, 'Chat::Session', {}, offset) items = SearchIndexBackend.search(query, 'Chat::Session', limit: limit, from: offset)
chat_sessions = [] chat_sessions = []
items.each do |item| items.each do |item|
chat_session = Chat::Session.lookup(id: item[:id]) chat_session = Chat::Session.lookup(id: item[:id])

View file

@ -83,7 +83,10 @@ returns
# try search index backend # try search index backend
if SearchIndexBackend.enabled? if SearchIndexBackend.enabled?
items = SearchIndexBackend.search(query, limit, 'Organization', {}, offset, sort_by, order_by) items = SearchIndexBackend.search(query, 'Organization', limit: limit,
from: offset,
sort_by: sort_by,
order_by: order_by)
organizations = [] organizations = []
items.each do |item| items.each do |item|
organization = Organization.lookup(id: item[:id]) organization = Organization.lookup(id: item[:id])

View file

@ -127,9 +127,9 @@ returns
# try search index backend # try search index backend
if condition.blank? && SearchIndexBackend.enabled? if condition.blank? && SearchIndexBackend.enabled?
query_extention = {} query_extension = {}
query_extention['bool'] = {} query_extension['bool'] = {}
query_extention['bool']['must'] = [] query_extension['bool']['must'] = []
if current_user.permissions?('ticket.agent') if current_user.permissions?('ticket.agent')
group_ids = current_user.group_ids_access('read') group_ids = current_user.group_ids_access('read')
@ -152,9 +152,13 @@ returns
end end
end end
query_extention['bool']['must'].push access_condition query_extension['bool']['must'].push access_condition
items = SearchIndexBackend.search(query, limit, 'Ticket', query_extention, offset, sort_by, order_by) items = SearchIndexBackend.search(query, 'Ticket', limit: limit,
query_extension: query_extension,
from: offset,
sort_by: sort_by,
order_by: order_by)
if !full if !full
ids = [] ids = []
items.each do |item| items.each do |item|

View file

@ -101,19 +101,24 @@ returns
# try search index backend # try search index backend
if SearchIndexBackend.enabled? if SearchIndexBackend.enabled?
query_extention = {} query_extension = {}
if params[:role_ids].present? if params[:role_ids].present?
query_extention['bool'] = {} query_extension['bool'] = {}
query_extention['bool']['must'] = [] query_extension['bool']['must'] = []
if !params[:role_ids].is_a?(Array) if !params[:role_ids].is_a?(Array)
params[:role_ids] = [params[:role_ids]] params[:role_ids] = [params[:role_ids]]
end end
access_condition = { access_condition = {
'query_string' => { 'default_field' => 'role_ids', 'query' => "\"#{params[:role_ids].join('" OR "')}\"" } 'query_string' => { 'default_field' => 'role_ids', 'query' => "\"#{params[:role_ids].join('" OR "')}\"" }
} }
query_extention['bool']['must'].push access_condition query_extension['bool']['must'].push access_condition
end end
items = SearchIndexBackend.search(query, limit, 'User', query_extention, offset, sort_by, order_by)
items = SearchIndexBackend.search(query, 'User', limit: limit,
query_extension: query_extension,
from: offset,
sort_by: sort_by,
order_by: order_by)
users = [] users = []
items.each do |item| items.each do |item|
user = User.lookup(id: item[:id]) user = User.lookup(id: item[:id])

View file

@ -36,7 +36,7 @@ class CleanupUserPreferencesNotificationSound2 < ActiveRecord::Migration[5.1]
user.save! user.save!
end end
items = SearchIndexBackend.search('preferences.notification_sound.enabled:*', 3000, 'User') || [] items = SearchIndexBackend.search('preferences.notification_sound.enabled:*', 'User', limit: 3000) || []
items.each do |item| items.each do |item|
next if !item[:id] next if !item[:id]

View file

@ -274,13 +274,19 @@ remove whole data from index
=begin =begin
return search result @param query [String] search query
@param index [String, Array<String>, nil] indexes to search in (see search_by_index)
@param options [Hash] search options (see build_query)
result = SearchIndexBackend.search('search query', limit, ['User', 'Organization']) @return search result
result = SearchIndexBackend.search('search query', limit, 'User') @example Sample queries
result = SearchIndexBackend.search('search query', limit, 'User', ['updated_at'], ['desc']) result = SearchIndexBackend.search('search query', ['User', 'Organization'], limit: limit)
result = SearchIndexBackend.search('search query', 'User', limit: limit)
result = SearchIndexBackend.search('search query', 'User', limit: limit, sort_by: ['updated_at'], order_by: ['desc'])
result = [ result = [
{ {
@ -299,25 +305,28 @@ return search result
=end =end
# rubocop:disable Metrics/ParameterLists def self.search(query, index = nil, options = {})
def self.search(query, limit = 10, index = nil, query_extention = {}, from = 0, sort_by = [], order_by = []) if !index.is_a? Array
# rubocop:enable Metrics/ParameterLists return search_by_index(query, index, options)
return [] if query.blank?
if index.class == Array
ids = []
index.each do |local_index|
local_ids = search_by_index(query, limit, local_index, query_extention, from, sort_by, order_by )
ids = ids.concat(local_ids)
end
return ids
end
search_by_index(query, limit, index, query_extention, from, sort_by, order_by)
end end
# rubocop:disable Metrics/ParameterLists index
def self.search_by_index(query, limit = 10, index = nil, query_extention = {}, from = 0, sort_by = [], order_by = []) .map { |local_index| search_by_index(query, local_index, options) }
# rubocop:enable Metrics/ParameterLists .compact
.flatten(1)
end
=begin
@param query [String] search query
@param index [String, Array<String>, nil] index name or list of index names. If index is nil or not present will, search will be performed globally
@param options [Hash] search options (see build_query)
@return search result
=end
def self.search_by_index(query, index = nil, options = {})
return [] if query.blank? return [] if query.blank?
url = build_url url = build_url
@ -332,15 +341,6 @@ return search result
else else
'/_search' '/_search'
end end
data = {}
data['from'] = from
data['size'] = limit
data['sort'] = search_by_index_sort(sort_by, order_by)
data['query'] = query_extention || {}
data['query']['bool'] ||= {}
data['query']['bool']['must'] ||= []
# real search condition # real search condition
condition = { condition = {
@ -350,14 +350,15 @@ return search result
'analyze_wildcard' => true, 'analyze_wildcard' => true,
} }
} }
data['query']['bool']['must'].push condition
query_data = build_query(condition, options)
Rails.logger.info "# curl -X POST \"#{url}\" \\" Rails.logger.info "# curl -X POST \"#{url}\" \\"
Rails.logger.debug { " -d'#{data.to_json}'" } Rails.logger.debug { " -d'#{query_data.to_json}'" }
response = UserAgent.get( response = UserAgent.get(
url, url,
data, query_data,
{ {
json: true, json: true,
open_timeout: 5, open_timeout: 5,
@ -372,35 +373,31 @@ return search result
Rails.logger.error humanized_error( Rails.logger.error humanized_error(
verb: 'GET', verb: 'GET',
url: url, url: url,
payload: data, payload: query_data,
response: response, response: response,
) )
return [] return []
end end
data = response.data data = response.data&.dig('hits', 'hits')
ids = [] return [] if !data
return ids if !data
return ids if !data['hits']
return ids if !data['hits']['hits']
data['hits']['hits'].each do |item| data.map do |item|
Rails.logger.info "... #{item['_type']} #{item['_id']}" Rails.logger.info "... #{item['_type']} #{item['_id']}"
data = {
{
id: item['_id'], id: item['_id'],
type: item['_type'], type: item['_type'],
} }
ids.push data
end end
ids
end end
def self.search_by_index_sort(sort_by = [], order_by = []) def self.search_by_index_sort(sort_by = nil, order_by = nil)
result = [] result = []
sort_by.each_with_index do |value, index| sort_by&.each_with_index do |value, index|
next if value.blank? next if value.blank?
next if order_by[index].blank? next if order_by&.at(index).blank?
if value !~ /\./ && value !~ /_(time|date|till|id|ids|at)$/ if value !~ /\./ && value !~ /_(time|date|till|id|ids|at)$/
value += '.raw' value += '.raw'
@ -735,4 +732,44 @@ return true if backend is configured
query += '*' if !query.match?(/:/) query += '*' if !query.match?(/:/)
query query
end end
=begin
@param condition [Hash] search condition
@param options [Hash] search options
@option options [Integer] :from
@option options [Integer] :limit
@option options [Hash] :query_extension applied to ElasticSearch query
@option options [Array<String>] :order_by ordering directions, desc or asc
@option options [Array<String>] :sort_by fields to sort by
=end
DEFAULT_QUERY_OPTIONS = {
from: 0,
limit: 10
}.freeze
def self.build_query(condition, options = {})
options = DEFAULT_QUERY_OPTIONS.merge(options.deep_symbolize_keys)
data = {
from: options[:from],
size: options[:limit],
sort: search_by_index_sort(options[:sort_by], options[:order_by]),
query: {
bool: {
must: []
}
}
}
if (extension = options.dig(:query_extension))
data[:query].deep_merge! extension.deep_dup
end
data[:query][:bool][:must].push condition
data
end
end end

View file

@ -1,6 +1,21 @@
require 'test_helper' require 'test_helper'
class SearchIndexBackendTest < ActiveSupport::TestCase class SearchIndexBackendTest < ActiveSupport::TestCase
test 'query extension keys are normalized to symbols' do
query_strings = SearchIndexBackend.build_query('', query_extension: { 'bool' => { 'filter' => { 'term' => { 'a' => 'b' } } } })
query_symbols = SearchIndexBackend.build_query('', query_extension: { bool: { filter: { term: { a: 'b' } } } })
assert_equal query_strings, query_symbols
assert_not_nil query_strings.dig(:query, :bool, :filter, :term, :a)
end
test 'search with ES off never returns nil in array' do
index_one = SearchIndexBackend.search('preferences.notification_sound.enabled:*', 'User', limit: 3000)
index_multi = SearchIndexBackend.search('preferences.notification_sound.enabled:*', %w[User Organization], limit: 3000)
assert_nil index_one
assert index_multi.empty?
end
test 'simple_query_append_wildcard correctly modifies simple queries' do test 'simple_query_append_wildcard correctly modifies simple queries' do
def clean_queries(query_string) def clean_queries(query_string)
@ -48,6 +63,7 @@ class SearchIndexBackendTest < ActiveSupport::TestCase
simple_queries = clean_queries %( simple_queries = clean_queries %(
M M
Max Max
Max. # dot and underscore are acceptable characters in simple queries Max. # dot and underscore are acceptable characters in simple queries
A_ A_