diff --git a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php index 3f395dd97999d56dab4a67b7681bb8ede26a718e..c7b3e8360f1dca3ab532a7615187b76ef38eb494 100644 --- a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php +++ b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php @@ -30,6 +30,7 @@ namespace VuFind\Search\Factory; use VuFind\Search\Solr\InjectHighlightingListener; +use VuFind\Search\Solr\InjectSpellingListener; use VuFind\Search\Solr\MultiIndexListener; use VuFind\Search\Solr\V3\ErrorListener as LegacyErrorListener; use VuFind\Search\Solr\V4\ErrorListener; @@ -54,7 +55,6 @@ use Zend\ServiceManager\FactoryInterface; */ abstract class AbstractSolrBackendFactory implements FactoryInterface { - /** * Logger. * @@ -140,21 +140,8 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface */ protected function createBackend(Connector $connector) { - - $config = $this->config->get('config'); $backend = new Backend($connector); $backend->setQueryBuilder($this->createQueryBuilder()); - - // Spellcheck - if (isset($config->Spelling->enabled) && $config->Spelling->enabled) { - if (isset($config->Spelling->simple) && $config->Spelling->simple) { - $dictionaries = array('basicSpell'); - } else { - $dictionaries = array('default', 'basicSpell'); - } - $backend->setDictionaries($dictionaries); - } - if ($this->logger) { $backend->setLogger($this->logger); } @@ -176,6 +163,18 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface $highlightListener = new InjectHighlightingListener($backend); $highlightListener->attach($events); + // Spellcheck + $config = $this->config->get('config'); + if (isset($config->Spelling->enabled) && $config->Spelling->enabled) { + if (isset($config->Spelling->simple) && $config->Spelling->simple) { + $dictionaries = array('basicSpell'); + } else { + $dictionaries = array('default', 'basicSpell'); + } + $spellingListener = new InjectSpellingListener($backend, $dictionaries); + $spellingListener->attach($events); + } + // Apply field stripping if applicable: $search = $this->config->get($this->searchConfig); if (isset($search->StripFields) && isset($search->IndexShards)) { diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php new file mode 100644 index 0000000000000000000000000000000000000000..d7d8272a305f15769097cf284793137dff5b8581 --- /dev/null +++ b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php @@ -0,0 +1,181 @@ +<?php + +/** + * Solr spelling listener. + * + * PHP version 5 + * + * Copyright (C) Villanova University 2013. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ + +namespace VuFind\Search\Solr; + +use VuFindSearch\Backend\BackendInterface; +use VuFindSearch\Backend\Solr\Response\Json\Spellcheck; +use VuFindSearch\ParamBag; +use VuFindSearch\Query\Query; + +use Zend\EventManager\SharedEventManagerInterface; +use Zend\EventManager\EventInterface; + +/** + * Solr spelling listener. + * + * @category VuFind2 + * @package Search + * @author David Maus <maus@hab.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org Main Site + */ +class InjectSpellingListener +{ + /** + * Backend. + * + * @var BackendInterface + */ + protected $backend; + + /** + * Is spelling active? + * + * @var bool + */ + protected $active = false; + + /** + * Dictionaries for spellcheck. + * + * @var array + */ + protected $dictionaries; + + /** + * Constructor. + * + * @param BackendInterface $backend Backend + * + * @return void + */ + public function __construct(BackendInterface $backend, array $dictionaries) + { + $this->backend = $backend; + $this->dictionaries = $dictionaries; + } + + /** + * Attach listener to shared event manager. + * + * @param SharedEventManagerInterface $manager Shared event manager + * + * @return void + */ + public function attach(SharedEventManagerInterface $manager) + { + $manager->attach('VuFind\Search', 'pre', array($this, 'onSearchPre')); + $manager->attach('VuFind\Search', 'post', array($this, 'onSearchPost')); + } + + /** + * Set up spelling parameters. + * + * @param EventInterface $event Event + * + * @return EventInterface + */ + public function onSearchPre(EventInterface $event) + { + $backend = $event->getTarget(); + if ($backend === $this->backend) { + $params = $event->getParam('params'); + if ($params) { + // Set spelling parameters unless explicitly disabled: + $sc = $params->get('spellcheck'); + if (!isset($sc[0]) || $sc[0] != 'false') { + $this->active = true; + if (empty($this->dictionaries)) { + throw new \Exception( + 'Spellcheck requested but no dictionary configured' + ); + } + + // Set relevant Solr parameters: + reset($this->dictionaries); + $params->set('spellcheck', 'true'); + $params->set( + 'spellcheck.dictionary', current($this->dictionaries) + ); + + // Turn on spellcheck.q generation in query builder: + $this->backend->getQueryBuilder() + ->setCreateSpellingQuery(true); + } + } + } + return $event; + } + + /** + * Inject additional spelling suggestions. + * + * @param EventInterface $event Event + * + * @return EventInterface + */ + public function onSearchPost(EventInterface $event) + { + // Do nothing if spelling is disabled.... + if (!$this->active) { + return $event; + } + + // Merge spelling details from extra dictionaries: + $backend = $event->getParam('backend'); + if ($backend == $this->backend->getIdentifier()) { + $result = $event->getTarget(); + $params = $event->getParam('params'); + $spellcheckQuery = $params->get('spellcheck.q'); + $this->aggregateSpellcheck( + $result->getSpellcheck(), end($spellcheckQuery) + ); + } + } + + /** + * Submit requests for more spelling suggestions. + * + * @param Spellcheck $spellcheck Aggregating spellcheck object + * @param string $query Spellcheck query + * + * @return void + */ + protected function aggregateSpellcheck(Spellcheck $spellcheck, $query) + { + while (next($this->dictionaries) !== false) { + $params = new ParamBag(); + $params->set('spellcheck', 'true'); + $params->set('spellcheck.dictionary', current($this->dictionaries)); + $collection = $this->backend->search(new Query($query), 0, 0, $params); + $spellcheck->mergeWith($collection->getSpellcheck()); + } + } +} diff --git a/module/VuFind/src/VuFind/Search/Solr/Results.php b/module/VuFind/src/VuFind/Search/Solr/Results.php index 05a4b716753c3b91fcebce363cf67771aedd0930..19a6cb654eb9127130365528f2894ee084f5d7eb 100644 --- a/module/VuFind/src/VuFind/Search/Solr/Results.php +++ b/module/VuFind/src/VuFind/Search/Solr/Results.php @@ -67,7 +67,7 @@ class Results extends BaseResults * * @var string */ - protected $spellingQuery; + protected $spellingQuery = ''; /** * Support method for performAndProcessSearch -- perform a search based on the @@ -88,8 +88,7 @@ class Results extends BaseResults $this->resultTotal = $collection->getTotal(); // Process spelling suggestions - $spellcheck = $collection->getSpellcheck(); - $this->processSpelling($spellcheck); + $this->processSpelling($collection->getSpellcheck()); // Construct record drivers for all the items in the response: $this->results = $collection->getRecords(); @@ -148,13 +147,10 @@ class Results extends BaseResults $backendParams = new ParamBag(); // Spellcheck - if ($params->getOptions()->spellcheckEnabled()) { - $spelling = $query->getAllTerms(); - if ($spelling) { - $backendParams->set('spellcheck.q', $spelling); - $this->spellingQuery = $spelling; - } - } + $backendParams->set( + 'spellcheck', + $params->getOptions()->spellcheckEnabled() ? 'true' : 'false' + ); // Facets $facets = $params->getFacetSettings(); @@ -212,9 +208,9 @@ class Results extends BaseResults */ protected function processSpelling(Spellcheck $spellcheck) { + $this->spellingQuery = $spellcheck->getQuery(); $this->suggestions = array(); foreach ($spellcheck as $term => $info) { - // TODO: Avoid reference to Options if ($this->getOptions()->shouldSkipNumericSpelling() && is_numeric($term) diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php index 6a9f3c1ea3d285fcb7721c76d3c5aa0713be1b43..054a79be7a7d8eaf4679c3116e39b80bbd71104b 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php @@ -36,7 +36,6 @@ use VuFindSearch\Response\RecordCollectionInterface; use VuFindSearch\Response\RecordCollectionFactoryInterface; use VuFindSearch\Backend\Solr\Response\Json\Terms; -use VuFindSearch\Backend\Solr\Response\Json\Spellcheck; use VuFindSearch\Backend\BackendInterface; use VuFindSearch\Feature\SimilarInterface; @@ -67,13 +66,6 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf */ protected $collectionFactory; - /** - * Dictionaries for spellcheck. - * - * @var array - */ - protected $dictionaries; - /** * Logger, if any. * @@ -112,7 +104,6 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf public function __construct(Connector $connector) { $this->connector = $connector; - $this->dictionaries = array(); $this->identifier = null; } @@ -128,19 +119,6 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf $this->identifier = $identifier; } - /** - * Set the spellcheck dictionaries to use. - * - * @param array $dictionaries Spellcheck dictionaries - * - * @return void - */ - public function setDictionaries(array $dictionaries) - { - $this->dictionaries = $dictionaries; - } - - /** * Perform a search and return record collection. * @@ -157,21 +135,6 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf $params = $params ?: new ParamBag(); $this->injectResponseWriter($params); - $spellcheck = $params->get('spellcheck.q'); - if ($spellcheck) { - if (empty($this->dictionaries)) { - $this->log( - 'warn', - 'Spellcheck requested but no spellcheck dictionary configured' - ); - $spellcheck = false; - } else { - reset($this->dictionaries); - $params->set('spellcheck', 'true'); - $params->set('spellcheck.dictionary', current($this->dictionaries)); - } - } - $params->set('rows', $limit); $params->set('start', $offset); $params->mergeWith($this->getQueryBuilder()->build($query)); @@ -179,13 +142,6 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf $collection = $this->createRecordCollection($response); $this->injectSourceIdentifier($collection); - if ($spellcheck) { - $spellcheckQuery = $params->get('spellcheck.q'); - $this->aggregateSpellcheck( - $collection->getSpellcheck(), end($spellcheckQuery) - ); - } - return $collection; } @@ -545,27 +501,4 @@ class Backend implements BackendInterface, SimilarInterface, RetrieveBatchInterf $params->set('wt', array('json')); $params->set('json.nl', array('arrarr')); } - - /** - * Submit requests for more spelling suggestions. - * - * @param Spellcheck $spellcheck Aggregating spellcheck object - * @param string $query Spellcheck query - * - * @return void - */ - protected function aggregateSpellcheck(Spellcheck $spellcheck, $query) - { - while (next($this->dictionaries) !== false) { - $params = new ParamBag(array('q' => '*:*', 'rows' => 0)); - $params->set('spellcheck', 'true'); - $params->set('spellcheck.q', $query); - $params->set('spellcheck.dictionary', current($this->dictionaries)); - $this->injectResponseWriter($params); - - $response = $this->connector->search($params); - $collection = $this->createRecordCollection($response); - $spellcheck->mergeWith($collection->getSpellcheck()); - } - } } \ No newline at end of file diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php index 1175ab33433126d99a3cde1806130df11b8891db..a2082950e967e61c6d42c7c18ef593ea2c30fe58 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php @@ -93,6 +93,13 @@ class QueryBuilder implements QueryBuilderInterface */ public $createHighlightingQuery = false; + /** + * Should we create the spellcheck.q parameter when appropriate? + * + * @var bool + */ + public $createSpellingQuery = false; + /** * Constructor. * @@ -159,6 +166,11 @@ class QueryBuilder implements QueryBuilderInterface } $params->set('q', $string); + // Add spelling query if applicable: + if ($this->createSpellingQuery) { + $params->set('spellcheck.q', $query->getAllTerms()); + } + return $params; } @@ -176,6 +188,19 @@ class QueryBuilder implements QueryBuilderInterface $this->createHighlightingQuery = $enable; } + /** + * Control whether or not the QueryBuilder should create a spellcheck.q + * parameter. (Turned off by default). + * + * @param bool $enable Should spelling query generation be enabled? + * + * @return void + */ + public function setCreateSpellingQuery($enable) + { + $this->createSpellingQuery = $enable; + } + /** * Return true if the search string contains advanced Lucene syntax. * diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php index 0d21e70873c938497b7ce4ced5d1622422fe620a..756e3d8e4805a5f320ae9c44c19fedac70c1e408 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php @@ -98,8 +98,11 @@ class RecordCollection extends AbstractRecordCollection public function getSpellcheck() { if (!$this->spellcheck) { + $sq = isset($this->response['responseHeader']['params']['spellcheck.q']) + ? $this->response['responseHeader']['params']['spellcheck.q'] + : $this->response['responseHeader']['params']['q']; $this->spellcheck - = new Spellcheck($this->response['spellcheck']['suggestions']); + = new Spellcheck($this->response['spellcheck']['suggestions'], $sq); } return $this->spellcheck; } diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php index c30f51ea0864cd2cf0b23cdbbeb83ab72d15b2b4..ba0a687259b991daaf28e76c3f6eb0c0ced9ec6d 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php @@ -51,14 +51,22 @@ class Spellcheck implements IteratorAggregate, Countable */ protected $terms; + /** + * Spelling query that generated suggestions + * + * @var string + */ + protected $query; + /** * Constructor. * - * @param array $spellcheck SOLR spellcheck information + * @param array $spellcheck SOLR spellcheck information + * @param string $query Spelling query that generated suggestions * * @return void */ - public function __construct(array $spellcheck) + public function __construct(array $spellcheck, $query) { $this->terms = new ArrayObject(); $list = new NamedList($spellcheck); @@ -67,6 +75,17 @@ class Spellcheck implements IteratorAggregate, Countable $this->terms->offsetSet($term, $info); } } + $this->query = $query; + } + + /** + * Get spelling query. + * + * @return string + */ + public function getQuery() + { + return $this->query; } /** diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php index c4acde2560de9aa37a3025480e0972f04837ce8b..283f7ceaa80b87727e27f90635991b02341fb1c7 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php @@ -55,16 +55,29 @@ class SpellcheckTest extends TestCase array('this is a phrase', array()), array('foo', array()), array('foobar', array()) - ) + ), + 'fake query' ); $s2 = new Spellcheck( array( array('is a', array()), array('bar', array()), array('foo bar', array()) - ) + ), + 'fake query' ); $s1->mergeWith($s2); $this->assertCount(5, $s1); } + + /** + * Test getQuery() + * + * @return void + */ + public function testGetQuery() + { + $s = new Spellcheck(array(), 'test'); + $this->assertEquals('test', $s->getQuery()); + } } \ No newline at end of file