Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 138 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
Results | |
0.00% |
0 / 138 |
|
0.00% |
0 / 10 |
1482 | |
0.00% |
0 / 1 |
performSearch | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
getFacetList | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
stripFilterParameters | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
formatFacetData | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
processSpelling | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getSpellingSuggestions | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getBestBets | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDatabaseRecommendations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTopicRecommendations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPartialFieldFacets | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
110 |
1 | <?php |
2 | |
3 | /** |
4 | * Summon Search Results |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2011, 2022. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Search_Summon |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org Main Page |
28 | */ |
29 | |
30 | namespace VuFind\Search\Summon; |
31 | |
32 | use VuFindSearch\Command\SearchCommand; |
33 | |
34 | use function in_array; |
35 | use function is_array; |
36 | |
37 | /** |
38 | * Summon Search Parameters |
39 | * |
40 | * @category VuFind |
41 | * @package Search_Summon |
42 | * @author Demian Katz <demian.katz@villanova.edu> |
43 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
44 | * @link https://vufind.org Main Page |
45 | */ |
46 | class Results extends \VuFind\Search\Base\Results |
47 | { |
48 | /** |
49 | * Facet details: |
50 | * |
51 | * @var array |
52 | */ |
53 | protected $responseFacets = null; |
54 | |
55 | /** |
56 | * Best bets |
57 | * |
58 | * @var array|bool |
59 | */ |
60 | protected $bestBets = false; |
61 | |
62 | /** |
63 | * Database recommendations |
64 | * |
65 | * @var array|bool |
66 | */ |
67 | protected $databaseRecommendations = false; |
68 | |
69 | /** |
70 | * Topic recommendations |
71 | * |
72 | * @var array|bool |
73 | */ |
74 | protected $topicRecommendations = false; |
75 | |
76 | /** |
77 | * Search backend identifier. |
78 | * |
79 | * @var string |
80 | */ |
81 | protected $backendId = 'Summon'; |
82 | |
83 | /** |
84 | * Support method for performAndProcessSearch -- perform a search based on the |
85 | * parameters passed to the object. |
86 | * |
87 | * @return void |
88 | */ |
89 | protected function performSearch() |
90 | { |
91 | $query = $this->getParams()->getQuery(); |
92 | $limit = $this->getParams()->getLimit(); |
93 | $offset = $this->getStartRecord() - 1; |
94 | $params = $this->getParams()->getBackendParameters(); |
95 | $command = new SearchCommand( |
96 | $this->backendId, |
97 | $query, |
98 | $offset, |
99 | $limit, |
100 | $params |
101 | ); |
102 | $collection = $this->getSearchService() |
103 | ->invoke($command)->getResult(); |
104 | $this->responseFacets = $collection->getFacets(); |
105 | $this->resultTotal = $collection->getTotal(); |
106 | |
107 | // Process spelling suggestions if enabled (note that we need this |
108 | // check here because sometimes the Summon API returns suggestions |
109 | // even when the spelling parameter is set to false). |
110 | if ($this->getOptions()->spellcheckEnabled()) { |
111 | $spellcheck = $collection->getSpellcheck(); |
112 | $this->processSpelling($spellcheck); |
113 | } |
114 | |
115 | // Get best bets and database recommendations. |
116 | $this->bestBets = $collection->getBestBets(); |
117 | $this->databaseRecommendations = $collection->getDatabaseRecommendations(); |
118 | $this->topicRecommendations = $collection->getTopicRecommendations(); |
119 | |
120 | // Add fake date facets if flagged earlier; this is necessary in order |
121 | // to display the date range facet control in the interface. |
122 | $dateFacets = $this->getParams()->getDateFacetSettings(); |
123 | if (!empty($dateFacets)) { |
124 | foreach ($dateFacets as $dateFacet) { |
125 | $this->responseFacets[] = [ |
126 | 'fieldName' => $dateFacet, |
127 | 'displayName' => $dateFacet, |
128 | 'counts' => [], |
129 | ]; |
130 | } |
131 | } |
132 | |
133 | // Construct record drivers for all the items in the response: |
134 | $this->results = $collection->getRecords(); |
135 | } |
136 | |
137 | /** |
138 | * Returns the stored list of facets for the last search |
139 | * |
140 | * @param array $filter Array of field => on-screen description listing |
141 | * all of the desired facet fields; set to null to get all configured values. |
142 | * |
143 | * @return array Facets data arrays |
144 | */ |
145 | public function getFacetList($filter = null) |
146 | { |
147 | // Make sure we have processed the search before proceeding: |
148 | if (null === $this->responseFacets) { |
149 | $this->performAndProcessSearch(); |
150 | } |
151 | |
152 | // If there is no filter, we'll use all facets as the filter: |
153 | $filter = null === $filter |
154 | ? $this->getParams()->getFacetConfig() |
155 | : $this->stripFilterParameters($filter); |
156 | |
157 | // We want to sort the facets to match the order in the .ini file. Let's |
158 | // create a lookup array to determine order: |
159 | $order = array_flip(array_keys($filter)); |
160 | |
161 | // Loop through the facets returned by Summon. |
162 | $facetResult = []; |
163 | if (is_array($this->responseFacets)) { |
164 | foreach ($this->responseFacets as $current) { |
165 | // The "displayName" value is actually the name of the field on |
166 | // Summon's side -- we'll probably need to translate this to a |
167 | // different value for actual display! |
168 | $field = $current['displayName']; |
169 | |
170 | // Is this one of the fields we want to display? If so, do work... |
171 | if (isset($filter[$field])) { |
172 | // Basic reformatting of the data: |
173 | $current = $this->formatFacetData($current); |
174 | |
175 | // Inject label from configuration: |
176 | $current['label'] = $filter[$field]; |
177 | |
178 | // Put the current facet cluster in order based on the .ini |
179 | // settings, then override the display name again using .ini |
180 | // settings. |
181 | $facetResult[$order[$field]] = $current; |
182 | } |
183 | } |
184 | } |
185 | ksort($facetResult); |
186 | |
187 | // Rewrite the sorted array with appropriate keys: |
188 | $finalResult = []; |
189 | foreach ($facetResult as $current) { |
190 | $finalResult[$current['displayName']] = $current; |
191 | } |
192 | |
193 | return $finalResult; |
194 | } |
195 | |
196 | /** |
197 | * Support method for getFacetList() -- strip extra parameters from field names. |
198 | * |
199 | * @param array $rawFilter Raw filter list |
200 | * |
201 | * @return array Processed filter list |
202 | */ |
203 | protected function stripFilterParameters($rawFilter) |
204 | { |
205 | $filter = []; |
206 | foreach ($rawFilter as $key => $value) { |
207 | $key = explode(',', $key); |
208 | $key = trim($key[0]); |
209 | $filter[$key] = $value; |
210 | } |
211 | return $filter; |
212 | } |
213 | |
214 | /** |
215 | * Support method for getFacetList() -- format a single facet field. |
216 | * |
217 | * @param array $current Facet data to format |
218 | * |
219 | * @return array Formatted data |
220 | */ |
221 | protected function formatFacetData($current) |
222 | { |
223 | // We'll need this in the loop below: |
224 | $filterList = $this->getParams()->getRawFilters(); |
225 | |
226 | // Should we translate values for the current facet? |
227 | $field = $current['displayName']; |
228 | $translate = in_array( |
229 | $field, |
230 | $this->getOptions()->getTranslatedFacets() |
231 | ); |
232 | if ($translate) { |
233 | $transTextDomain = $this->getOptions() |
234 | ->getTextDomainForTranslatedFacet($field); |
235 | } |
236 | |
237 | // Loop through all the facet values to see if any are applied. |
238 | foreach ($current['counts'] as $facetIndex => $facetDetails) { |
239 | // Is the current field negated? If so, we don't want to |
240 | // show it -- this is currently used only for the special |
241 | // "exclude newspapers" facet: |
242 | if ($facetDetails['isNegated']) { |
243 | unset($current['counts'][$facetIndex]); |
244 | continue; |
245 | } |
246 | |
247 | // We need to check two things to determine if the current |
248 | // value is an applied filter. First, is the current field |
249 | // present in the filter list? Second, is the current value |
250 | // an active filter for the current field? |
251 | $orField = '~' . $field; |
252 | $itemsToCheck = $filterList[$field] ?? []; |
253 | if (isset($filterList[$orField])) { |
254 | $itemsToCheck += $filterList[$orField]; |
255 | } |
256 | $isApplied = in_array($facetDetails['value'], $itemsToCheck); |
257 | |
258 | // Inject "applied" value into Summon results: |
259 | $current['counts'][$facetIndex]['isApplied'] = $isApplied; |
260 | |
261 | // Set operator: |
262 | $current['counts'][$facetIndex]['operator'] |
263 | = $this->getParams()->getFacetOperator($field); |
264 | |
265 | // Create display value: |
266 | $current['counts'][$facetIndex]['displayText'] = $translate |
267 | ? $this->translate("$transTextDomain::{$facetDetails['value']}") |
268 | : $facetDetails['value']; |
269 | } |
270 | |
271 | // Create a reference to counts called list for consistency with |
272 | // Solr output format -- this allows the facet recommendations |
273 | // modules to be shared between the Search and Summon modules. |
274 | $current['list'] = & $current['counts']; |
275 | |
276 | return $current; |
277 | } |
278 | |
279 | /** |
280 | * Process spelling suggestions from the results object |
281 | * |
282 | * @param array $spelling Suggestions from Summon |
283 | * |
284 | * @return void |
285 | */ |
286 | protected function processSpelling($spelling) |
287 | { |
288 | $this->suggestions = []; |
289 | foreach ($spelling as $current) { |
290 | $current = $current['suggestion']; |
291 | if (!isset($this->suggestions[$current['originalQuery']])) { |
292 | $this->suggestions[$current['originalQuery']] = [ |
293 | 'suggestions' => [], |
294 | ]; |
295 | } |
296 | $this->suggestions[$current['originalQuery']]['suggestions'][] |
297 | = $current['suggestedQuery']; |
298 | } |
299 | } |
300 | |
301 | /** |
302 | * Turn the list of spelling suggestions into an array of urls |
303 | * for on-screen use to implement the suggestions. |
304 | * |
305 | * @return array Spelling suggestion data arrays |
306 | */ |
307 | public function getSpellingSuggestions() |
308 | { |
309 | $retVal = []; |
310 | foreach ($this->getRawSuggestions() as $term => $details) { |
311 | foreach ($details['suggestions'] as $word) { |
312 | // Strip escaped characters in the search term (for example, "\:") |
313 | $term = stripcslashes($term); |
314 | $word = stripcslashes($word); |
315 | $retVal[$term]['suggestions'][$word] = ['new_term' => $word]; |
316 | } |
317 | } |
318 | return $retVal; |
319 | } |
320 | |
321 | /** |
322 | * Get best bets from Summon, if any. |
323 | * |
324 | * @return array|bool false if no recommendations, detailed array otherwise. |
325 | */ |
326 | public function getBestBets() |
327 | { |
328 | return $this->bestBets; |
329 | } |
330 | |
331 | /** |
332 | * Get database recommendations from Summon, if any. |
333 | * |
334 | * @return array|bool false if no recommendations, detailed array otherwise. |
335 | */ |
336 | public function getDatabaseRecommendations() |
337 | { |
338 | return $this->databaseRecommendations; |
339 | } |
340 | |
341 | /** |
342 | * Get topic recommendations from Summon, if any. |
343 | * |
344 | * @return array|bool false if no recommendations, detailed array otherwise. |
345 | */ |
346 | public function getTopicRecommendations() |
347 | { |
348 | return $this->topicRecommendations; |
349 | } |
350 | |
351 | /** |
352 | * Get complete facet counts for several index fields |
353 | * |
354 | * @param array $facetfields name of the Solr fields to return facets for |
355 | * @param bool $removeFilter Clear existing filters from selected fields (true) |
356 | * or retain them (false)? |
357 | * @param int $limit A limit for the number of facets returned, this |
358 | * may be useful for very large amounts of facets that can break the JSON parse |
359 | * method because of PHP out of memory exceptions (default = -1, no limit). |
360 | * @param string $facetSort A facet sort value to use (null to retain current) |
361 | * @param int $page 1 based. Offsets results by limit. |
362 | * |
363 | * @return array an array with the facet values for each index field |
364 | */ |
365 | public function getPartialFieldFacets( |
366 | $facetfields, |
367 | $removeFilter = true, |
368 | $limit = -1, |
369 | $facetSort = null, |
370 | $page = null |
371 | ) { |
372 | $params = $this->getParams(); |
373 | $query = $params->getQuery(); |
374 | // No limit not implemented with Summon: cause page loop |
375 | if ($limit == -1) { |
376 | if ($page === null) { |
377 | $page = 1; |
378 | } |
379 | $limit = 50; |
380 | } |
381 | $params->resetFacetConfig(); |
382 | if (null !== $facetSort && 'count' !== $facetSort) { |
383 | throw new \Exception("$facetSort facet sort not supported by Summon."); |
384 | } |
385 | foreach ($facetfields as $facet) { |
386 | $mode = $params->getFacetOperator($facet) === 'OR' ? 'or' : 'and'; |
387 | $params->addFacet("$facet,$mode,$page,$limit"); |
388 | |
389 | // Clear existing filters for the selected field if necessary: |
390 | if ($removeFilter) { |
391 | $params->removeAllFilters($facet); |
392 | } |
393 | } |
394 | $params = $params->getBackendParameters(); |
395 | $command = new SearchCommand( |
396 | $this->backendId, |
397 | $query, |
398 | 0, |
399 | 0, |
400 | $params |
401 | ); |
402 | $collection = $this->getSearchService()->invoke($command) |
403 | ->getResult(); |
404 | $facets = $collection->getFacets(); |
405 | $ret = []; |
406 | foreach ($facets as $data) { |
407 | if (in_array($data['displayName'], $facetfields)) { |
408 | $formatted = $this->formatFacetData($data); |
409 | $list = $formatted['counts']; |
410 | $ret[$data['displayName']] = [ |
411 | 'data' => [ |
412 | 'label' => $data['displayName'], |
413 | 'list' => $list, |
414 | ], |
415 | 'more' => null, |
416 | ]; |
417 | } |
418 | } |
419 | |
420 | // Send back data: |
421 | return $ret; |
422 | } |
423 | } |