require "fiona7/search_engine" require "rails_connector/verity_search_request" require "fiona7/custom_verity_accessor" module Fiona7 class VeritySearchEngine < SearchEngine def results # 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 construct_search_request 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 @search_request = VeritySearchRequest.new(@query, options, @klass == Fiona7::ReleasedObj) end def fetch_results @results = @search_request.fetch_hits @count = @results.total_hits 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_string = build_query_string(query) @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) if full_text_query?(query) full_text_query_string(extract_words(query)) else complex_query_string(query) end end def full_text_query?(query) query.any? {|q| q[:field] == :'*' && (q[:operator] == :prefix_search || q[:operator] == :search)} end def complex_query_string(query) conditions = query.map do |q| if q[:field] == :'*' Rails.logger.error("Unexpected field * when processing search request: #{q.inspect}") next end resolved_field = resolve_field_name(q[:field]) # paths are sadly not in the search index. next if resolved_field == :visiblePath case q[:operator] when :equal if q[:field] == :_modification '("edited" <#IN> state)' else field_operator_value(resolved_field, "=", q[:value]) end when :search field_operator_value(resolved_field, "<#CONTAINS>", q[:value]) when :greater_than field_operator_value(resolved_field, ">", q[:value]) when :less_than field_operator_value(resolved_field, "<", q[:value]) when :prefix, :prefix_search values = q[:value] values = [values] unless values.kind_of?(Array) values.map {|v| "#{v}*" } values_operator_field(values, "<#IN>", resolved_field) else raise "Operator: #{q[:operator]} not supported" end end.compact if conditions.empty? query_string = "" else query_string = "<#AND> (" + conditions.join(", ") + ")" end end def field_operator_value(field, operator, values) values = [values] unless values.kind_of?(Array) condition = values.map do |value| %|(#{field} #{operator} "#{self.class.sanitize(value)}")| end.join(", ") %|(<#OR> #{condition})| end def values_operator_field(values, operator, field) values = [values] unless values.kind_of?(Array) condition = values.map do |value| %|("#{self.class.sanitize(value)}" #{operator} #{field})| end.join(", ") %|(<#OR> #{condition})| end def resolve_field_name(field) if Fiona7.mode != :legacy Rails.logger.error("This method is not compatible with modes other than legacy") end 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 extract_words(query) 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 end end end