From 975381cc4a25bb480b2ba6266b932139795c81d0 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Thu, 6 Jul 2017 12:03:52 +0200 Subject: [PATCH] TASK: Integrate working code Copied code from customer installation with working implementation. --- .../ConfigurationContainerInterface.php | 2 + Classes/Connection/Elasticsearch.php | 40 +++++- Classes/Connection/Elasticsearch/Facet.php | 93 +++++++++++++ .../Connection/Elasticsearch/FacetOption.php | 61 +++++++++ .../Elasticsearch/MappingFactory.php | 73 +++++++++++ .../Connection/Elasticsearch/ResultItem.php | 56 ++++++++ .../Connection/Elasticsearch/SearchResult.php | 123 +++++++++++++++++- Classes/Connection/FacetInterface.php | 39 ++++++ Classes/Connection/FacetOptionInterface.php | 42 ++++++ Classes/Connection/FacetRequestInterface.php | 42 ++++++ Classes/Connection/ResultItemInterface.php | 29 +++++ Classes/Connection/SearchResultInterface.php | 14 +- Classes/Domain/Index/IndexerFactory.php | 1 + Classes/Domain/Index/TcaIndexer.php | 1 - Classes/Domain/Model/FacetRequest.php | 62 +++++++++ Classes/Domain/Model/SearchRequest.php | 28 +++- Classes/Domain/Search/QueryFactory.php | 103 ++++++++++++--- Classes/Domain/Search/SearchService.php | 56 +++++++- 18 files changed, 832 insertions(+), 33 deletions(-) create mode 100644 Classes/Connection/Elasticsearch/Facet.php create mode 100644 Classes/Connection/Elasticsearch/FacetOption.php create mode 100644 Classes/Connection/Elasticsearch/MappingFactory.php create mode 100644 Classes/Connection/Elasticsearch/ResultItem.php create mode 100644 Classes/Connection/FacetInterface.php create mode 100644 Classes/Connection/FacetOptionInterface.php create mode 100644 Classes/Connection/FacetRequestInterface.php create mode 100644 Classes/Connection/ResultItemInterface.php create mode 100644 Classes/Domain/Model/FacetRequest.php diff --git a/Classes/Configuration/ConfigurationContainerInterface.php b/Classes/Configuration/ConfigurationContainerInterface.php index 7bc45ef..d0d9828 100644 --- a/Classes/Configuration/ConfigurationContainerInterface.php +++ b/Classes/Configuration/ConfigurationContainerInterface.php @@ -34,6 +34,8 @@ interface ConfigurationContainerInterface extends Singleton * * @param string $path In dot notation. E.g. indexer.tca.allowedTables * @return mixed + * + * @throws InvalidArgumentException */ public function get($path); diff --git a/Classes/Connection/Elasticsearch.php b/Classes/Connection/Elasticsearch.php index 386e19c..88b93c2 100644 --- a/Classes/Connection/Elasticsearch.php +++ b/Classes/Connection/Elasticsearch.php @@ -20,8 +20,10 @@ namespace Leonmrni\SearchCore\Connection; * 02110-1301, USA. */ -use TYPO3\CMS\Core\SingletonInterface as Singleton; +use Leonmrni\SearchCore\Connection\Elasticsearch\SearchResult; use Leonmrni\SearchCore\Domain\Search\QueryFactory; +use TYPO3\CMS\Core\SingletonInterface as Singleton; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** * Outer wrapper to elasticsearch. @@ -43,6 +45,11 @@ class Elasticsearch implements Singleton, ConnectionInterface */ protected $typeFactory; + /** + * @var Elasticsearch\MappingFactory + */ + protected $mappingFactory; + /** * @var Elasticsearch\DocumentFactory */ @@ -58,6 +65,11 @@ class Elasticsearch implements Singleton, ConnectionInterface */ protected $logger; + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + /** * Inject log manager to get concrete logger from it. * @@ -68,10 +80,19 @@ class Elasticsearch implements Singleton, ConnectionInterface $this->logger = $logManager->getLogger(__CLASS__); } + /** + * @param ObjectManagerInterface $objectManager + */ + public function injectObjectManager(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + /** * @param Elasticsearch\Connection $connection * @param Elasticsearch\IndexFactory $indexFactory * @param Elasticsearch\TypeFactory $typeFactory + * @param Elasticsearch\MappingFactory $mappingFactory * @param Elasticsearch\DocumentFactory $documentFactory * @param QueryFactory $queryFactory */ @@ -79,12 +100,14 @@ class Elasticsearch implements Singleton, ConnectionInterface Elasticsearch\Connection $connection, Elasticsearch\IndexFactory $indexFactory, Elasticsearch\TypeFactory $typeFactory, + Elasticsearch\MappingFactory $mappingFactory, Elasticsearch\DocumentFactory $documentFactory, QueryFactory $queryFactory ) { $this->connection = $connection; $this->indexFactory = $indexFactory; $this->typeFactory = $typeFactory; + $this->mappingFactory = $mappingFactory; $this->documentFactory = $documentFactory; $this->queryFactory = $queryFactory; } @@ -142,6 +165,13 @@ class Elasticsearch implements Singleton, ConnectionInterface protected function withType($documentType, callable $callback) { $type = $this->getType($documentType); + // TODO: Check whether it's to heavy to send it so often e.g. for every single document. + // Perhaps add command controller to submit mapping?! + // Also it's not possible to change mapping without deleting index first. + // Mattes told about a solution. + // So command looks like the best way so far, except we manage mattes solution. + // Still then this should be done once. So perhaps singleton which tracks state and does only once? + $this->mappingFactory->getMapping($type)->send(); $callback($type); $type->getIndex()->refresh(); } @@ -149,7 +179,7 @@ class Elasticsearch implements Singleton, ConnectionInterface /** * @param SearchRequestInterface $searchRequest * - * @return \Elastica\ResultSet + * @return SearchResultInterface */ public function search(SearchRequestInterface $searchRequest) { @@ -157,11 +187,9 @@ class Elasticsearch implements Singleton, ConnectionInterface $search = new \Elastica\Search($this->connection->getClient()); $search->addIndex('typo3content'); - $search->setQuery($this->queryFactory->create($this, $searchRequest)); + $search->setQuery($this->queryFactory->create($searchRequest)); - // TODO: Return wrapped result to implement our interface. - // Also update php doc to reflect the change. - return $search->search(); + return $this->objectManager->get(SearchResult::class, $search->search()); } /** diff --git a/Classes/Connection/Elasticsearch/Facet.php b/Classes/Connection/Elasticsearch/Facet.php new file mode 100644 index 0000000..79a87b3 --- /dev/null +++ b/Classes/Connection/Elasticsearch/Facet.php @@ -0,0 +1,93 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Connection\FacetInterface; + +class Facet implements FacetInterface +{ + /** + * @var string + */ + protected $name = ''; + + /** + * @var string + */ + protected $field = ''; + + /** + * @var array + */ + protected $buckets = []; + + /** + * @var array + */ + protected $options = []; + + public function __construct($name, array $aggregation, ConfigurationContainerInterface $configuration) + { + $this->name = $name; + $this->buckets = $aggregation['buckets']; + $this->field = $configuration->getIfExists('searching.facets.' . $this->name . '.field') ?: ''; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getField() + { + return $this->field; + } + + /** + * Returns all possible options for this facet. + * + * @return array + */ + public function getOptions() + { + $this->initOptions(); + + return $this->options; + } + + protected function initOptions() + { + if ($this->options !== []) { + return; + } + + foreach ($this->buckets as $bucket) { + $this->options[] = new FacetOption($bucket); + } + } +} diff --git a/Classes/Connection/Elasticsearch/FacetOption.php b/Classes/Connection/Elasticsearch/FacetOption.php new file mode 100644 index 0000000..a618be1 --- /dev/null +++ b/Classes/Connection/Elasticsearch/FacetOption.php @@ -0,0 +1,61 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Connection\FacetOptionInterface; + +class FacetOption implements FacetOptionInterface +{ + /** + * @var string + */ + protected $name = ''; + + /** + * @var int + */ + protected $count = 0; + + /** + * @param array $bucket + */ + public function __construct(array $bucket) + { + $this->name = $bucket['key']; + $this->count = $bucket['doc_count']; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return int + */ + public function getCount() + { + return $this->count; + } +} diff --git a/Classes/Connection/Elasticsearch/MappingFactory.php b/Classes/Connection/Elasticsearch/MappingFactory.php new file mode 100644 index 0000000..f029cd1 --- /dev/null +++ b/Classes/Connection/Elasticsearch/MappingFactory.php @@ -0,0 +1,73 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Configuration\InvalidArgumentException; +use TYPO3\CMS\Core\SingletonInterface as Singleton; + +/** + * Factory to get mappings. + */ +class MappingFactory implements Singleton +{ + /** + * @var ConfigurationContainerInterface + */ + protected $configuration; + + /** + * @param ConfigurationContainerInterface $configuration + */ + public function __construct(ConfigurationContainerInterface $configuration) + { + $this->configuration = $configuration; + } + + /** + * Get an mapping based on type. + * + * @param \Elastica\Type $type + * + * @return \Elastica\Mapping + */ + public function getMapping(\Elastica\Type $type) + { + $mapping = new \Elastica\Type\Mapping(); + $mapping->setType($type); + $mapping->setProperties($this->getConfiguration($type->getName())); + + return $mapping; + } + + /** + * @param string $identifier + * @return array + */ + protected function getConfiguration($identifier) + { + try { + return $this->configuration->get('indexing.' . $identifier . '.mapping'); + } catch (InvalidArgumentException $e) { + return []; + } + } +} diff --git a/Classes/Connection/Elasticsearch/ResultItem.php b/Classes/Connection/Elasticsearch/ResultItem.php new file mode 100644 index 0000000..dd8cb62 --- /dev/null +++ b/Classes/Connection/Elasticsearch/ResultItem.php @@ -0,0 +1,56 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Connection\ResultItemInterface; + +class ResultItem implements ResultItemInterface +{ + /** + * @var array + */ + protected $data = []; + + public function __construct(\Elastica\Result $result) + { + $this->data = $result->getData(); + } + + public function offsetExists($offset) + { + return isset($this->data[$offset]); + } + + public function offsetGet($offset) + { + return $this->data[$offset]; + } + + public function offsetSet() + { + throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077); + } + + public function offsetUnset() + { + throw new \BadMethodCallException('It\'s not possible to change the search result.', 1499179077); + } +} diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 3f90d60..63c93b0 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -20,12 +20,127 @@ namespace Leonmrni\SearchCore\Connection\Elasticsearch; * 02110-1301, USA. */ +use Leonmrni\SearchCore\Connection\FacetInterface; +use Leonmrni\SearchCore\Connection\ResultItemInterface; use Leonmrni\SearchCore\Connection\SearchResultInterface; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; -/** - * - */ -class SearchResult extends \Elastica\SearchResult implements SearchResultInterface +class SearchResult implements SearchResultInterface { + /** + * @var \Elastica\ResultSet + */ + protected $result; + /** + * @var array + */ + protected $facets = []; + + /** + * @var array + */ + protected $results = []; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + public function __construct(\Elastica\ResultSet $result, ObjectManagerInterface $objectManager) + { + $this->result = $result; + $this->objectManager = $objectManager; + } + + /** + * @return array + */ + public function getResults() + { + $this->initResults(); + + return $this->results; + } + + /** + * Return all facets, if any. + * + * @return array + */ + public function getFacets() + { + $this->initFacets(); + + return $this->facets; + } + + /** + * Returns the total sum of matching results. + * + * @return int + */ + public function getTotalCount() + { + return $this->result->getTotalHits(); + } + + // Countable - Interface + /** + * Returns the total sum of results contained in this result. + * + * @return int + */ + public function count() + { + return $this->result->count(); + } + + // Iterator - Interface + public function current() + { + return $this->result->current(); + } + + public function next() + { + return $this->result->next(); + } + + public function key() + { + return $this->result->key(); + } + + public function valid() + { + return $this->result->valid(); + } + + public function rewind() + { + $this->result->rewind(); + } + + protected function initResults() + { + if ($this->results !== []) { + return; + } + + foreach ($this->result->getResults() as $result) { + $this->results[] = new ResultItem($result); + } + } + + protected function initFacets() + { + if ($this->facets !== [] || !$this->result->hasAggregations()) { + return; + } + + foreach ($this->result->getAggregations() as $aggregationName => $aggregation) { + $this->facets[] = $this->objectManager->get(Facet::class, $aggregationName, $aggregation); + } + } } diff --git a/Classes/Connection/FacetInterface.php b/Classes/Connection/FacetInterface.php new file mode 100644 index 0000000..f422e98 --- /dev/null +++ b/Classes/Connection/FacetInterface.php @@ -0,0 +1,39 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * A single facet, e.g. keyword based. + */ +interface FacetInterface +{ + /** + * @return string + */ + public function getName(); + + /** + * Returns all possible options for this facet. + * + * @return array + */ + public function getOptions(); +} diff --git a/Classes/Connection/FacetOptionInterface.php b/Classes/Connection/FacetOptionInterface.php new file mode 100644 index 0000000..b05445b --- /dev/null +++ b/Classes/Connection/FacetOptionInterface.php @@ -0,0 +1,42 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * A single possible option of a facet. + */ +interface FacetOptionInterface +{ + /** + * Returns the name of this option. Equivalent + * to value used for filtering. + * + * @return string + */ + public function getName(); + + /** + * Returns the number of found results for this option. + * + * @return int + */ + public function getCount(); +} diff --git a/Classes/Connection/FacetRequestInterface.php b/Classes/Connection/FacetRequestInterface.php new file mode 100644 index 0000000..e5a60a1 --- /dev/null +++ b/Classes/Connection/FacetRequestInterface.php @@ -0,0 +1,42 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * Used to request facets / aggregates from connection. + */ +interface FacetRequestInterface +{ + /** + * The identifier of the facet, used as key in arrays and to get the facet + * from search request, etc. + * + * @return string + */ + public function getIdentifier(); + + /** + * The field to use for facet building. + * + * @return string + */ + public function getField(); +} diff --git a/Classes/Connection/ResultItemInterface.php b/Classes/Connection/ResultItemInterface.php new file mode 100644 index 0000000..f4c9185 --- /dev/null +++ b/Classes/Connection/ResultItemInterface.php @@ -0,0 +1,29 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * Use ArrayAccess to enable retrieval of information in fluid. + */ +interface ResultItemInterface extends \ArrayAccess +{ + +} diff --git a/Classes/Connection/SearchResultInterface.php b/Classes/Connection/SearchResultInterface.php index f00e97c..a6b2c90 100644 --- a/Classes/Connection/SearchResultInterface.php +++ b/Classes/Connection/SearchResultInterface.php @@ -21,9 +21,19 @@ namespace Leonmrni\SearchCore\Connection; */ /** - * + * A search result. */ -interface SearchResultInterface extends \Iterator, \Countable, \ArrayAccess +interface SearchResultInterface extends \Iterator, \Countable { + /** + * @return array + */ + public function getResults(); + /** + * Return all facets, if any. + * + * @return array + */ + public function getFacets(); } diff --git a/Classes/Domain/Index/IndexerFactory.php b/Classes/Domain/Index/IndexerFactory.php index 0140122..24addd1 100644 --- a/Classes/Domain/Index/IndexerFactory.php +++ b/Classes/Domain/Index/IndexerFactory.php @@ -45,6 +45,7 @@ class IndexerFactory implements Singleton /** * @param ObjectManagerInterface $objectManager + * @param ConfigurationContainerInterface $configuration */ public function __construct( ObjectManagerInterface $objectManager, diff --git a/Classes/Domain/Index/TcaIndexer.php b/Classes/Domain/Index/TcaIndexer.php index 52f1efa..17dc61e 100644 --- a/Classes/Domain/Index/TcaIndexer.php +++ b/Classes/Domain/Index/TcaIndexer.php @@ -20,7 +20,6 @@ namespace Leonmrni\SearchCore\Domain\Index; * 02110-1301, USA. */ -use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; use Leonmrni\SearchCore\Connection\ConnectionInterface; /** diff --git a/Classes/Domain/Model/FacetRequest.php b/Classes/Domain/Model/FacetRequest.php new file mode 100644 index 0000000..715e74a --- /dev/null +++ b/Classes/Domain/Model/FacetRequest.php @@ -0,0 +1,62 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use Leonmrni\SearchCore\Connection\FacetRequestInterface; + +class FacetRequest implements FacetRequestInterface +{ + /** + * @var string + */ + protected $identifier = ''; + + /** + * @var string + */ + protected $field = ''; + + /** + * @param string $identifier + * @param string $field + */ + public function __construct($identifier, $field) + { + $this->identifier = $identifier; + $this->field = $field; + } + + /** + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * @return string + */ + public function getField() + { + return $this->field; + } +} diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index f1ad18e..65ab979 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -20,6 +20,7 @@ namespace Leonmrni\SearchCore\Domain\Model; * 02110-1301, USA. */ +use Leonmrni\SearchCore\Connection\FacetRequestInterface; use Leonmrni\SearchCore\Connection\SearchRequestInterface; /** @@ -39,6 +40,11 @@ class SearchRequest implements SearchRequestInterface */ protected $filter = []; + /** + * @var array + */ + protected $facets = []; + /** * @param string $query */ @@ -68,7 +74,7 @@ class SearchRequest implements SearchRequestInterface */ public function setFilter(array $filter) { - $this->filter = array_map('strval', $filter); + $this->filter = array_filter(array_map('strval', $filter)); } /** @@ -86,4 +92,24 @@ class SearchRequest implements SearchRequestInterface { return $this->filter; } + + /** + * Add a facet to gather in this search request. + * + * @param FacetRequestInterface $facet + */ + public function addFacet(FacetRequestInterface $facet) + { + $this->facets[$facet->getIdentifier()] = $facet; + } + + /** + * Returns all configured facets to fetch in this search request. + * + * @return array + */ + public function getFacets() + { + return $this->facets; + } } diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index 31c50df..778b575 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -23,44 +23,117 @@ namespace Leonmrni\SearchCore\Domain\Search; use Leonmrni\SearchCore\Connection\ConnectionInterface; use Leonmrni\SearchCore\Connection\Elasticsearch\Query; use Leonmrni\SearchCore\Connection\SearchRequestInterface; +use TYPO3\CMS\Extbase\Utility\ArrayUtility; class QueryFactory { /** - * @param ConnectionInterface $connection + * @var \TYPO3\CMS\Core\Log\Logger + */ + protected $logger; + + /** + * @var array + */ + protected $query = []; + + /** + * @param \TYPO3\CMS\Core\Log\LogManager $logManager + */ + public function __construct(\TYPO3\CMS\Core\Log\LogManager $logManager) + { + $this->logger = $logManager->getLogger(__CLASS__); + } + + /** * @param SearchRequestInterface $searchRequest * * @return \Elastica\Query */ - public function create( - ConnectionInterface $connection, - SearchRequestInterface $searchRequest - ) { + public function create(SearchRequestInterface $searchRequest) + { return $this->createElasticaQuery($searchRequest); } /** * @param SearchRequestInterface $searchRequest + * + * TODO: This is not in scope Elasticsearch, therefore should not return elastica. * @return \Elastica\Query */ protected function createElasticaQuery(SearchRequestInterface $searchRequest) { - $query = [ - 'bool' => [ - 'must' => [ - [ - 'match' => [ - '_all' => $searchRequest->getSearchTerm() + $this->addSearch($searchRequest); + $this->addFilter($searchRequest); + $this->addFacets($searchRequest); + + // TODO: Add logging here. + $this->logger->debug('Generated elasticsearch query.', [$this->query]); + return new \Elastica\Query($this->query); + } + + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addSearch(SearchRequestInterface $searchRequest) + { + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'query' => [ + 'bool' => [ + 'must' => [ + [ + 'match' => [ + '_all' => $searchRequest->getSearchTerm() + ], ], ], ], ], - ]; + ]); + } - if ($searchRequest->hasFilter()) { - $query['bool']['filter'] = ['term' => $searchRequest->getFilter()]; + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addFilter(SearchRequestInterface $searchRequest) + { + if (! $searchRequest->hasFilter()) { + return; } - return new \Elastica\Query(['query' => $query]); + $terms = []; + foreach ($searchRequest->getFilter() as $name => $value) { + $terms[] = [ + 'term' => [ + $name => $value, + ], + ]; + } + + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'query' => [ + 'bool' => [ + 'filter' => $terms, + ], + ], + ]); + } + + /** + * @param SearchRequestInterface $searchRequest + */ + protected function addFacets(SearchRequestInterface $searchRequest) + { + foreach ($searchRequest->getFacets() as $facet) { + $this->query = ArrayUtility::arrayMergeRecursiveOverrule($this->query, [ + 'aggs' => [ + $facet->getIdentifier() => [ + 'terms' => [ + 'field' => $facet->getField(), + ], + ], + ], + ]); + } } } diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index afbaf7e..ac991cd 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -20,10 +20,13 @@ namespace Leonmrni\SearchCore\Domain\Search; * 02110-1301, USA. */ +use Leonmrni\SearchCore\Configuration\ConfigurationContainerInterface; +use Leonmrni\SearchCore\Configuration\InvalidArgumentException; use Leonmrni\SearchCore\Connection\ConnectionInterface; use Leonmrni\SearchCore\Connection\SearchRequestInterface; use Leonmrni\SearchCore\Connection\SearchResultInterface; -use Leonmrni\SearchCore\Domain\Model\SearchRequest; +use Leonmrni\SearchCore\Domain\Model\FacetRequest; +use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** * Service to process a search request. @@ -36,11 +39,28 @@ class SearchService protected $connection; /** - * @param ConnectionInterface $connection + * @var ConfigurationContainerInterface */ - public function __construct(ConnectionInterface $connection) - { + protected $configuration; + + /** + * @var ObjectManagerInterface + */ + protected $objectManager; + + /** + * @param ConnectionInterface $connection + * @param ConfigurationContainerInterface $configuration + * @param ObjectManagerInterface $objectManager + */ + public function __construct( + ConnectionInterface $connection, + ConfigurationContainerInterface $configuration, + ObjectManagerInterface $objectManager + ) { $this->connection = $connection; + $this->configuration = $configuration; + $this->objectManager = $objectManager; } /** @@ -49,6 +69,34 @@ class SearchService */ public function search(SearchRequestInterface $searchRequest) { + $this->addConfiguredFacets($searchRequest); + return $this->connection->search($searchRequest); } + + /** + * Add facets from configuration to request. + * + * @param SearchRequestInterface $searchRequest + */ + protected function addConfiguredFacets(SearchRequestInterface $searchRequest) + { + $facetsConfig = $this->configuration->getIfExists('searching.facets'); + if ($facetsConfig === null) { + return; + } + + foreach ($facetsConfig as $identifier => $facetConfig) { + if (!isset($facetConfig['field']) || trim($facetConfig['field']) === '') { + // TODO: Finish throw + throw new \Exception('message', 1499171142); + } + + $searchRequest->addFacet($this->objectManager->get( + FacetRequest::class, + $identifier, + $facetConfig['field'] + )); + } + } }