Skip to content

Commit 0772adf

Browse files
authored
Merge pull request #3597 from projectblacklight/facet-builder
Extract FacetSearchBuilder
2 parents 3225a08 + debe75a commit 0772adf

File tree

14 files changed

+1276
-378
lines changed

14 files changed

+1276
-378
lines changed

app/controllers/concerns/blacklight/catalog.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Blacklight::Catalog
99
include Blacklight::Configurable
1010
include Blacklight::SearchContext
1111
include Blacklight::Searchable
12+
include Blacklight::Facetable
1213

1314
# The following code is executed when someone includes blacklight::catalog in their
1415
# own controller.
@@ -84,11 +85,11 @@ def facet
8485
raise ActionController::RoutingError, 'Not Found' unless @facet
8586

8687
@response = if params[:query_fragment].present?
87-
search_service.facet_suggest_response(@facet.key, params[:query_fragment])
88+
facet_search_service.facet_suggest_response(@facet.key, params[:query_fragment])
8889
else
89-
search_service.facet_field_response(@facet.key)
90+
facet_search_service.facet_field_response(@facet.key)
9091
end
91-
# @display_facet is a Blacklight::Solr::Response::Facets::FacetField
92+
9293
@display_facet = @response.aggregations[@facet.field]
9394

9495
# @presenter is a Blacklight::FacetFieldPresenter
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
# The Facetable module can be included onto classes that need to initialize a FacetSearchService.
4+
# There are two dependencies you must provide on the including class. Typically these
5+
# would be provided by Blacklight::Controller
6+
# 1. search_state
7+
# 2. blacklight_config
8+
#
9+
# Additionally, the including class may override the facet_search_service_context method to provide
10+
# further context to the SearchService. For example you could override this to provide the
11+
# currently signed in user.
12+
module Blacklight::Facetable
13+
extend ActiveSupport::Concern
14+
15+
included do
16+
# Which class to use for the search service. You can subclass SearchService if you
17+
# want to override any of the methods (e.g. SearchService#fetch)
18+
class_attribute :facet_search_service_class
19+
self.facet_search_service_class = Blacklight::FacetSearchService
20+
end
21+
22+
# @return [Blacklight::FacetSearchService]
23+
def facet_search_service
24+
facet_search_service_class.new(config: blacklight_config, search_state: search_state, user_params: search_state.to_h, **facet_search_service_context)
25+
end
26+
27+
# Override this method on the class that includes Blacklight::Facetable to provide more context to the search service if necessary.
28+
# For example, if your search builder needs to be aware of the current user, override this method to return a hash including the current user.
29+
# Then the search builder could use some property about the current user to construct a constraint on the search.
30+
# @return [Hash] a hash of context information to pass through to the search service
31+
def facet_search_service_context
32+
search_service_context
33+
end
34+
end

app/models/facet_search_builder.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
class FacetSearchBuilder < Blacklight::FacetSearchBuilder
4+
include Blacklight::Solr::FacetSearchBuilderBehavior
5+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
# FacetSearchService returns a facet list from the repository. This is for drawing the "more facets" modal
4+
module Blacklight
5+
class FacetSearchService
6+
def initialize(config:, search_state:, search_builder_class: config.facet_search_builder_class, **context)
7+
@blacklight_config = config
8+
@search_state = search_state
9+
@user_params = @search_state.params
10+
@search_builder_class = search_builder_class
11+
@context = context
12+
end
13+
14+
# The blacklight_config + controller are accessed by the search_builder
15+
attr_reader :blacklight_config, :context
16+
17+
def search_builder
18+
search_builder_class.new(self)
19+
end
20+
21+
def search_state_class
22+
@search_state.class
23+
end
24+
25+
##
26+
# Get the solr response when retrieving only a single facet field
27+
# @return [Blacklight::Solr::Response] the solr response
28+
def facet_field_response(facet_field, extra_controller_params = {})
29+
query = search_builder.with(search_state).facet(facet_field)
30+
repository.search(params: query.merge(extra_controller_params))
31+
end
32+
33+
def facet_suggest_response(facet_field, facet_suggestion_query, extra_controller_params = {})
34+
query = search_builder.with(search_state).facet(facet_field).facet_suggestion_query(facet_suggestion_query)
35+
repository.search(params: query.merge(extra_controller_params))
36+
end
37+
38+
private
39+
40+
attr_reader :search_builder_class, :search_state
41+
42+
delegate :repository, to: :blacklight_config
43+
end
44+
end
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# frozen_string_literal: true
2+
3+
module Blacklight
4+
##
5+
# Blacklight's SearchBuilder converts blacklight request parameters into
6+
# query parameters appropriate for search index. It does so by evaluating a
7+
# chain of processing methods to populate a result hash (see {#to_hash}).
8+
class AbstractSearchBuilder
9+
class_attribute :default_processor_chain
10+
self.default_processor_chain = []
11+
12+
attr_reader :processor_chain, :search_state, :blacklight_params
13+
14+
# @overload initialize(scope)
15+
# @param [Object] scope scope the scope where the filter methods reside in.
16+
# @overload initialize(processor_chain, scope)
17+
# @param [List<Symbol>,TrueClass] processor_chain options a list of filter methods to run or true, to use the default methods
18+
# @param [Object] scope the scope where the filter methods reside in.
19+
def initialize(*options)
20+
case options.size
21+
when 1
22+
@processor_chain = default_processor_chain.dup
23+
@scope = options.first
24+
when 2
25+
@processor_chain, @scope = options
26+
else
27+
raise ArgumentError, "wrong number of arguments. (#{options.size} for 1..2)"
28+
end
29+
30+
@blacklight_params = {}
31+
search_state_class = @scope.try(:search_state_class) || Blacklight::SearchState
32+
@search_state = search_state_class.new(@blacklight_params, @scope&.blacklight_config, @scope)
33+
@additional_filters = {}
34+
@merged_params = {}
35+
@reverse_merged_params = {}
36+
end
37+
38+
delegate :blacklight_config, to: :scope
39+
40+
##
41+
# Set the parameters to pass through the processor chain
42+
def with(blacklight_params_or_search_state = {})
43+
params_will_change!
44+
@search_state = blacklight_params_or_search_state.is_a?(Blacklight::SearchState) ? blacklight_params_or_search_state : @search_state.reset(blacklight_params_or_search_state)
45+
@blacklight_params = @search_state.params.dup
46+
self
47+
end
48+
49+
# sets the facet that this query pertains to, for the purpose of facet pagination
50+
def facet=(value)
51+
params_will_change!
52+
@facet = value
53+
end
54+
55+
# @param [Object] value
56+
def facet(value = nil)
57+
if value
58+
self.facet = value
59+
return self
60+
end
61+
@facet
62+
end
63+
64+
##
65+
# Merge additional, repository-specific parameters
66+
def merge(extra_params, &)
67+
if extra_params
68+
params_will_change!
69+
@merged_params.merge!(extra_params.to_hash, &)
70+
end
71+
self
72+
end
73+
74+
##
75+
# "Reverse merge" additional, repository-specific parameters
76+
def reverse_merge(extra_params, &)
77+
if extra_params
78+
params_will_change!
79+
@reverse_merged_params.reverse_merge!(extra_params.to_hash, &)
80+
end
81+
self
82+
end
83+
84+
delegate :[], :key?, to: :to_hash
85+
86+
# a solr query method
87+
# @return [Blacklight::Solr::Response] the solr response object
88+
def to_hash
89+
return @params unless params_need_update?
90+
91+
@params = processed_parameters
92+
.reverse_merge(@reverse_merged_params)
93+
.merge(@merged_params)
94+
.tap { clear_changes }
95+
end
96+
97+
alias query to_hash
98+
alias to_h to_hash
99+
100+
delegate :search_field, to: :search_state
101+
102+
private
103+
104+
attr_reader :scope
105+
106+
def should_add_field_to_request? _field_name, field
107+
field.include_in_request || (field.include_in_request.nil? && blacklight_config.add_field_configuration_to_solr_request)
108+
end
109+
110+
def request
111+
Blacklight::Solr::Request.new
112+
end
113+
114+
# The CatalogController #index and #facet actions use this.
115+
# Solr parameters can come from a number of places. From lowest
116+
# precedence to highest:
117+
# 1. General defaults in blacklight config (are trumped by)
118+
# 2. defaults for the particular search field identified by params[:search_field] (are trumped by)
119+
# 3. certain parameters directly on input HTTP query params
120+
# * not just any parameter is grabbed willy nilly, only certain ones are allowed by HTTP input)
121+
# * for legacy reasons, qt in http query does not over-ride qt in search field definition default.
122+
# 4. extra parameters passed in as argument.
123+
#
124+
# spellcheck.q will be supplied with the [:q] value unless specifically
125+
# specified otherwise.
126+
#
127+
# Incoming parameter :f is mapped to :fq solr parameter.
128+
#
129+
# @return a params hash for searching solr.
130+
def processed_parameters
131+
request.tap do |request_parameters|
132+
processor_chain.each do |method_name|
133+
send(method_name, request_parameters)
134+
end
135+
end
136+
end
137+
138+
def params_will_change!
139+
@dirty = true
140+
end
141+
142+
def params_changed?
143+
!!@dirty
144+
end
145+
146+
def clear_changes
147+
@dirty = false
148+
end
149+
150+
def params_need_update?
151+
params_changed? || @params.nil?
152+
end
153+
end
154+
end

lib/blacklight/configuration.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def initialized_default_configuration?
9999
# @!attribute search_builder_class
100100
# @return [Class] class for converting Blacklight parameters to request parameters for the repository_class
101101
property :search_builder_class, default: ::SearchBuilder
102+
# @!attribute search_builder_class
103+
# @return [Class] class for converting Blacklight parameters to request parameters for the repository_class
104+
property :facet_search_builder_class, default: ::FacetSearchBuilder
102105
# @!attribute response_model
103106
# model that maps index responses to the blacklight response model
104107
# @return [Class]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Blacklight
4+
class FacetSearchBuilder < AbstractSearchBuilder
5+
def facet_suggestion_query=(value)
6+
params_will_change!
7+
@facet_suggestion_query = value
8+
end
9+
10+
def facet_suggestion_query(value = nil)
11+
if value
12+
self.facet_suggestion_query = value
13+
return self
14+
end
15+
@facet_suggestion_query
16+
end
17+
end
18+
end

0 commit comments

Comments
 (0)