Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.67% covered (danger)
21.67%
13 / 60
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
InjectSpellingListener
21.67% covered (danger)
21.67%
13 / 60
40.00% covered (danger)
40.00%
2 / 5
173.73
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 attach
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 onSearchPre
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
56
 onSearchPost
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 aggregateSpellcheck
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3/**
4 * Solr spelling listener.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2013.
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
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 Site
28 */
29
30namespace VuFind\Search\Solr;
31
32use Laminas\EventManager\EventInterface;
33use Laminas\EventManager\SharedEventManagerInterface;
34use Laminas\Log\LoggerInterface;
35use VuFind\Log\LoggerAwareTrait;
36use VuFindSearch\Backend\BackendInterface;
37use VuFindSearch\Backend\Solr\Response\Json\Spellcheck;
38use VuFindSearch\ParamBag;
39use VuFindSearch\Query\Query;
40use VuFindSearch\Service;
41
42/**
43 * Solr spelling listener.
44 *
45 * @category VuFind
46 * @package  Search
47 * @author   Demian Katz <demian.katz@villanova.edu>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
49 * @link     https://vufind.org Main Site
50 */
51class InjectSpellingListener
52{
53    use LoggerAwareTrait;
54
55    /**
56     * Backend.
57     *
58     * @var BackendInterface
59     */
60    protected $backend;
61
62    /**
63     * Is spelling active?
64     *
65     * @var bool
66     */
67    protected $active = false;
68
69    /**
70     * Dictionaries for spellcheck.
71     *
72     * @var array
73     */
74    protected $dictionaries;
75
76    /**
77     * Constructor.
78     *
79     * @param BackendInterface $backend      Backend
80     * @param array            $dictionaries Spelling dictionaries to use.
81     * @param LoggerInterface  $logger       Logger
82     *
83     * @return void
84     */
85    public function __construct(
86        BackendInterface $backend,
87        array $dictionaries,
88        LoggerInterface $logger = null
89    ) {
90        $this->backend = $backend;
91        $this->dictionaries = $dictionaries;
92        $this->setLogger($logger);
93    }
94
95    /**
96     * Attach listener to shared event manager.
97     *
98     * @param SharedEventManagerInterface $manager Shared event manager
99     *
100     * @return void
101     */
102    public function attach(SharedEventManagerInterface $manager)
103    {
104        $manager->attach(
105            Service::class,
106            Service::EVENT_PRE,
107            [$this, 'onSearchPre']
108        );
109        $manager->attach(
110            Service::class,
111            Service::EVENT_POST,
112            [$this, 'onSearchPost']
113        );
114    }
115
116    /**
117     * Set up spelling parameters.
118     *
119     * @param EventInterface $event Event
120     *
121     * @return EventInterface
122     */
123    public function onSearchPre(EventInterface $event)
124    {
125        $command = $event->getParam('command');
126        if ($command->getContext() !== 'search') {
127            return $event;
128        }
129        if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) {
130            if ($params = $command->getSearchParameters()) {
131                // Set spelling parameters when enabled:
132                $sc = $params->get('spellcheck');
133                if (isset($sc[0]) && $sc[0] != 'false') {
134                    $this->active = true;
135                    if (empty($this->dictionaries)) {
136                        throw new \Exception(
137                            'Spellcheck requested but no dictionary configured'
138                        );
139                    }
140
141                    // Set relevant Solr parameters:
142                    reset($this->dictionaries);
143                    $params->set('spellcheck', 'true');
144                    $params->set(
145                        'spellcheck.dictionary',
146                        current($this->dictionaries)
147                    );
148
149                    // Turn on spellcheck.q generation in query builder:
150                    $this->backend->getQueryBuilder()->setCreateSpellingQuery(true);
151                } else {
152                    $this->backend->getQueryBuilder()->setCreateSpellingQuery(false);
153                }
154            }
155        }
156        return $event;
157    }
158
159    /**
160     * Inject additional spelling suggestions.
161     *
162     * @param EventInterface $event Event
163     *
164     * @return EventInterface
165     */
166    public function onSearchPost(EventInterface $event)
167    {
168        // Do nothing if spelling is disabled or context is wrong
169        $command = $event->getParam('command');
170        if (!$this->active || $command->getContext() !== 'search') {
171            return $event;
172        }
173
174        // Merge spelling details from extra dictionaries:
175        if ($command->getTargetIdentifier() === $this->backend->getIdentifier()) {
176            $result = $command->getResult();
177            $params = $command->getSearchParameters();
178            $spellcheckQuery = $params->get('spellcheck.q');
179            if (!empty($spellcheckQuery)) {
180                $this->aggregateSpellcheck(
181                    $result->getSpellcheck(),
182                    end($spellcheckQuery)
183                );
184            }
185        }
186        return $event;
187    }
188
189    /**
190     * Submit requests for more spelling suggestions.
191     *
192     * @param Spellcheck $spellcheck Aggregating spellcheck object
193     * @param string     $query      Spellcheck query
194     *
195     * @return void
196     */
197    protected function aggregateSpellcheck(Spellcheck $spellcheck, $query)
198    {
199        while (next($this->dictionaries) !== false) {
200            $params = new ParamBag();
201            $params->set('spellcheck', 'true');
202            $params->set('spellcheck.dictionary', current($this->dictionaries));
203            $queryObj = new Query($query, 'AllFields');
204            try {
205                $collection = $this->backend->search($queryObj, 0, 0, $params);
206                $spellcheck->mergeWith($collection->getSpellcheck());
207            } catch (\VuFindSearch\Backend\Exception\BackendException $e) {
208                // Don't let exceptions cause the whole search to fail
209                if ($this->logger instanceof \VuFind\Log\ExtendedLoggerInterface) {
210                    $this->logger->logException(
211                        $e,
212                        new \Laminas\Stdlib\Parameters()
213                    );
214                }
215            }
216        }
217    }
218}