require "fiona7/verity_search_engine" module Fiona7 class FacetBuilder def initialize(facet_params, query, klass) @query = query @klass = klass @attribute = facet_params[:attribute] @limit = [facet_params[:limit] || 10, 100].min # default = 10, limit <= 100 @fake_limit = 100 @include_objs = facet_params[:include_objs] || false @known_values = Set.new @facets = [] @next_facets = [] end def build @facets << fetch_more_facets until limit_reached or !more_facets_available # adjust totals for index in 0.upto(@facets.length-2) next_facet_total = @facets[index+1].first[:total] rescue 0 @facets[index].each do |facet| facet[:total] -= next_facet_total end end # flatten @facets.flatten! # sort @facets.sort_by! {|f| f[:total] }.reverse! # delete superfluous elements @facets.slice!(@limit, @facets.length) # add included objs if @include_objs @facets.each do |facet| include_objs_query = @query.dup include_objs_query << {field: @attribute, operator: :__in__, value: facet[:value]} search = VeritySearchEngine.new(@klass, include_objs_query, 0, @include_objs, @attribute, :desc) facet[:results] = search.results.map {|o_id| {id: o_id.to_s}} # this gives a true total! facet[:total] = search.total end else @facets.each do |facet| facet[:results] = [] end end @facets end protected def limit_reached (@fake_limit -= 1) < 0 end def fetch_more_facets @next_facets end def more_facets_available @next_facets = [] search = build_search return false if search.results.empty? sample_object = Fiona7::WriteObj.where(obj_id: search.results).first return false if sample_object.nil? new_values = get_attribute_values(sample_object) new_values.each do |new_value| next if @known_values.include?(new_value) @known_values << new_value @next_facets << { value: new_value, total: search.total } end return !@next_facets.empty? end def build_search facet_query = @query.dup facet_query << {field: @attribute, operator: :__not_in__, value: serialize_known_values} unless @known_values.empty? facet_query << any_value_present_in_attribute_query # search engine likes to return object which do not exist anymore # hence we try ten objects facet_size = 10 facet_offset = 0 facet_sort = @attribute facet_order = :desc VeritySearchEngine.new(@klass, facet_query, facet_offset, facet_size, facet_sort, facet_order) end # TODO: remove this ugly code def get_attribute_values(sample_object) # TODO: extend this code to handle stringlists and multienums type_definition = Fiona7::TypeRegister.instance.read_mangled(sample_object.obj_class) attribute = type_definition.find_attribute(@attribute) return nil unless attribute if attribute.type == :stringlist # TODO: remove deserialization duplication deserialized = ::JSON.parse(sample_object[attribute.real_name]) rescue [] deserialized.kind_of?(Array) ? deserialized : [] elsif attribute.real_type == :multienum sample_object[attribute.real_name] || [] else # TODO: add warning/handling for wrong attributes [sample_object[attribute.real_name].to_s] end end def any_value_present_in_attribute_query letters = ('a'..'z').map {|l| "*#{l}*" } numbers = ('0'..'9').map {|l| "*#{l}*" } sanity_patterns = letters + numbers {field: @attribute, operator: :__in__, value: sanity_patterns} end def serialize_known_values @known_values.map do |kv| kv.blank? ? "dummy123dummy456dummy" : kv.to_s end end end end