From 0e697c4f4de242d9d44fd6a121bda255e63f7cf3 Mon Sep 17 00:00:00 2001 From: Justin Coyne Date: Thu, 19 Mar 2015 14:46:33 -0500 Subject: [PATCH] Generate SearchBuilder into the host application This gives plugins a place to add additional search methods without haveing an complex inheritance hierarchy. --- lib/blacklight.rb | 3 +- lib/blacklight/configuration.rb | 2 +- lib/blacklight/search_builder.rb | 138 +-------- lib/blacklight/search_builder_behavior.rb | 144 +++++++++ lib/blacklight/solr.rb | 2 +- lib/blacklight/solr/search_builder.rb | 269 +---------------- .../solr/search_builder_behavior.rb | 273 ++++++++++++++++++ .../blacklight/document_generator.rb | 4 + .../blacklight/templates/search_builder.rb | 3 + 9 files changed, 433 insertions(+), 405 deletions(-) create mode 100644 lib/blacklight/search_builder_behavior.rb create mode 100644 lib/blacklight/solr/search_builder_behavior.rb create mode 100644 lib/generators/blacklight/templates/search_builder.rb diff --git a/lib/blacklight.rb b/lib/blacklight.rb index 244ce779ca..9e74840841 100644 --- a/lib/blacklight.rb +++ b/lib/blacklight.rb @@ -8,7 +8,8 @@ module Blacklight autoload :Configuration, 'blacklight/configuration' autoload :SearchFields, 'blacklight/search_fields' autoload :SearchBuilder, 'blacklight/search_builder' - + autoload :SearchBuilderBehavior, 'blacklight/search_builder_behavior' + autoload :Document, 'blacklight/document' autoload :Solr, 'blacklight/solr' diff --git a/lib/blacklight/configuration.rb b/lib/blacklight/configuration.rb index 00e9ce2287..54c51788b2 100644 --- a/lib/blacklight/configuration.rb +++ b/lib/blacklight/configuration.rb @@ -212,7 +212,7 @@ def repository_class end def search_builder_class - super || Blacklight::Solr::SearchBuilder + super || ::SearchBuilder end def default_per_page diff --git a/lib/blacklight/search_builder.rb b/lib/blacklight/search_builder.rb index 6adedc401f..dfcaf10da2 100644 --- a/lib/blacklight/search_builder.rb +++ b/lib/blacklight/search_builder.rb @@ -1,140 +1,6 @@ module Blacklight class SearchBuilder - extend Deprecation - self.deprecation_horizon = "blacklight 6.0" - - class_attribute :default_processor_chain - self.default_processor_chain = [] - - attr_reader :processor_chain, :blacklight_params - - # @param [List,TrueClass] processor_chain a list of filter methods to run or true, to use the default methods - # @param [Object] scope the scope where the filter methods reside in. - def initialize(processor_chain, scope) - @processor_chain = if processor_chain === true - default_processor_chain.dup - else - processor_chain - end - - @scope = scope - @blacklight_params = {} - end - - ## - # Set the parameters to pass through the processor chain - def with blacklight_params = {} - @blacklight_params = blacklight_params.dup - self - end - - ## - # Update the :q (query) parameter - def where conditions - @blacklight_params[:q] = conditions - self - end - - ## - # Append additional processor chain directives - def append *addl_processor_chain - self.class.new(processor_chain + addl_processor_chain, scope).with(blacklight_params) - end - - # a solr query method - # @param [Hash,HashWithIndifferentAccess] extra_controller_params (nil) extra parameters to add to the search - # @return [Blacklight::SolrResponse] the solr response object - def query(extra_params = nil) - extra_params ? processed_parameters.merge(extra_params) : processed_parameters - end - - # @returns a params hash for searching solr. - # The CatalogController #index action uses this. - # Solr parameters can come from a number of places. From lowest - # precedence to highest: - # 1. General defaults in blacklight config (are trumped by) - # 2. defaults for the particular search field identified by params[:search_field] (are trumped by) - # 3. certain parameters directly on input HTTP query params - # * not just any parameter is grabbed willy nilly, only certain ones are allowed by HTTP input) - # * for legacy reasons, qt in http query does not over-ride qt in search field definition default. - # 4. extra parameters passed in as argument. - # - # spellcheck.q will be supplied with the [:q] value unless specifically - # specified otherwise. - # - # Incoming parameter :f is mapped to :fq solr parameter. - def processed_parameters - request.tap do |request_parameters| - processor_chain.each do |method_name| - if scope.respond_to?(method_name, true) - Deprecation.warn Blacklight::SearchBuilder, "Building search parameters by calling #{method_name} on #{scope.class}. This behavior will be deprecated in Blacklight 6.0. Instead, define #{method_name} on a subclass of #{self.class} and set search_builder_class in the configuration" - scope.send(method_name, request_parameters, blacklight_params) - else - send(method_name, request_parameters) - end - end - end - end - - def blacklight_config - scope.blacklight_config - end - - protected - def request - Blacklight::Solr::Request.new - end - - def page - if blacklight_params[:page].blank? - 1 - else - blacklight_params[:page].to_i - end - end - - def rows default = nil - # default number of rows - rows = default - rows ||= blacklight_config.default_per_page - rows ||= 10 - - # user-provided parameters should override any default row - rows = blacklight_params[:rows].to_i unless blacklight_params[:rows].blank? - rows = blacklight_params[:per_page].to_i unless blacklight_params[:per_page].blank? - - # ensure we don't excede the max page size - rows = blacklight_config.max_per_page if rows.to_i > blacklight_config.max_per_page - - - rows - end - - def sort - field = if blacklight_params[:sort].blank? and sort_field = blacklight_config.default_sort_field - # no sort param provided, use default - sort_field.sort - elsif sort_field = blacklight_config.sort_fields[blacklight_params[:sort]] - # check for sort field key - sort_field.sort - else - # just pass the key through - blacklight_params[:sort] - end - - field unless field.blank? - end - - def search_field - blacklight_config.search_fields[blacklight_params[:search_field]] - end - - def should_add_field_to_request? field_name, field - field.include_in_request || (field.include_in_request.nil? && blacklight_config.add_field_configuration_to_solr_request) - end - - def scope - @scope - end + # TODO deprecate this class + include SearchBuilderBehavior end end diff --git a/lib/blacklight/search_builder_behavior.rb b/lib/blacklight/search_builder_behavior.rb new file mode 100644 index 0000000000..7bd6c6b2c6 --- /dev/null +++ b/lib/blacklight/search_builder_behavior.rb @@ -0,0 +1,144 @@ +module Blacklight + module SearchBuilderBehavior + extend ActiveSupport::Concern + + included do + extend Deprecation + self.deprecation_horizon = "blacklight 6.0" + + class_attribute :default_processor_chain + self.default_processor_chain = [] + + attr_reader :processor_chain, :blacklight_params + end + + # @param [List,TrueClass] processor_chain a list of filter methods to run or true, to use the default methods + # @param [Object] scope the scope where the filter methods reside in. + def initialize(processor_chain, scope) + @processor_chain = if processor_chain === true + default_processor_chain.dup + else + processor_chain + end + + @scope = scope + @blacklight_params = {} + end + + ## + # Set the parameters to pass through the processor chain + def with blacklight_params = {} + @blacklight_params = blacklight_params.dup + self + end + + ## + # Update the :q (query) parameter + def where conditions + @blacklight_params[:q] = conditions + self + end + + ## + # Append additional processor chain directives + def append *addl_processor_chain + self.class.new(processor_chain + addl_processor_chain, scope).with(blacklight_params) + end + + # a solr query method + # @param [Hash,HashWithIndifferentAccess] extra_controller_params (nil) extra parameters to add to the search + # @return [Blacklight::SolrResponse] the solr response object + def query(extra_params = nil) + extra_params ? processed_parameters.merge(extra_params) : processed_parameters + end + + # @returns a params hash for searching solr. + # The CatalogController #index action uses this. + # Solr parameters can come from a number of places. From lowest + # precedence to highest: + # 1. General defaults in blacklight config (are trumped by) + # 2. defaults for the particular search field identified by params[:search_field] (are trumped by) + # 3. certain parameters directly on input HTTP query params + # * not just any parameter is grabbed willy nilly, only certain ones are allowed by HTTP input) + # * for legacy reasons, qt in http query does not over-ride qt in search field definition default. + # 4. extra parameters passed in as argument. + # + # spellcheck.q will be supplied with the [:q] value unless specifically + # specified otherwise. + # + # Incoming parameter :f is mapped to :fq solr parameter. + def processed_parameters + request.tap do |request_parameters| + processor_chain.each do |method_name| + if scope.respond_to?(method_name, true) + Deprecation.warn Blacklight::SearchBuilder, "Building search parameters by calling #{method_name} on #{scope.class}. This behavior will be deprecated in Blacklight 6.0. Instead, define #{method_name} on a subclass of #{self.class} and set search_builder_class in the configuration" + scope.send(method_name, request_parameters, blacklight_params) + else + send(method_name, request_parameters) + end + end + end + end + + def blacklight_config + scope.blacklight_config + end + + protected + def request + Blacklight::Solr::Request.new + end + + def page + if blacklight_params[:page].blank? + 1 + else + blacklight_params[:page].to_i + end + end + + def rows default = nil + # default number of rows + rows = default + rows ||= blacklight_config.default_per_page + rows ||= 10 + + # user-provided parameters should override any default row + rows = blacklight_params[:rows].to_i unless blacklight_params[:rows].blank? + rows = blacklight_params[:per_page].to_i unless blacklight_params[:per_page].blank? + + # ensure we don't excede the max page size + rows = blacklight_config.max_per_page if rows.to_i > blacklight_config.max_per_page + + + rows + end + + def sort + field = if blacklight_params[:sort].blank? and sort_field = blacklight_config.default_sort_field + # no sort param provided, use default + sort_field.sort + elsif sort_field = blacklight_config.sort_fields[blacklight_params[:sort]] + # check for sort field key + sort_field.sort + else + # just pass the key through + blacklight_params[:sort] + end + + field unless field.blank? + end + + def search_field + blacklight_config.search_fields[blacklight_params[:search_field]] + end + + def should_add_field_to_request? field_name, field + field.include_in_request || (field.include_in_request.nil? && blacklight_config.add_field_configuration_to_solr_request) + end + + def scope + @scope + end + end +end diff --git a/lib/blacklight/solr.rb b/lib/blacklight/solr.rb index a4896934ef..49bde082ef 100644 --- a/lib/blacklight/solr.rb +++ b/lib/blacklight/solr.rb @@ -6,5 +6,5 @@ module Blacklight::Solr autoload :Document, 'blacklight/solr/document' autoload :Request, 'blacklight/solr/request' autoload :SearchBuilder, 'blacklight/solr/search_builder' - + autoload :SearchBuilderBehavior, 'blacklight/solr/search_builder_behavior' end diff --git a/lib/blacklight/solr/search_builder.rb b/lib/blacklight/solr/search_builder.rb index 557a5035b4..72d88f12de 100644 --- a/lib/blacklight/solr/search_builder.rb +++ b/lib/blacklight/solr/search_builder.rb @@ -1,269 +1,6 @@ module Blacklight::Solr - class SearchBuilder < Blacklight::SearchBuilder - - self.default_processor_chain = [:default_solr_parameters, :add_query_to_solr, :add_facet_fq_to_solr, :add_facetting_to_solr, :add_solr_fields_to_query, :add_paging_to_solr, :add_sorting_to_solr, :add_group_config_to_solr ] - - #### - # Start with general defaults from BL config. Need to use custom - # merge to dup values, to avoid later mutating the original by mistake. - def default_solr_parameters(solr_parameters) - blacklight_config.default_solr_params.each do |key, value| - solr_parameters[key] = if value.respond_to? :deep_dup - value.deep_dup - elsif value.respond_to? :dup and value.duplicable? - value.dup - else - value - end - end - end - - ## - # Take the user-entered query, and put it in the solr params, - # including config's "search field" params for current search field. - # also include setting spellcheck.q. - def add_query_to_solr(solr_parameters) - ### - # Merge in search field configured values, if present, over-writing general - # defaults - ### - # legacy behavior of user param :qt is passed through, but over-ridden - # by actual search field config if present. We might want to remove - # this legacy behavior at some point. It does not seem to be currently - # rspec'd. - solr_parameters[:qt] = blacklight_params[:qt] if blacklight_params[:qt] - - if search_field - solr_parameters[:qt] = search_field.qt - solr_parameters.merge!( search_field.solr_parameters) if search_field.solr_parameters - end - - ## - # Create Solr 'q' including the user-entered q, prefixed by any - # solr LocalParams in config, using solr LocalParams syntax. - # http://wiki.apache.org/solr/LocalParams - ## - if (search_field && hash = search_field.solr_local_parameters) - local_params = hash.collect do |key, val| - key.to_s + "=" + solr_param_quote(val, :quote => "'") - end.join(" ") - solr_parameters[:q] = "{!#{local_params}}#{blacklight_params[:q]}" - - ## - # Set Solr spellcheck.q to be original user-entered query, without - # our local params, otherwise it'll try and spellcheck the local - # params! - solr_parameters["spellcheck.q"] ||= blacklight_params[:q] - elsif blacklight_params[:q].is_a? Hash - q = blacklight_params[:q] - solr_parameters[:q] = if q.values.any?(&:blank?) - # if any field parameters are empty, exclude _all_ results - "{!lucene}NOT *:*" - else - "{!lucene}" + q.map do |field, values| - "#{field}:(#{ Array(values).map { |x| solr_param_quote(x) }.join(" OR ")})" - end.join(" AND ") - end - - solr_parameters[:spellcheck] = 'false' - elsif blacklight_params[:q] - solr_parameters[:q] = blacklight_params[:q] - end - end - - ## - # Add any existing facet limits, stored in app-level HTTP query - # as :f, to solr as appropriate :fq query. - def add_facet_fq_to_solr(solr_parameters) - - # convert a String value into an Array - if solr_parameters[:fq].is_a? String - solr_parameters[:fq] = [solr_parameters[:fq]] - end - - # :fq, map from :f. - if ( blacklight_params[:f]) - f_request_params = blacklight_params[:f] - - f_request_params.each_pair do |facet_field, value_list| - Array(value_list).each do |value| - next if value.blank? # skip empty strings - solr_parameters.append_filter_query facet_value_to_fq_string(facet_field, value) - end - end - end - end - - ## - # Add appropriate Solr facetting directives in, including - # taking account of our facet paging/'more'. This is not - # about solr 'fq', this is about solr facet.* params. - def add_facetting_to_solr(solr_parameters) - # While not used by BL core behavior, legacy behavior seemed to be - # to accept incoming params as "facet.field" or "facets", and add them - # on to any existing facet.field sent to Solr. Legacy behavior seemed - # to be accepting these incoming params as arrays (in Rails URL with [] - # on end), or single values. At least one of these is used by - # Stanford for "faux hieararchial facets". - if blacklight_params.has_key?("facet.field") || blacklight_params.has_key?("facets") - solr_parameters[:"facet.field"].concat( [blacklight_params["facet.field"], blacklight_params["facets"]].flatten.compact ).uniq! - end - - blacklight_config.facet_fields.select { |field_name,facet| - facet.include_in_request || (facet.include_in_request.nil? && blacklight_config.add_facet_fields_to_solr_request) - }.each do |field_name, facet| - solr_parameters[:facet] ||= true - - case - when facet.pivot - solr_parameters.append_facet_pivot with_ex_local_param(facet.ex, facet.pivot.join(",")) - when facet.query - solr_parameters.append_facet_query facet.query.map { |k, x| with_ex_local_param(facet.ex, x[:fq]) } - else - solr_parameters.append_facet_fields with_ex_local_param(facet.ex, facet.field) - end - - if facet.sort - solr_parameters[:"f.#{facet.field}.facet.sort"] = facet.sort - end - - if facet.solr_params - facet.solr_params.each do |k, v| - solr_parameters[:"f.#{facet.field}.#{k}"] = v - end - end - - # Support facet paging and 'more' - # links, by sending a facet.limit one more than what we - # want to page at, according to configured facet limits. - solr_parameters[:"f.#{facet.field}.facet.limit"] = (facet_limit_for(field_name) + 1) if facet_limit_for(field_name) - end - end - - def add_solr_fields_to_query solr_parameters - blacklight_config.show_fields.select(&method(:should_add_field_to_request?)).each do |field_name, field| - if field.solr_params - field.solr_params.each do |k, v| - solr_parameters[:"f.#{field.field}.#{k}"] = v - end - end - end - - blacklight_config.index_fields.select(&method(:should_add_field_to_request?)).each do |field_name, field| - if field.highlight - solr_parameters[:hl] = true - solr_parameters.append_highlight_field field.field - end - - if field.solr_params - field.solr_params.each do |k, v| - solr_parameters[:"f.#{field.field}.#{k}"] = v - end - end - end - end - - ### - # copy paging params from BL app over to solr, changing - # app level per_page and page to Solr rows and start. - def add_paging_to_solr(solr_params) - # user-provided parameters should override any default row - solr_params[:rows] = rows(solr_params[:rows]) - if page > 1 - solr_params[:start] = solr_params[:rows].to_i * (page - 1) - end - end - - ### - # copy sorting params from BL app over to solr - def add_sorting_to_solr(solr_parameters) - solr_parameters[:sort] = sort unless sort.blank? - end - - # Remove the group parameter if we've faceted on the group field (e.g. for the full results for a group) - def add_group_config_to_solr solr_parameters - if blacklight_params[:f] and blacklight_params[:f][grouped_key_for_results] - solr_parameters[:group] = false - end - end - - def with_ex_local_param(ex, value) - if ex - "{!ex=#{ex}}#{value}" - else - value - end - end - - # Look up facet limit for given facet_field. Will look at config, and - # if config is 'true' will look up from Solr @response if available. If - # no limit is avaialble, returns nil. Used from #add_facetting_to_solr - # to supply f.fieldname.facet.limit values in solr request (no @response - # available), and used in display (with @response available) to create - # a facet paginator with the right limit. - def facet_limit_for(facet_field) - facet = blacklight_config.facet_fields[facet_field] - return if facet.blank? - - if facet.limit - facet.limit == true ? blacklight_config.default_facet_limit : facet.limit - end - end - - ## - # A helper method used for generating solr LocalParams, put quotes - # around the term unless it's a bare-word. Escape internal quotes - # if needed. - def solr_param_quote(val, options = {}) - options[:quote] ||= '"' - unless val =~ /^[a-zA-Z0-9$_\-\^]+$/ - val = options[:quote] + - # Yes, we need crazy escaping here, to deal with regexp esc too! - val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") + - options[:quote] - end - return val - end - - private - - ## - # Convert a facet/value pair into a solr fq parameter - def facet_value_to_fq_string(facet_field, value) - facet_config = blacklight_config.facet_fields[facet_field] - - solr_field = facet_config.field if facet_config and not facet_config.query - solr_field ||= facet_field - - local_params = [] - local_params << "tag=#{facet_config.tag}" if facet_config and facet_config.tag - - prefix = "" - prefix = "{!#{local_params.join(" ")}}" unless local_params.empty? - - fq = case - when (facet_config and facet_config.query) - facet_config.query[value][:fq] - when (facet_config and facet_config.date) - # in solr 3.2+, this could be replaced by a !term query - "#{prefix}#{solr_field}:#{RSolr.solr_escape(value)}" - when (value.is_a?(DateTime) or value.is_a?(Date) or value.is_a?(Time)) - "#{prefix}#{solr_field}:#{RSolr.solr_escape(value.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ"))}" - when (value.is_a?(TrueClass) or value.is_a?(FalseClass) or value == 'true' or value == 'false'), - (value.is_a?(Integer) or (value.to_i.to_s == value if value.respond_to? :to_i)), - (value.is_a?(Float) or (value.to_f.to_s == value if value.respond_to? :to_f)) - "#{prefix}#{solr_field}:#{RSolr.solr_escape(value.to_s)}" - when value.is_a?(Range) - "#{prefix}#{solr_field}:[#{value.first} TO #{value.last}]" - else - "{!raw f=#{solr_field}#{(" " + local_params.join(" ")) unless local_params.empty?}}#{value}" - end - end - - ## - # The key to use to retrieve the grouped field to display - def grouped_key_for_results - blacklight_config.index.group - end + class SearchBuilder + # TODO deprecate this class + include Blacklight::Solr::SearchBuilderBehavior end end diff --git a/lib/blacklight/solr/search_builder_behavior.rb b/lib/blacklight/solr/search_builder_behavior.rb new file mode 100644 index 0000000000..fd6790ae81 --- /dev/null +++ b/lib/blacklight/solr/search_builder_behavior.rb @@ -0,0 +1,273 @@ +module Blacklight::Solr + module SearchBuilderBehavior + extend ActiveSupport::Concern + include Blacklight::SearchBuilderBehavior + + included do + self.default_processor_chain = [:default_solr_parameters, :add_query_to_solr, :add_facet_fq_to_solr, :add_facetting_to_solr, :add_solr_fields_to_query, :add_paging_to_solr, :add_sorting_to_solr, :add_group_config_to_solr ] + end + + #### + # Start with general defaults from BL config. Need to use custom + # merge to dup values, to avoid later mutating the original by mistake. + def default_solr_parameters(solr_parameters) + blacklight_config.default_solr_params.each do |key, value| + solr_parameters[key] = if value.respond_to? :deep_dup + value.deep_dup + elsif value.respond_to? :dup and value.duplicable? + value.dup + else + value + end + end + end + + ## + # Take the user-entered query, and put it in the solr params, + # including config's "search field" params for current search field. + # also include setting spellcheck.q. + def add_query_to_solr(solr_parameters) + ### + # Merge in search field configured values, if present, over-writing general + # defaults + ### + # legacy behavior of user param :qt is passed through, but over-ridden + # by actual search field config if present. We might want to remove + # this legacy behavior at some point. It does not seem to be currently + # rspec'd. + solr_parameters[:qt] = blacklight_params[:qt] if blacklight_params[:qt] + + if search_field + solr_parameters[:qt] = search_field.qt + solr_parameters.merge!( search_field.solr_parameters) if search_field.solr_parameters + end + + ## + # Create Solr 'q' including the user-entered q, prefixed by any + # solr LocalParams in config, using solr LocalParams syntax. + # http://wiki.apache.org/solr/LocalParams + ## + if (search_field && hash = search_field.solr_local_parameters) + local_params = hash.collect do |key, val| + key.to_s + "=" + solr_param_quote(val, :quote => "'") + end.join(" ") + solr_parameters[:q] = "{!#{local_params}}#{blacklight_params[:q]}" + + ## + # Set Solr spellcheck.q to be original user-entered query, without + # our local params, otherwise it'll try and spellcheck the local + # params! + solr_parameters["spellcheck.q"] ||= blacklight_params[:q] + elsif blacklight_params[:q].is_a? Hash + q = blacklight_params[:q] + solr_parameters[:q] = if q.values.any?(&:blank?) + # if any field parameters are empty, exclude _all_ results + "{!lucene}NOT *:*" + else + "{!lucene}" + q.map do |field, values| + "#{field}:(#{ Array(values).map { |x| solr_param_quote(x) }.join(" OR ")})" + end.join(" AND ") + end + + solr_parameters[:spellcheck] = 'false' + elsif blacklight_params[:q] + solr_parameters[:q] = blacklight_params[:q] + end + end + + ## + # Add any existing facet limits, stored in app-level HTTP query + # as :f, to solr as appropriate :fq query. + def add_facet_fq_to_solr(solr_parameters) + + # convert a String value into an Array + if solr_parameters[:fq].is_a? String + solr_parameters[:fq] = [solr_parameters[:fq]] + end + + # :fq, map from :f. + if ( blacklight_params[:f]) + f_request_params = blacklight_params[:f] + + f_request_params.each_pair do |facet_field, value_list| + Array(value_list).each do |value| + next if value.blank? # skip empty strings + solr_parameters.append_filter_query facet_value_to_fq_string(facet_field, value) + end + end + end + end + + ## + # Add appropriate Solr facetting directives in, including + # taking account of our facet paging/'more'. This is not + # about solr 'fq', this is about solr facet.* params. + def add_facetting_to_solr(solr_parameters) + # While not used by BL core behavior, legacy behavior seemed to be + # to accept incoming params as "facet.field" or "facets", and add them + # on to any existing facet.field sent to Solr. Legacy behavior seemed + # to be accepting these incoming params as arrays (in Rails URL with [] + # on end), or single values. At least one of these is used by + # Stanford for "faux hieararchial facets". + if blacklight_params.has_key?("facet.field") || blacklight_params.has_key?("facets") + solr_parameters[:"facet.field"].concat( [blacklight_params["facet.field"], blacklight_params["facets"]].flatten.compact ).uniq! + end + + blacklight_config.facet_fields.select { |field_name,facet| + facet.include_in_request || (facet.include_in_request.nil? && blacklight_config.add_facet_fields_to_solr_request) + }.each do |field_name, facet| + solr_parameters[:facet] ||= true + + case + when facet.pivot + solr_parameters.append_facet_pivot with_ex_local_param(facet.ex, facet.pivot.join(",")) + when facet.query + solr_parameters.append_facet_query facet.query.map { |k, x| with_ex_local_param(facet.ex, x[:fq]) } + else + solr_parameters.append_facet_fields with_ex_local_param(facet.ex, facet.field) + end + + if facet.sort + solr_parameters[:"f.#{facet.field}.facet.sort"] = facet.sort + end + + if facet.solr_params + facet.solr_params.each do |k, v| + solr_parameters[:"f.#{facet.field}.#{k}"] = v + end + end + + # Support facet paging and 'more' + # links, by sending a facet.limit one more than what we + # want to page at, according to configured facet limits. + solr_parameters[:"f.#{facet.field}.facet.limit"] = (facet_limit_for(field_name) + 1) if facet_limit_for(field_name) + end + end + + def add_solr_fields_to_query solr_parameters + blacklight_config.show_fields.select(&method(:should_add_field_to_request?)).each do |field_name, field| + if field.solr_params + field.solr_params.each do |k, v| + solr_parameters[:"f.#{field.field}.#{k}"] = v + end + end + end + + blacklight_config.index_fields.select(&method(:should_add_field_to_request?)).each do |field_name, field| + if field.highlight + solr_parameters[:hl] = true + solr_parameters.append_highlight_field field.field + end + + if field.solr_params + field.solr_params.each do |k, v| + solr_parameters[:"f.#{field.field}.#{k}"] = v + end + end + end + end + + ### + # copy paging params from BL app over to solr, changing + # app level per_page and page to Solr rows and start. + def add_paging_to_solr(solr_params) + # user-provided parameters should override any default row + solr_params[:rows] = rows(solr_params[:rows]) + if page > 1 + solr_params[:start] = solr_params[:rows].to_i * (page - 1) + end + end + + ### + # copy sorting params from BL app over to solr + def add_sorting_to_solr(solr_parameters) + solr_parameters[:sort] = sort unless sort.blank? + end + + # Remove the group parameter if we've faceted on the group field (e.g. for the full results for a group) + def add_group_config_to_solr solr_parameters + if blacklight_params[:f] and blacklight_params[:f][grouped_key_for_results] + solr_parameters[:group] = false + end + end + + def with_ex_local_param(ex, value) + if ex + "{!ex=#{ex}}#{value}" + else + value + end + end + + # Look up facet limit for given facet_field. Will look at config, and + # if config is 'true' will look up from Solr @response if available. If + # no limit is avaialble, returns nil. Used from #add_facetting_to_solr + # to supply f.fieldname.facet.limit values in solr request (no @response + # available), and used in display (with @response available) to create + # a facet paginator with the right limit. + def facet_limit_for(facet_field) + facet = blacklight_config.facet_fields[facet_field] + return if facet.blank? + + if facet.limit + facet.limit == true ? blacklight_config.default_facet_limit : facet.limit + end + end + + ## + # A helper method used for generating solr LocalParams, put quotes + # around the term unless it's a bare-word. Escape internal quotes + # if needed. + def solr_param_quote(val, options = {}) + options[:quote] ||= '"' + unless val =~ /^[a-zA-Z0-9$_\-\^]+$/ + val = options[:quote] + + # Yes, we need crazy escaping here, to deal with regexp esc too! + val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") + + options[:quote] + end + return val + end + + private + + ## + # Convert a facet/value pair into a solr fq parameter + def facet_value_to_fq_string(facet_field, value) + facet_config = blacklight_config.facet_fields[facet_field] + + solr_field = facet_config.field if facet_config and not facet_config.query + solr_field ||= facet_field + + local_params = [] + local_params << "tag=#{facet_config.tag}" if facet_config and facet_config.tag + + prefix = "" + prefix = "{!#{local_params.join(" ")}}" unless local_params.empty? + + fq = case + when (facet_config and facet_config.query) + facet_config.query[value][:fq] + when (facet_config and facet_config.date) + # in solr 3.2+, this could be replaced by a !term query + "#{prefix}#{solr_field}:#{RSolr.solr_escape(value)}" + when (value.is_a?(DateTime) or value.is_a?(Date) or value.is_a?(Time)) + "#{prefix}#{solr_field}:#{RSolr.solr_escape(value.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ"))}" + when (value.is_a?(TrueClass) or value.is_a?(FalseClass) or value == 'true' or value == 'false'), + (value.is_a?(Integer) or (value.to_i.to_s == value if value.respond_to? :to_i)), + (value.is_a?(Float) or (value.to_f.to_s == value if value.respond_to? :to_f)) + "#{prefix}#{solr_field}:#{RSolr.solr_escape(value.to_s)}" + when value.is_a?(Range) + "#{prefix}#{solr_field}:[#{value.first} TO #{value.last}]" + else + "{!raw f=#{solr_field}#{(" " + local_params.join(" ")) unless local_params.empty?}}#{value}" + end + end + + ## + # The key to use to retrieve the grouped field to display + def grouped_key_for_results + blacklight_config.index.group + end + end +end diff --git a/lib/generators/blacklight/document_generator.rb b/lib/generators/blacklight/document_generator.rb index ff380d3e48..e9ce0b85d6 100644 --- a/lib/generators/blacklight/document_generator.rb +++ b/lib/generators/blacklight/document_generator.rb @@ -16,5 +16,9 @@ def create_solr_document template "solr_document.rb", "app/models/#{model_name}.rb" end + def create_search_builder + template "search_builder.rb", "app/models/search_builder.rb" + end + end end diff --git a/lib/generators/blacklight/templates/search_builder.rb b/lib/generators/blacklight/templates/search_builder.rb new file mode 100644 index 0000000000..0b52e84f68 --- /dev/null +++ b/lib/generators/blacklight/templates/search_builder.rb @@ -0,0 +1,3 @@ +class SearchBuilder + include Blacklight::Solr::SearchBuilderBehavior +end