# frozen_string_literal: true require 'ostruct' module Blacklight::Solr::Response::Facets # represents a facet value; which is a field value and its hit count class FacetItem < OpenStruct def initialize *args options = args.extract_options! # Backwards-compat method signature value = args.shift hits = args.shift options[:value] = value if value options[:hits] = hits if hits super(options) end def label super || value end def as_json(props = nil) table.as_json(props) end end # represents a facet; which is a field and its values class FacetField attr_reader :name, :items attr_accessor :missing def initialize name, items, options = {} @name = name @items = items @options = options end def limit @options[:limit] || solr_default_limit end def sort @options[:sort] || solr_default_sort end def offset @options[:offset] || solr_default_offset end def prefix @options[:prefix] || solr_default_prefix end def type @options[:type] || 'terms' end def data @options[:data] || {} end def response @options[:response] end def index? sort == 'index' end def count? sort == 'count' end private # Per https://wiki.apache.org/solr/SimpleFacetParameters#facet.limit def solr_default_limit 100 end # Per https://wiki.apache.org/solr/SimpleFacetParameters#facet.sort def solr_default_sort if limit > 0 'count' else 'index' end end # Per https://wiki.apache.org/solr/SimpleFacetParameters#facet.offset def solr_default_offset 0 end def solr_default_prefix nil end end class NullFacetField < FacetField def initialize name, items = [], response: nil, **kwargs super end end ## # Get all the Solr facet data (fields, queries, pivots) as a hash keyed by # both the Solr field name and/or by the blacklight field name def aggregations @aggregations ||= default_aggregations.merge(facet_field_aggregations).merge(facet_query_aggregations).merge(facet_pivot_aggregations).merge(json_facet_aggregations) end def facet_counts @facet_counts ||= self['facet_counts'] || {} end # Returns the hash of all the facet_fields (ie: { 'instock_b' => ['true', 123, 'false', 20] } def facet_fields @facet_fields ||= begin val = facet_counts['facet_fields'] || {} # this is some old solr (1.4? earlier?) serialization of facet fields if val.is_a? Array val.to_h else val end end end # Returns all of the facet queries def facet_queries @facet_queries ||= facet_counts['facet_queries'] || {} end # Returns all of the facet queries def facet_pivot @facet_pivot ||= facet_counts['facet_pivot'] || {} end # Merge or add new facet count values to existing response def merge_facet(name:, value:, hits: nil) if dig('facet_counts', 'facet_fields', name) self['facet_counts']['facet_fields'][name] << value << hits else self['facet_counts']['facet_fields'][name] = [value, hits] end end private # @return [Hash] establish a null object pattern for facet data look-up, allowing # the response and applied parameters to get passed through even if there was no # facet data in the response def default_aggregations @default_aggregations ||= begin h = Hash.new { |key| null_facet_field_object(key) } h.with_indifferent_access end end # @return [Blacklight::Solr::Response::FacetField] a "null object" facet field def null_facet_field_object(key) Blacklight::Solr::Response::FacetField.new(key, [], facet_field_aggregation_options(key).merge(response: self)) end ## # Convert Solr responses of various json.nl flavors to def list_as_hash solr_list # map if solr_list.values.first.is_a? Hash solr_list else solr_list.transform_values do |values| if values.first.is_a? Array # arrarr values.to_h else # flat values.each_slice(2).to_a.to_h end end end end ## # Convert Solr's facet_field response into # a hash of Blacklight::Solr::Response::Facet::FacetField objects def facet_field_aggregations list_as_hash(facet_fields).each_with_object({}) do |(facet_field_name, values), hash| items = values.map do |value, hits| i = FacetItem.new(value: value, hits: hits) # legacy solr facet.missing serialization if value.nil? i.label = I18n.t(:"blacklight.search.fields.facet.missing.#{facet_field_name}", default: [:'blacklight.search.facets.missing']) i.fq = "-#{facet_field_name}:[* TO *]" # this explicit fq is deprecated; the missing attribute below is a better thing to check for this case i.value = Blacklight::SearchState::FilterField::MISSING i.missing = true end i end options = facet_field_aggregation_options(facet_field_name) facet_field = FacetField.new(facet_field_name, items, options.merge(response: self)) if values[nil] facet_field.missing = items.find(&:missing) end hash[facet_field_name] = facet_field # alias all the possible blacklight config names.. blacklight_config.facet_fields.select { |_k, v| v.field == facet_field_name }.each_key do |key| hash[key] = hash[facet_field_name] end if blacklight_config && !blacklight_config.facet_fields[facet_field_name] end end ## # Aggregate Solr's facet_query response into the virtual facet fields defined # in the blacklight configuration def facet_query_aggregations return {} unless blacklight_config blacklight_config.facet_fields.select { |_k, v| v.query }.each_with_object({}) do |(field_name, facet_field), hash| salient_facet_queries = facet_field.query.map { |_k, x| x[:fq] } items = facet_queries.select { |k, _v| salient_facet_queries.include?(k) }.reject { |_value, hits| hits.zero? }.map do |value, hits| salient_fields = facet_field.query.select { |_key, val| val[:fq] == value } key = ((salient_fields.keys if salient_fields.respond_to? :keys) || salient_fields.first).first Blacklight::Solr::Response::Facets::FacetItem.new(value: key, hits: hits, label: facet_field.query[key][:label]) end items += facet_query_aggregations_from_json(facet_field) items = items.sort_by(&:hits).reverse if facet_field.sort && facet_field.sort.to_sym == :count facet_field = Blacklight::Solr::Response::Facets::FacetField.new field_name, items, response: response hash[field_name] = facet_field end end def facet_query_aggregations_from_json(facet_field) return [] unless self['facets'] salient_facet_queries = facet_field.query.map { |_k, x| x[:fq] } relevant_facet_data = self['facets'].select { |k, _v| salient_facet_queries.include?(k) }.reject { |_key, data| data['count'].zero? } relevant_facet_data.map do |key, data| salient_fields = facet_field.query.select { |_key, val| val[:fq] == key } facet_key = ((salient_fields.keys if salient_fields.respond_to? :keys) || salient_fields.first).first Blacklight::Solr::Response::Facets::FacetItem.new(value: facet_key, hits: data[:count], label: facet_field.query[facet_key][:label]) end end ## # Convert Solr's facet_pivot response into # a hash of Blacklight::Solr::Response::Facet::FacetField objects def facet_pivot_aggregations facet_pivot.each_with_object({}) do |(field_name, values), hash| next unless blacklight_config && !blacklight_config.facet_fields[field_name] items = values.map do |lst| construct_pivot_field(lst) end # alias all the possible blacklight config names.. blacklight_config.facet_fields.select { |_k, v| v.pivot && v.pivot.join(",") == field_name }.each_key do |key| facet_field = Blacklight::Solr::Response::Facets::FacetField.new key, items, response: self hash[key] = facet_field end end end ## # Recursively parse the pivot facet response to build up the full pivot tree def construct_pivot_field lst, parent_fq = {} items = Array(lst[:pivot]).map do |i| construct_pivot_field(i, parent_fq.merge({ lst[:field] => lst[:value] })) end Blacklight::Solr::Response::Facets::FacetItem.new(value: lst[:value], hits: lst[:count], field: lst[:field], items: items, fq: parent_fq) end def construct_json_nested_facet_fields(bucket, parent_fq = {}) bucket.select { |_, nested| nested.is_a?(Hash) && nested.key?('buckets') }.map do |facet_field_name, nested| nested['buckets'].map do |subbucket| i = Blacklight::Solr::Response::Facets::FacetItem.new(field: facet_field_name, value: subbucket['val'], hits: subbucket['count'], fq: parent_fq, data: subbucket) i.items = construct_json_nested_facet_fields(subbucket, parent_fq.merge(key => subbucket['val'])) if has_json_nested_facets?(subbucket) i end end.flatten end def has_json_nested_facets?(bucket) bucket.any? { |_, nested| nested.is_a?(Hash) && nested.key?('buckets') } end def json_facet_aggregations return {} unless self['facets'] self['facets'].each_with_object({}) do |(facet_field_name, data), hash| next if facet_field_name == 'count' items = (data['buckets'] || []).map do |bucket| i = Blacklight::Solr::Response::Facets::FacetItem.new(value: bucket['val'], hits: bucket['count'], data: bucket) i.items = construct_json_nested_facet_fields(bucket, facet_field_name => bucket['val']) if has_json_nested_facets?(bucket) i end options = facet_field_aggregation_options(facet_field_name).merge(data: data, response: self) facet_field = FacetField.new(facet_field_name, items, options) facet_field.missing = Blacklight::Solr::Response::Facets::FacetItem.new( hits: data.dig('missing', 'count'), data: data['missing'] ) if data['missing'] hash[facet_field_name] = facet_field end end end