-
Notifications
You must be signed in to change notification settings - Fork 253
/
solr_helper.rb
317 lines (266 loc) · 12 KB
/
solr_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# SolrHelper is a controller layer mixin. It is in the controller scope: request params, session etc.
#
# NOTE: Be careful when creating variables here as they may be overriding something that already exists.
# The ActionController docs: http://api.rubyonrails.org/classes/ActionController/Base.html
#
# Override these methods in your own controller for customizations:
#
# class CatalogController < ActionController::Base
#
# include Blacklight::SolrHelper
#
# def solr_search_params
# super.merge :per_page=>10
# end
#
# end
#
module Blacklight::SolrHelper
MaxPerPage = 100
# When a request for a single solr document by id
# is not successful, raise this:
class InvalidSolrID < RuntimeError; 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-Z$_\-\^]+$/
val = options[:quote] +
# Yes, we need crazy escaping here, to deal with regexp esc too!
val.gsub("'", "\\\\\'").gsub('"', "\\\\\"") +
options[:quote]
end
return val
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 solr_search_params(extra_controller_params={})
solr_parameters = {}
# Order of precedence for all the places solr params can come from,
# start lowest, and keep over-riding with higher.
####
# Start with general defaults from BL config.
solr_parameters.deep_merge!(Blacklight.config[:default_solr_params]) if Blacklight.config[:default_solr_params]
###
# Merge in search field configured values, if present, over-writing general
# defaults
###
search_field_def = Blacklight.search_field_def_for_key(params[:search_field] || extra_controller_params[:search_field])
solr_parameters[:qt] = search_field_def[:qt] if search_field_def
if ( search_field_def && search_field_def[:solr_parameters])
solr_parameters.merge!( search_field_def[:solr_parameters])
end
###
# Merge in certain values from HTTP query itelf
###
# Omit empty strings and nil values.
[:facets, :f, :page, :sort, :per_page].each do |key|
solr_parameters[key] = params[key] unless params[key].blank?
end
# :q is meaningful as an empty string, should be used unless nil!
[:q].each do |key|
solr_parameters[key] = params[key] if params[key]
end
# qt is handled different for legacy reasons; qt in HTTP param can not
# over-ride qt from search_field_def defaults, it's only used if there
# was no qt from search_field_def_defaults
unless params[:qt].blank? || ( search_field_def && search_field_def[:qt])
solr_parameters[:qt] = params[:qt]
end
# add any facet fields params["facet.field"] that aren't already included
# for example, if a selected facet value means a *new* facet is desired
# (Stanford is doing faux "hierarchical" facets this way; the
# hierarchical facet code for SOLR isn't fully baked yet and won't be
# included until Solr 1.5)
# TODO: jrochkind asks why we need to copy "facet.field" to RSolr-specific
# :facets, can't we just use "facet.field" alone and ignore the duplicate
# confusing one?
if params.has_key?("facet.field")
solr_parameters[:facets] ||= []
params["facet.field"].each do |ff|
if !solr_parameters[:facets].include?(ff)
solr_parameters[:facets] << ff
end
end
end
###
# Merge in any values from extra_params argument. It doesn't seem like
# we should have to take a slice of just certain keys, but legacy code
# seems to put arguments in here that aren't really expected to turn
# into solr params.
###
solr_parameters.deep_merge!(extra_controller_params.slice(:qt, :q, :facets, :page, :per_page, :phrase_filters, :f, :fq, :fl, :sort, :qf, :df ) )
###
# Defaults for otherwise blank values and normalization.
###
# TODO: Change calling code to expect this as a symbol instead of
# a string, for consistency? :'spellcheck.q' is a symbol. Right now
# callers assume a string.
solr_parameters["spellcheck.q"] = solr_parameters[:q] unless solr_parameters["spellcheck.q"]
# And fix the 'facets' parameter to be the way the solr expects it.
solr_parameters[:facets]= {:fields => solr_parameters[:facets]} if solr_parameters[:facets]
# :fq, map from :f.
if ( solr_parameters[:f])
f_request_params = solr_parameters.delete(:f)
solr_parameters[:fq] ||= []
f_request_params.each_pair do |facet_field, value_list|
value_list.each do |value|
solr_parameters[:fq] << "{!raw f=#{facet_field}}#{value}"
end
end
end
# Facet 'more' limits. Add +1 to any configured facets limits,
# also include 'nil' default limit.
if ( default_limit = facet_limit_for(nil))
solr_parameters[:"facet.limit"] = (default_limit + 1)
end
facet_limit_hash.each_pair do |field_name, limit|
next if field_name.nil? # skip the 'default' key
solr_parameters[:"f.#{field_name}.facet.limit"] = (limit + 1)
end
##
# Merge in search-field-specified LocalParams into q param in
# solr LocalParams syntax
##
if (search_field_def && hash = search_field_def[: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}} #{solr_parameters[:q]}"
end
###
# Sanity/requirements checks.
###
# limit to MaxPerPage (100). Tests want this to be a string not an integer,
# not sure why.
solr_parameters[:per_page] = solr_parameters[:per_page].to_i > MaxPerPage ? MaxPerPage.to_s : solr_parameters[:per_page]
return solr_parameters
end
# a solr query method
# given a user query, return a solr response containing both result docs and facets
# - mixes in the Blacklight::Solr::SpellingSuggestions module
# - the response will have a spelling_suggestions method
# Returns a two-element array (aka duple) with first the solr response object,
# and second an array of SolrDocuments representing the response.docs
def get_search_results(extra_controller_params={})
solr_response = Blacklight.solr.find( self.solr_search_params(extra_controller_params) )
document_list = solr_response.docs.collect {|doc| SolrDocument.new(doc)}
return [solr_response, document_list]
end
# returns a params hash for finding a single solr document (CatalogController #show action)
# If the id arg is nil, then the value is fetched from params[:id]
# This method is primary called by the get_solr_response_for_doc_id method.
def solr_doc_params(id=nil, extra_controller_params={})
id ||= params[:id]
# just to be consistent with the other solr param methods:
input = params.deep_merge(extra_controller_params)
{
:qt => :document,
:id => id
}
end
# a solr query method
# retrieve a solr document, given the doc id
# TODO: shouldn't hardcode id field; should be setable to unique_key field in schema.xml
def get_solr_response_for_doc_id(id=nil, extra_controller_params={})
solr_response = Blacklight.solr.find solr_doc_params(id, extra_controller_params)
raise InvalidSolrID.new if solr_response.docs.empty?
document = SolrDocument.new(solr_response.docs.first)
[solr_response, document]
end
# returns a params hash for a single facet field solr query.
# used primary by the get_facet_pagination method.
# Looks up Facet Paginator request params from current request
# params to figure out sort and offset.
# Default limit for facet list can be specified by defining a controller
# method facet_list_limit, otherwise 20.
def solr_facet_params(facet_field, extra_controller_params={})
input = params.deep_merge(extra_controller_params)
# First start with a standard solr search params calculations,
# for any search context in our request params.
solr_params = solr_search_params(extra_controller_params)
# Now override with our specific things for fetching facet values
solr_params[:facets] = {:fields => facet_field}
# Need to set as f.facet_field.facet.limit to make sure we
# override any field-specific default in the solr request handler.
solr_params[:"f.#{facet_field}.facet.limit"] =
if solr_params["facet.limit"]
solr_params["facet.limit"] + 1
elsif respond_to?(:facet_list_limit)
facet_list_limit.to_s.to_i + 1
else
20 + 1
end
solr_params['facet.offset'] = input[ Blacklight::Solr::FacetPaginator.request_keys[:offset] ].to_i # will default to 0 if nil
solr_params['facet.sort'] = input[ Blacklight::Solr::FacetPaginator.request_keys[:sort] ]
solr_params[:rows] = 0
return solr_params
end
# a solr query method
# used to paginate through a single facet field's values
# /catalog/facet/language_facet
def get_facet_pagination(facet_field, extra_controller_params={})
solr_params = solr_facet_params(facet_field, extra_controller_params)
# Make the solr call
response = Blacklight.solr.find(solr_params)
limit =
if respond_to?(:facet_list_limit)
facet_list_limit.to_s.to_i
elsif solr_params[:"f.#{facet_field}.facet.limit"]
solr_params[:"f.#{facet_field}.facet.limit"] - 1
else
nil
end
# Actually create the paginator!
return Blacklight::Solr::FacetPaginator.new(response.facets.first.items,
:offset => solr_params['facet.offset'],
:limit => limit,
:sort => response["responseHeader"]["params"]["facet.sort"]
)
end
# a solr query method
# this is used when selecting a search result: we have a query and a
# position in the search results and possibly some facets
def get_single_doc_via_search(extra_controller_params={})
solr_params = solr_search_params(extra_controller_params)
solr_params[:per_page] = 1
solr_params[:fl] = '*'
Blacklight.solr.find(solr_params).docs.first
end
# returns a solr params hash
# if field is nil, the value is fetched from Blacklight.config[:index][:show_link]
# the :fl (solr param) is set to the "field" value.
# per_page is set to 10
def solr_opensearch_params(field, extra_controller_params={})
solr_params = solr_search_params(extra_controller_params)
solr_params[:per_page] = 10
solr_params[:fl] = Blacklight.config[:index][:show_link]
solr_params
end
# a solr query method
# does a standard search but returns a simplified object.
# an array is returned, the first item is the query string,
# the second item is an other array. This second array contains
# all of the field values for each of the documents...
# where the field is the "field" argument passed in.
def get_opensearch_response(field=nil, extra_controller_params={})
solr_params = solr_opensearch_params(extra_controller_params)
response = Blacklight.solr.find(solr_params)
a = [solr_params[:q]]
a << response.docs.map {|doc| doc[solr_params[:fl]].to_s }
end
end