require "fiona7/search_engine" require "rails_connector/verity_search_request" require "fiona7/custom_verity_accessor" require "fiona7/attribute_names_from_queries" require "fiona7/attribute_names_from_cms" module Fiona7 class VeritySearchEngine < SearchEngine def initialize(*args) super # scrivito sdk likes to send a query with limit 0 # just to fetch the total count of results # but verity does not like limit 0 very much if @limit == 0 @limit = 1 end end def results @results || fetch_results end def objects return @objects if @objects @results || fetch_results fetch_objects end protected def construct_search_request # TODO: sort order! options = {} options[:sort_order] = [["score", "desc"], ["lastChanged", "desc"]] options[:limit] = @limit.to_i if @limit options[:offset] = @offset.to_i if @offset.to_i > 0 VeritySearchRequest.new(@query, options, @klass == Fiona7::ReleasedObj) end def fetch_results @search_request = construct_search_request @search_response = @search_request.fetch_hits @count = @search_response.total_hits @results = @search_response.map(&:id) end def fetch_objects unordered_objects = @klass.where(obj_id: @results.map(&:id)).to_a ordered_objects = @results.map {|r| unordered_objects.find {|o| o.id.to_i == r.id } }.compact end class VeritySearchRequest < ::RailsConnector::VeritySearchRequest def initialize(query, options={}, use_released=false) @query = query @query_string = build_query_string(query) Rails.logger.debug "VERITY QUERY:\n#{@query_string}" @options = default_search_options.merge(options) @use_released ||= use_released end def fetch_hits Fiona7::CustomVerityAccessor.new(@query_string, {:base_query => base_query}.merge(@options)).search end protected class << self attr_accessor :configured_host, :configured_port end def default_search_options { :host => self.class.configured_host, :port => self.class.configured_port, :collection => 'cm-contents' } end def build_query_string(query) query = normalize_query(query) complex_query_string(query) end def complex_query_string(query) conditions = query.map do |q| field = resolve_field_name(q[:field]) # paths are sadly not in the search index by default. next if field == :visiblePath case q[:operator] when :equal if field == :__dummy__ '("edited" <#IN> state)' else search_in(field, q[:value]) end when :search if field == :'*' full_text_query_string(as_values_array(q[:value])) else values = as_values_array(q[:value]) values = values.map {|v| "*#{v}*" } search_in(field, values) end when :greater_than search_term(field, ">", q[:value]) when :less_than search_term(field, "<", q[:value]) when :prefix, :prefix_search if field == :'*' values = as_values_array(q[:value]) values = values.map {|v| "#{v}*" } full_text_query_string(values) else values = as_values_array(q[:value]) values = values.map {|v| "#{v}*" } search_in(field, values) end when :__in__ search_in(field, q[:value]) when :__not_in__ search_not_in(field, q[:value]) else raise "Operator: #{q[:operator]} not supported" end end.compact if conditions.empty? query_string = "" else query_string = "<#AND> (" + conditions.join(", ") + ")" end end def search_in(field, values) values = [values] unless values.kind_of?(Array) if [:objClass, :obj_id, :permalink, :lastChanged, :name].include?(field) fields = [field.to_s] else fields = (Fiona7::AttributeNamesFromQueries.new(field.to_s, @query).attributes || Fiona7::AttributeNamesFromCms.new(field.to_s).attributes).presence || [field.to_s] # NOTE: verity does not like more than 94 fields in its queries # so to be safe we limit the number of fields to 80 fields = fields[0, 80] end sanitized_values = values.map {|v| "`#{self.class.sanitize(v)}`" } %|<#OR> ((#{sanitized_values.join(', ')}) <#IN> (#{fields.join(', ')}))| end def search_not_in(field, values) %|<#NOT> (#{search_in(field, values)})| end def search_term(field, operator, values, options={}) values = [values] unless values.kind_of?(Array) join_op = options[:join] || "OR" negate = options[:negate] ? "<#NOT> " : "" reverse = options[:reverse] condition = values.map do |value| if reverse %|(#{negate}`#{self.class.sanitize(value)}` #{operator} #{field})| else %|(#{negate}#{field} #{operator} `#{self.class.sanitize(value)}`)| end end.join(", ") %|(<##{join_op}> #{condition})| end def resolve_field_name(field) case field when :_obj_class :objClass when :_path :visiblePath when :_modification :__dummy__ when :id :obj_id when :_permalink :permalink when :_last_changed :lastChanged when :_name :name else field end end def normalize_query(query) query = query.map do |q| q.symbolize_keys q[:operator] = q[:operator].to_sym q[:field] = q[:field].to_sym q end end def as_values_array(values) values = [values] unless values.kind_of?(Array) values end def extract_words(q) q = query.find {|q| q[:field] == :'*' && (q[:operator] == :prefix_search || q[:operator] == :search) } if q[:value].kind_of?(Array) q[:value] else [q[:value]] end end def full_text_query_string(words) words = words.map do |word| word = self.class.sanitize(word) word unless %w(and or not).include?(word.downcase) end.compact.join(", ") "<#AND> (#{words})" end def base_query_conditions conditions = {} conditions[:objTypes] = '<#ANY> ( ("generic" <#IN> objType), ("document" <#IN> objType), ("publication" <#IN> objType) )' if @use_released conditions[:content] = '("released" <#IN> state)' conditions[:suppressExport] = '("0" <#IN> suppressExport)' conditions[:validFrom] = "(validFrom < #{Time.now.to_iso})" conditions[:validUntil] = %!<#OR> ( (validUntil = ""), (validUntil > #{Time.now.to_iso}) )! else conditions[:suppressExport] = '("0" <#IN> suppressExport)' end #conditions[:notWidget] = '("Widget" <#IN/NOT> objClass)' conditions[:notWidget] = '(objClass <#ENDS/NOT> "Widget")' conditions end def find_obj_class_in_query(query) obj_class_q = query.find {|q| q[:operator] == :equal && q[:field] == :_obj_class && !q[:value].kind_of?(Array) } obj_class_q[:value] if obj_class_q end def real_field_name_in_obj_class(obj_class, field) type_def = Fiona7::TypeRegister.instance.read(obj_class) if type_def attribute = type_def.find_attribute(field) if attribute field = attribute.real_name end end field end end end end