Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.83% covered (warning)
89.83%
53 / 59
92.86% covered (success)
92.86%
13 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SwitchQuery
89.83% covered (warning)
89.83%
53 / 59
92.86% covered (success)
92.86%
13 / 14
37.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConfig
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 init
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 queryShouldBeSkipped
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkFuzzy
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 checkLowercaseBools
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 checkUnwantedBools
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 checkUnwantedQuotes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 checkWildcard
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 checkTruncatechar
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getLuceneHelper
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getResults
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSuggestions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * SwitchQuery Recommendations Module
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
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  Recommendations
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @author   Chris Hallberg <challber@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development:plugins:recommendation_modules Wiki
29 */
30
31namespace VuFind\Recommend;
32
33use VuFindSearch\Command\GetLuceneHelperCommand;
34use VuFindSearch\Service;
35
36use function in_array;
37use function strlen;
38
39/**
40 * SwitchQuery Recommendations Module
41 *
42 * This class recommends adjusting your search query to yield better results.
43 *
44 * @category VuFind
45 * @package  Recommendations
46 * @author   Demian Katz <demian.katz@villanova.edu>
47 * @author   Chris Hallberg <challber@villanova.edu>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
49 * @link     https://vufind.org/wiki/development:plugins:recommendation_modules Wiki
50 */
51class SwitchQuery implements RecommendInterface
52{
53    /**
54     * Search backend identifier that we are working with.
55     *
56     * @var string
57     */
58    protected $backend;
59
60    /**
61     * Search service.
62     *
63     * @var Service
64     */
65    protected $searchService;
66
67    /**
68     * Improved query suggestions.
69     *
70     * @var array
71     */
72    protected $suggestions = [];
73
74    /**
75     * Names of checks that should be skipped. These should correspond
76     * with check method names -- e.g. to skip the check found in the
77     * checkWildcard() method, you would put 'wildcard' into this array.
78     *
79     * @var array
80     */
81    protected $skipChecks = [];
82
83    /**
84     * List of 'opt-in' methods (others are 'opt-out' by default).
85     *
86     * @var array
87     */
88    protected $optInMethods = ['fuzzy', 'truncatechar'];
89
90    /**
91     * Search results object.
92     *
93     * @var \VuFind\Search\Base\Results
94     */
95    protected $results;
96
97    /**
98     * Constructor
99     *
100     * @param Service $searchService Search backend plugin manager
101     */
102    public function __construct(Service $searchService)
103    {
104        $this->searchService = $searchService;
105    }
106
107    /**
108     * Store the configuration of the recommendation module.
109     *
110     * @param string $settings Settings from searches.ini.
111     *
112     * @return void
113     */
114    public function setConfig($settings)
115    {
116        $params = explode(':', $settings);
117        $this->backend = !empty($params[0]) ? $params[0] : 'Solr';
118        $callback = function ($i) {
119            return trim(strtolower($i));
120        };
121        // Get a list of "opt out" preferences from the user...
122        $this->skipChecks = !empty($params[1])
123            ? array_map($callback, explode(',', $params[1])) : [];
124        $optIns = !empty($params[2])
125            ? explode(',', $params[2]) : [];
126        $this->skipChecks = array_merge(
127            $this->skipChecks,
128            array_diff($this->optInMethods, $optIns)
129        );
130    }
131
132    /**
133     * Called before the Search Results object performs its main search
134     * (specifically, in response to \VuFind\Search\SearchRunner::EVENT_CONFIGURED).
135     * This method is responsible for setting search parameters needed by the
136     * recommendation module and for reading any existing search parameters that may
137     * be needed.
138     *
139     * @param \VuFind\Search\Base\Params $params  Search parameter object
140     * @param \Laminas\Stdlib\Parameters $request Parameter object representing user
141     * request.
142     *
143     * @return void
144     */
145    public function init($params, $request)
146    {
147    }
148
149    /**
150     * Called after the Search Results object has performed its main search. This
151     * may be used to extract necessary information from the Search Results object
152     * or to perform completely unrelated processing.
153     *
154     * @param \VuFind\Search\Base\Results $results Search results object
155     *
156     * @return void
157     */
158    public function process($results)
159    {
160        $this->results = $results;
161
162        // We can't currently deal with advanced searches:
163        if ($this->results->getParams()->getSearchType() == 'advanced') {
164            return;
165        }
166
167        // Get the query to manipulate:
168        $query = $this->results->getParams()->getDisplayQuery();
169
170        // If the query is of a type that should be skipped, go no further:
171        if ($this->queryShouldBeSkipped($query)) {
172            return;
173        }
174
175        // Perform all checks (based on naming convention):
176        $methods = get_class_methods($this);
177        foreach ($methods as $method) {
178            if (str_starts_with($method, 'check')) {
179                $currentCheck = strtolower(substr($method, 5));
180                if (!in_array($currentCheck, $this->skipChecks)) {
181                    if ($result = $this->$method($query)) {
182                        $this->suggestions['switchquery_' . $currentCheck] = $result;
183                    }
184                }
185            }
186        }
187    }
188
189    /**
190     * Should the query be ignored when making recommendations?
191     *
192     * @param string $query Query to check
193     *
194     * @return bool
195     */
196    protected function queryShouldBeSkipped($query)
197    {
198        // If this is an ID list query, it was probably generated by New Items,
199        // Course Reserves, etc., and thus should not be further manipulated by
200        // the user.
201        return str_starts_with($query, 'id:');
202    }
203
204    /**
205     * Will a fuzzy search help?
206     *
207     * @param string $query Query to check
208     *
209     * @return string|bool
210     */
211    protected function checkFuzzy($query)
212    {
213        // Don't stack tildes:
214        if (str_contains($query, '~')) {
215            return false;
216        }
217        $query = trim($query, ' ?*');
218        // Fuzzy search only works for single keywords, not phrases:
219        if (str_ends_with($query, '"')) {
220            return false;
221        }
222        return str_ends_with($query, '~') ? false : $query . '~';
223    }
224
225    /**
226     * Does the query contain lowercase boolean operators that should be uppercased?
227     *
228     * @param string $query Query to check
229     *
230     * @return string|bool
231     */
232    protected function checkLowercaseBools($query)
233    {
234        // This test only applies if booleans are case-sensitive and there is a
235        // capitalization method available:
236        $lh = $this->getLuceneHelper();
237        if (!$lh || !$lh->hasCaseSensitiveBooleans()) {
238            return false;
239        }
240
241        // Try to capitalize booleans, return new query if a change is found:
242        $newQuery = $lh->capitalizeBooleans($query);
243        return ($query == $newQuery) ? false : $newQuery;
244    }
245
246    /**
247     * Does the query contain terms that are being treated as boolean operators,
248     * perhaps unintentionally?
249     *
250     * @param string $query Query to check
251     *
252     * @return string|bool
253     */
254    protected function checkUnwantedBools($query)
255    {
256        $query = trim($query);
257        $lh = $this->getLuceneHelper();
258        if (!$lh || !$lh->containsBooleans($query)) {
259            return false;
260        }
261        return '"' . addcslashes($query, '"') . '"';
262    }
263
264    /**
265     * Would removing quotes help?
266     *
267     * @param string $query Query to check
268     *
269     * @return string|bool
270     */
271    protected function checkUnwantedQuotes($query)
272    {
273        // Remove escaped quotes as they are of no consequence:
274        $query = str_replace('\"', ' ', $query);
275        return (!str_contains($query, '"'))
276            ? false : trim(str_replace('"', ' ', $query));
277    }
278
279    /**
280     * Will adding a wildcard help?
281     *
282     * @param string $query Query to check
283     *
284     * @return string|bool
285     */
286    protected function checkWildcard($query)
287    {
288        $query = trim($query, ' ?~');
289        // Don't pile wildcards on phrases:
290        if (str_ends_with($query, '"')) {
291            return false;
292        }
293        return !str_ends_with($query, '*') ? $query . '*' : false;
294    }
295
296    /**
297     * Broaden search by truncating one character (e.g. call number)
298     *
299     * @param string $query Query to transform
300     *
301     * @return string|bool
302     */
303    protected function checkTruncatechar($query)
304    {
305        // Don't truncate phrases:
306        if (str_ends_with($query, '"')) {
307            return false;
308        }
309        $query = trim($query);
310        return (strlen($query) > 1) ? substr($query, 0, -1) : false;
311    }
312
313    /**
314     * Extract a Lucene syntax helper from the search backend, if possible.
315     *
316     * @return bool|\VuFindSearch\Backend\Solr\LuceneSyntaxHelper
317     */
318    protected function getLuceneHelper()
319    {
320        $command = new GetLuceneHelperCommand($this->backend);
321        return $this->searchService->invoke($command)->getResult();
322    }
323
324    /**
325     * Get results stored in the object.
326     *
327     * @return \VuFind\Search\Base\Results
328     */
329    public function getResults()
330    {
331        return $this->results;
332    }
333
334    /**
335     * Get an array of suggestion messages.
336     *
337     * @return array
338     */
339    public function getSuggestions()
340    {
341        return $this->suggestions;
342    }
343}