eBooru/app/logical/elastic_query_builder.rb

160 lines
4.0 KiB
Ruby

# frozen_string_literal: true
class ElasticQueryBuilder
attr_accessor :q, :must, :must_not, :should, :order
def initialize(query)
@q = query
@must = [] # These terms are ANDed together
@must_not = [] # These terms are NOT ANDed together
@should = [] # These terms are ORed together
@order = []
@function_score = nil
build
end
def search
if must.empty?
must.push({ match_all: {} })
end
query = {
bool: {
must: must,
must_not: must_not,
should: should,
},
}
query[:bool][:minimum_should_match] = 1 if should.any?
if @function_score.present?
@function_score[:query] = query
query = { function_score: @function_score }
end
search_body = {
query: query,
sort: order,
_source: false,
timeout: "#{CurrentUser.user.try(:statement_timeout) || 3_000}ms",
}
model_class.document_store.search(search_body)
end
def match_any(*args)
# Explicitly set minimum should match, even though it may not be required in this context.
{ bool: { minimum_should_match: 1, should: args } }
end
def match_none(*args)
{ bool: { must_not: args } }
end
def range_relation(arr, field)
return if arr.nil?
return if arr.size < 2
return if arr[1].nil?
case arr[0]
when :eq
if arr[1].is_a?(Time)
{ range: { field => { gte: arr[1].beginning_of_day, lte: arr[1].end_of_day } } }
else
{ term: { field => arr[1] } }
end
when :gt
{ range: { field => { gt: arr[1] } } }
when :gte
{ range: { field => { gte: arr[1] } } }
when :lt
{ range: { field => { lt: arr[1] } } }
when :lte
{ range: { field => { lte: arr[1] } } }
when :in
{ terms: { field => arr[1] } }
when :between
{ range: { field => { gte: arr[1], lte: arr[2] } } }
end
end
def add_array_range_relation(key, index_field)
if q[key]
must.concat(q[key].map { |x| range_relation(x, index_field) })
end
if q[:"#{key}_must_not"]
must_not.concat(q[:"#{key}_must_not"].map { |x| range_relation(x, index_field) })
end
if q[:"#{key}_should"]
should.concat(q[:"#{key}_should"].map { |x| range_relation(x, index_field) })
end
end
def add_array_relation(key, index_field, any_none_key: nil, action: :term)
if q[key]
must.concat(q[key].map { |x| { action => { index_field => x } } })
end
if q[:"#{key}_must_not"]
must_not.concat(q[:"#{key}_must_not"].map { |x| { action => { index_field => x } } })
end
if q[:"#{key}_should"]
should.concat(q[:"#{key}_should"].map { |x| { action => { index_field => x } } })
end
if q[any_none_key] == "any"
must.push({ exists: { field: index_field } })
elsif q[any_none_key] == "none"
must_not.push({ exists: { field: index_field } })
end
if q[:"#{any_none_key}_should"] == "any"
should.push({ exists: { field: index_field } })
elsif q[:"#{any_none_key}_should"] == "none"
should.push(match_none({ exists: { field: index_field } }))
end
end
def add_boolean_match(key, index_field)
return if q[key].blank?
if q[key].to_s.truthy?
must.push({ term: { index_field => true } })
elsif q[key].to_s.falsy?
must.push({ term: { index_field => false } })
else
raise ArgumentError, "value must be truthy or falsy"
end
end
def add_range_relation(key, index_field, type: :integer)
if q[key].present?
must.push(range_relation(ParseValue.range(q[key], type), index_field))
end
end
def add_text_match(key, index_field)
value = q[key]
return if value.blank?
must.push(match: { index_field => value })
end
def apply_basic_order(key: :order)
case q[key]
when "id_asc"
order.push({ id: { order: "asc" } })
when "id_desc"
order.push({ id: { order: "desc" } })
else
apply_default_order
end
end
def apply_default_order
order.push({ id: { order: "desc" } })
end
end