Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 138
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Results
0.00% covered (danger)
0.00%
0 / 138
0.00% covered (danger)
0.00%
0 / 10
1482
0.00% covered (danger)
0.00%
0 / 1
 performSearch
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 getFacetList
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 stripFilterParameters
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 formatFacetData
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 processSpelling
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getSpellingSuggestions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getBestBets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDatabaseRecommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTopicRecommendations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPartialFieldFacets
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
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
30namespace VuFind\Search\Summon;
31
32use VuFindSearch\Command\SearchCommand;
33
34use function in_array;
35use 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 */
46class 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}