2021-07-19 20:23:36 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-02-12 23:52:43 +08:00
|
|
|
module SearchableModel
|
|
|
|
extend ActiveSupport::Concern
|
|
|
|
|
|
|
|
included do
|
|
|
|
# Helper function for relations that
|
|
|
|
# adds OR ILIKE where clause for all specified attributes
|
|
|
|
# for the given search query
|
2017-05-05 22:41:23 +08:00
|
|
|
scope :where_attributes_like, lambda { |attributes, query, options = {}|
|
|
|
|
return unless query
|
2020-01-13 23:09:07 +08:00
|
|
|
|
2024-04-26 17:14:42 +08:00
|
|
|
attrs = normalized_attributes(attributes)
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2017-05-05 22:41:23 +08:00
|
|
|
if options[:whole_word].to_s == 'true' ||
|
2018-07-06 20:26:20 +08:00
|
|
|
options[:whole_phrase].to_s == 'true' ||
|
|
|
|
options[:at_search].to_s == 'true'
|
2021-07-23 17:56:28 +08:00
|
|
|
unless attrs.blank?
|
2017-05-05 22:41:23 +08:00
|
|
|
like = options[:match_case].to_s == 'true' ? '~' : '~*'
|
2018-07-06 20:26:20 +08:00
|
|
|
like = 'SIMILAR TO' if options[:at_search].to_s == 'true'
|
2017-05-05 22:41:23 +08:00
|
|
|
|
|
|
|
if options[:whole_word].to_s == 'true'
|
|
|
|
a_query = query.split
|
|
|
|
.map { |a| Regexp.escape(a) }
|
|
|
|
.join('|')
|
2018-12-03 15:52:42 +08:00
|
|
|
a_query = "(#{a_query})"
|
2018-07-06 20:26:20 +08:00
|
|
|
elsif options[:at_search].to_s == 'true'
|
|
|
|
a_query = "%#{Regexp.escape(query).downcase}%"
|
2017-05-05 22:41:23 +08:00
|
|
|
else
|
|
|
|
a_query = Regexp.escape(query)
|
|
|
|
end
|
2018-07-06 20:26:20 +08:00
|
|
|
|
2017-05-05 22:41:23 +08:00
|
|
|
where_str =
|
|
|
|
(attrs.map.with_index do |a, i|
|
2020-01-10 16:46:25 +08:00
|
|
|
if %w(repository_rows.id repository_number_values.data).include?(a)
|
2020-01-13 23:09:07 +08:00
|
|
|
"((#{a})::text) #{like} :t#{i} OR "
|
2021-07-19 20:23:36 +08:00
|
|
|
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
|
|
|
|
"#{a} #{like} :t#{i} OR "
|
2018-04-26 17:05:02 +08:00
|
|
|
else
|
2018-07-06 20:26:20 +08:00
|
|
|
col = options[:at_search].to_s == 'true' ? "lower(#{a})": a
|
2020-01-13 23:09:07 +08:00
|
|
|
"(trim_html_tags(#{col})) #{like} :t#{i} OR "
|
2018-04-26 17:05:02 +08:00
|
|
|
end
|
2017-05-05 22:41:23 +08:00
|
|
|
end
|
|
|
|
).join[0..-5]
|
|
|
|
vals = (
|
|
|
|
attrs.map.with_index do |_, i|
|
2018-12-03 15:52:42 +08:00
|
|
|
["t#{i}".to_sym, a_query]
|
2017-05-05 22:41:23 +08:00
|
|
|
end
|
|
|
|
).to_h
|
|
|
|
return where(where_str, vals)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
like = options[:match_case].to_s == 'true' ? 'LIKE' : 'ILIKE'
|
|
|
|
|
|
|
|
if query.count(' ') > 0
|
2021-07-23 17:56:28 +08:00
|
|
|
unless attrs.blank?
|
2017-05-05 22:41:23 +08:00
|
|
|
a_query = query.split.map { |a| "%#{sanitize_sql_like(a)}%" }
|
2016-07-21 19:11:15 +08:00
|
|
|
where_str =
|
2016-11-11 18:41:23 +08:00
|
|
|
(attrs.map.with_index do |a, i|
|
2020-01-09 18:09:24 +08:00
|
|
|
if %w(repository_rows.id repository_number_values.data).include?(a)
|
2020-01-13 23:09:07 +08:00
|
|
|
"((#{a})::text) #{like} ANY (array[:t#{i}]) OR "
|
2021-07-19 20:23:36 +08:00
|
|
|
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
|
|
|
|
"#{a} #{like} ANY (array[:t#{i}]) OR "
|
2018-04-26 17:05:02 +08:00
|
|
|
else
|
|
|
|
"(trim_html_tags(#{a})) #{like} ANY (array[:t#{i}]) OR "
|
|
|
|
end
|
2016-11-11 18:41:23 +08:00
|
|
|
end
|
|
|
|
).join[0..-5]
|
2017-05-05 22:41:23 +08:00
|
|
|
vals = (
|
|
|
|
attrs.map.with_index do |_, i|
|
2018-11-30 22:32:01 +08:00
|
|
|
["t#{i}".to_sym, a_query]
|
2017-05-05 22:41:23 +08:00
|
|
|
end
|
|
|
|
).to_h
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2016-07-21 19:11:15 +08:00
|
|
|
return where(where_str, vals)
|
|
|
|
end
|
|
|
|
else
|
2021-07-23 17:56:28 +08:00
|
|
|
unless attrs.blank?
|
2016-07-21 19:11:15 +08:00
|
|
|
where_str =
|
2016-11-11 18:41:23 +08:00
|
|
|
(attrs.map.with_index do |a, i|
|
2020-01-10 16:46:25 +08:00
|
|
|
if %w(repository_rows.id repository_number_values.data).include?(a)
|
2020-01-13 23:09:07 +08:00
|
|
|
"((#{a})::text) #{like} :t#{i} OR "
|
2021-07-19 20:23:36 +08:00
|
|
|
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
|
|
|
|
"#{a} #{like} :t#{i} OR "
|
2018-04-04 17:55:11 +08:00
|
|
|
else
|
2020-01-10 16:46:25 +08:00
|
|
|
"(trim_html_tags(#{a})) #{like} :t#{i} OR "
|
2018-04-04 17:55:11 +08:00
|
|
|
end
|
2016-11-11 18:41:23 +08:00
|
|
|
end
|
|
|
|
).join[0..-5]
|
2017-05-05 22:41:23 +08:00
|
|
|
vals = (
|
|
|
|
attrs.map.with_index do |_, i|
|
2018-11-30 22:32:01 +08:00
|
|
|
["t#{i}".to_sym, "%#{sanitize_sql_like(query.to_s)}%"]
|
2017-05-05 22:41:23 +08:00
|
|
|
end
|
|
|
|
).to_h
|
2016-07-21 19:11:15 +08:00
|
|
|
|
|
|
|
return where(where_str, vals)
|
|
|
|
end
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
2017-05-05 22:41:23 +08:00
|
|
|
}
|
2024-04-18 21:45:34 +08:00
|
|
|
|
|
|
|
scope :where_attributes_like_boolean, lambda { |attributes, query, options = {}|
|
|
|
|
return unless query
|
|
|
|
|
2024-05-15 17:59:05 +08:00
|
|
|
normalized_attrs = normalized_attributes(attributes)
|
|
|
|
query_clauses = []
|
|
|
|
value_hash = {}
|
|
|
|
|
|
|
|
extract_phrases(query).each_with_index do |phrase, index|
|
|
|
|
if options[:with_subquery]
|
|
|
|
subquery_result = if phrase[:negate]
|
|
|
|
options[:raw_input].where.not(id: search_subquery(phrase[:query], options[:raw_input]))
|
|
|
|
else
|
|
|
|
options[:raw_input].where(id: search_subquery(phrase[:query], options[:raw_input]))
|
|
|
|
end
|
|
|
|
query_clauses = if index.zero?
|
|
|
|
where(id: subquery_result)
|
|
|
|
elsif phrase[:current_operator] == 'or'
|
|
|
|
query_clauses.or(subquery_result)
|
|
|
|
else
|
|
|
|
query_clauses.and(subquery_result)
|
|
|
|
end
|
2024-04-18 21:45:34 +08:00
|
|
|
else
|
2024-05-15 17:59:05 +08:00
|
|
|
phrase[:current_operator] = '' if index.zero?
|
2024-05-29 22:03:14 +08:00
|
|
|
create_query_clause(normalized_attrs, index, phrase[:negate], query_clauses,
|
|
|
|
value_hash, phrase[:query], phrase[:current_operator])
|
2024-04-18 21:45:34 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-15 17:59:05 +08:00
|
|
|
options[:with_subquery] ? query_clauses : where(query_clauses.join, value_hash)
|
2024-04-18 21:45:34 +08:00
|
|
|
}
|
|
|
|
|
2024-04-26 17:14:42 +08:00
|
|
|
def self.normalized_attributes(attributes)
|
2024-04-18 21:45:34 +08:00
|
|
|
attrs = []
|
|
|
|
if attributes.blank?
|
|
|
|
# Do nothing in this case
|
|
|
|
elsif attributes.is_a? Symbol
|
|
|
|
attrs = [attributes.to_s]
|
|
|
|
elsif attributes.is_a? String
|
|
|
|
attrs = [attributes]
|
|
|
|
elsif attributes.is_a? Array
|
|
|
|
attrs = attributes.collect(&:to_s)
|
|
|
|
else
|
|
|
|
raise ArgumentError, ':attributes must be an array, symbol or string'
|
|
|
|
end
|
|
|
|
|
|
|
|
attrs
|
|
|
|
end
|
|
|
|
|
2024-05-15 17:59:05 +08:00
|
|
|
def self.extract_phrases(query)
|
|
|
|
extracted_phrases = []
|
|
|
|
negate = false
|
|
|
|
current_operator = ''
|
|
|
|
|
|
|
|
query.scan(/"[^"]+"|\S+/) do |phrase|
|
2024-05-29 22:03:14 +08:00
|
|
|
phrase = phrase.to_s.strip
|
2024-05-15 17:59:05 +08:00
|
|
|
|
|
|
|
case phrase.downcase
|
|
|
|
when *%w(and or)
|
|
|
|
current_operator = phrase.downcase
|
|
|
|
when 'not'
|
|
|
|
negate = true
|
|
|
|
else
|
|
|
|
extracted_phrases << { query: phrase,
|
|
|
|
negate: negate,
|
|
|
|
current_operator: current_operator.presence || 'and' }
|
|
|
|
current_operator = ''
|
|
|
|
negate = false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
extracted_phrases
|
|
|
|
end
|
|
|
|
|
2024-05-29 22:03:14 +08:00
|
|
|
def self.create_query_clause(attrs, index, negate, query_clauses, value_hash, phrase, current_operator)
|
|
|
|
phrase = sanitize_sql_like(phrase)
|
|
|
|
phrase = Regexp.escape(phrase)
|
|
|
|
exact_match = phrase =~ /^".*"$/
|
2024-04-18 21:45:34 +08:00
|
|
|
like = exact_match ? '~' : 'ILIKE'
|
2024-05-29 22:03:14 +08:00
|
|
|
phrase = exact_match ? "(^|\\s)#{phrase[1..-2]}(\\s|$)" : "%#{phrase}%"
|
2024-04-18 21:45:34 +08:00
|
|
|
|
|
|
|
where_clause = (attrs.map.with_index do |a, i|
|
|
|
|
i = (index * attrs.count) + i
|
|
|
|
if %w(repository_rows.id repository_number_values.data).include?(a)
|
|
|
|
"#{a} IS NOT NULL AND (((#{a})::text) #{like} :t#{i}) OR "
|
|
|
|
elsif defined?(model::PREFIXED_ID_SQL) && a == model::PREFIXED_ID_SQL
|
|
|
|
"#{a} IS NOT NULL AND (#{a} #{like} :t#{i}) OR "
|
2024-05-24 17:24:33 +08:00
|
|
|
elsif ['asset_text_data.data_vector', 'tables.data_vector'].include?(a)
|
|
|
|
"#{a} @@ plainto_tsquery(:t#{i}) OR "
|
2024-04-18 21:45:34 +08:00
|
|
|
else
|
|
|
|
"#{a} IS NOT NULL AND ((trim_html_tags(#{a})) #{like} :t#{i}) OR "
|
|
|
|
end
|
|
|
|
end).join[0..-5]
|
|
|
|
|
2024-05-15 17:59:05 +08:00
|
|
|
query_clauses << if negate
|
|
|
|
" #{current_operator} NOT (#{where_clause})"
|
|
|
|
else
|
|
|
|
"#{current_operator} (#{where_clause})"
|
|
|
|
end
|
2024-04-18 21:45:34 +08:00
|
|
|
|
2024-05-15 17:59:05 +08:00
|
|
|
value_hash.merge!(
|
2024-04-18 21:45:34 +08:00
|
|
|
(attrs.map.with_index do |_, i|
|
|
|
|
i = (index * attrs.count) + i
|
|
|
|
["t#{i}".to_sym, phrase]
|
|
|
|
end).to_h
|
|
|
|
)
|
|
|
|
end
|
2024-05-15 17:59:05 +08:00
|
|
|
|
|
|
|
def self.search_subquery(query, raw_input)
|
|
|
|
raise NotImplementedError
|
|
|
|
end
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
2016-11-11 18:41:23 +08:00
|
|
|
end
|