From a16bc2a60b8247a4c5e1a25f02e9ebfb417d9131 Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Thu, 21 Dec 2017 11:13:56 +0100 Subject: [PATCH] WIP|FEATURE: Provide working suggest implementation Add all necessary code to provide suggest feature of elasticsearch. --- .../Connection/Elasticsearch/SearchResult.php | 29 +++++++ Classes/Connection/Elasticsearch/Suggest.php | 78 +++++++++++++++++++ .../Elasticsearch/SuggestOption.php | 49 ++++++++++++ Classes/Connection/SearchRequestInterface.php | 30 +++++++ Classes/Connection/SearchResultInterface.php | 7 ++ Classes/Connection/SuggestInterface.php | 46 +++++++++++ Classes/Connection/SuggestOptionInterface.php | 41 ++++++++++ .../Connection/SuggestRequestInterface.php | 42 ++++++++++ Classes/Controller/SearchController.php | 6 ++ Classes/DataProcessing/CopyToProcessor.php | 3 +- Classes/DataProcessing/ExplodeProcessor.php | 51 ++++++++++++ Classes/Domain/Model/SearchRequest.php | 26 +++++++ Classes/Domain/Model/SuggestRequest.php | 66 ++++++++++++++++ Classes/Domain/Search/QueryFactory.php | 23 ++++++ Classes/Domain/Search/SearchService.php | 28 +++++++ 15 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 Classes/Connection/Elasticsearch/Suggest.php create mode 100644 Classes/Connection/Elasticsearch/SuggestOption.php create mode 100644 Classes/Connection/SuggestInterface.php create mode 100644 Classes/Connection/SuggestOptionInterface.php create mode 100644 Classes/Connection/SuggestRequestInterface.php create mode 100644 Classes/DataProcessing/ExplodeProcessor.php create mode 100644 Classes/Domain/Model/SuggestRequest.php diff --git a/Classes/Connection/Elasticsearch/SearchResult.php b/Classes/Connection/Elasticsearch/SearchResult.php index 3919722..ea7e57f 100644 --- a/Classes/Connection/Elasticsearch/SearchResult.php +++ b/Classes/Connection/Elasticsearch/SearchResult.php @@ -24,6 +24,7 @@ use Codappix\SearchCore\Connection\FacetInterface; use Codappix\SearchCore\Connection\ResultItemInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; +use Codappix\SearchCore\Connection\SuggestInterface; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; class SearchResult implements SearchResultInterface @@ -43,6 +44,11 @@ class SearchResult implements SearchResultInterface */ protected $facets = []; + /** + * @var array + */ + protected $suggests = []; + /** * @var array */ @@ -92,6 +98,18 @@ class SearchResult implements SearchResultInterface return $this->facets; } + /** + * Return all suggests, if any. + * + * @return array + */ + public function getSuggests() + { + $this->initSuggests(); + + return $this->suggests; + } + public function getCurrentCount() { return $this->result->count(); @@ -119,6 +137,17 @@ class SearchResult implements SearchResultInterface } } + protected function initSuggests() + { + if ($this->suggests !== []) { + return; + } + + foreach ($this->result->getSuggests() as $suggestName => $suggest) { + $this->suggests[$suggestName] = $this->objectManager->get(Suggest::class, $suggestName, $suggest); + } + } + // Countable - Interface public function count() { diff --git a/Classes/Connection/Elasticsearch/Suggest.php b/Classes/Connection/Elasticsearch/Suggest.php new file mode 100644 index 0000000..1a9ff9a --- /dev/null +++ b/Classes/Connection/Elasticsearch/Suggest.php @@ -0,0 +1,78 @@ + + * + * 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 Codappix\SearchCore\Configuration\ConfigurationContainerInterface; +use Codappix\SearchCore\Connection\SuggestInterface; + +class Suggest implements SuggestInterface +{ + /** + * @var string + */ + protected $name = ''; + + /** + * @var array + */ + protected $options = []; + + /** + * @var array + */ + protected $rawOptions = []; + + public function __construct($name, array $suggest, ConfigurationContainerInterface $configuration) + { + $this->name = $name; + $this->rawOptions = $suggest[0]['options']; + } + + public function getName() + { + return $this->name; + } + + public function getOptions() + { + $this->initOptions(); + + return $this->options; + } + + public function getUniqueOptions() + { + $this->initOptions(); + + return array_unique($this->options); + } + + protected function initOptions() + { + if ($this->options !== []) { + return; + } + + foreach ($this->rawOptions as $rawOption) { + $this->options[] = new SuggestOption($rawOption); + } + } +} diff --git a/Classes/Connection/Elasticsearch/SuggestOption.php b/Classes/Connection/Elasticsearch/SuggestOption.php new file mode 100644 index 0000000..7c98a44 --- /dev/null +++ b/Classes/Connection/Elasticsearch/SuggestOption.php @@ -0,0 +1,49 @@ + + * + * 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 Codappix\SearchCore\Connection\SuggestOptionInterface; + +class SuggestOption implements SuggestOptionInterface +{ + /** + * @var string + */ + protected $text = ''; + + /** + * @param array $option + */ + public function __construct(array $option) + { + $this->text = $option['text']; + } + + public function getText() + { + return $this->text; + } + + public function __toString() + { + return $this->text; + } +} diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index 7c7956e..71b2a05 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -31,6 +31,11 @@ interface SearchRequestInterface extends QueryInterface */ public function getSearchTerm(); + /** + * @param array $filter + */ + public function setFilter(array $filter); + /** * @return bool */ @@ -40,4 +45,29 @@ interface SearchRequestInterface extends QueryInterface * @return array */ public function getFilter(); + + /** + * @param FacetRequestInterface $facet + */ + public function addFacet(FacetRequestInterface $facet); + + /** + * @return array + */ + public function getFacets(); + + /** + * @param SuggestRequestInterface $suggest + */ + public function addSuggest(SuggestRequestInterface $suggest); + + /** + * @return array + */ + public function getSuggests(); + + /** + * @param ConnectionInterface $connection + */ + public function setConnection(ConnectionInterface $connection); } diff --git a/Classes/Connection/SearchResultInterface.php b/Classes/Connection/SearchResultInterface.php index 60718cd..5d9eb83 100644 --- a/Classes/Connection/SearchResultInterface.php +++ b/Classes/Connection/SearchResultInterface.php @@ -39,6 +39,13 @@ interface SearchResultInterface extends \Iterator, \Countable, QueryResultInterf */ public function getFacets(); + /** + * Return all suggests, if any. + * + * @return array + */ + public function getSuggests(); + /** * Returns the number of results in current result * diff --git a/Classes/Connection/SuggestInterface.php b/Classes/Connection/SuggestInterface.php new file mode 100644 index 0000000..0e834e8 --- /dev/null +++ b/Classes/Connection/SuggestInterface.php @@ -0,0 +1,46 @@ + + * + * 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 suggest, e.g. autocomplete. + */ +interface SuggestInterface +{ + /** + * @return string + */ + public function getName(); + + /** + * Returns all possible options for this suggest. + * + * @return array + */ + public function getOptions(); + + /** + * Returns all possible options unique for this suggest. + * + * @return array + */ + public function getUniqueOptions(); +} diff --git a/Classes/Connection/SuggestOptionInterface.php b/Classes/Connection/SuggestOptionInterface.php new file mode 100644 index 0000000..4d15621 --- /dev/null +++ b/Classes/Connection/SuggestOptionInterface.php @@ -0,0 +1,41 @@ + + * + * 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 suggest. + */ +interface SuggestOptionInterface +{ + /** + * Returns the text of this suggest option. + * + * @return string + */ + public function getText(); + + /** + * Returns the text of this suggest option. + * + * @return string + */ + public function __toString(); +} diff --git a/Classes/Connection/SuggestRequestInterface.php b/Classes/Connection/SuggestRequestInterface.php new file mode 100644 index 0000000..5a24127 --- /dev/null +++ b/Classes/Connection/SuggestRequestInterface.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 suggest from connection. + */ +interface SuggestRequestInterface +{ + /** + * The identifier of the suggest, used as key in arrays and to get the + * suggest from search request, etc. + * + * @return string + */ + public function getIdentifier(); + + /** + * The field to use for suggest building. + * + * @return string + */ + public function getField(); +} diff --git a/Classes/Controller/SearchController.php b/Classes/Controller/SearchController.php index 0fa7f73..e484fcd 100644 --- a/Classes/Controller/SearchController.php +++ b/Classes/Controller/SearchController.php @@ -56,6 +56,12 @@ class SearchController extends ActionController ] )); } + + if ($this->arguments->hasArgument('searchRequest')) { + $this->arguments->getArgument('searchRequest')->getPropertyMappingConfiguration() + ->allowAllProperties() + ; + } } /** diff --git a/Classes/DataProcessing/CopyToProcessor.php b/Classes/DataProcessing/CopyToProcessor.php index a0b41de..52cee21 100644 --- a/Classes/DataProcessing/CopyToProcessor.php +++ b/Classes/DataProcessing/CopyToProcessor.php @@ -21,6 +21,7 @@ namespace Codappix\SearchCore\DataProcessing; */ use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Extbase\Utility\ArrayUtility; /** * Copies values from one field to another one. @@ -43,7 +44,7 @@ class CopyToProcessor implements ProcessorInterface $this->addArray($result, $record); $result = array_filter($result); - $record[$configuration['to']] = implode(PHP_EOL, $result); + $record = ArrayUtility::setValueByPath($record, $configuration['to'], implode(PHP_EOL, $result)); return $record; } diff --git a/Classes/DataProcessing/ExplodeProcessor.php b/Classes/DataProcessing/ExplodeProcessor.php new file mode 100644 index 0000000..6e00569 --- /dev/null +++ b/Classes/DataProcessing/ExplodeProcessor.php @@ -0,0 +1,51 @@ + + * + * 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 TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Executes TYPO3's explode function on a certain field. + */ +class ExplodeProcessor implements ProcessorInterface +{ + public function processRecord(array $record, array $configuration) + { + if (!isset($configuration['delimiter'])) { + $configuration['delimiter'] = ' '; + } + + if (is_array($record[$configuration['field']])) { + $record[$configuration['field']] = implode( + $configuration['delimiter'], + $record[$configuration['field']] + ); + } + + $record[$configuration['field']] = GeneralUtility::trimExplode( + $configuration['delimiter'], + $record[$configuration['field']], + true + ); + + return $record; + } +} diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index 39e477c..b8aebe1 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -23,6 +23,7 @@ namespace Codappix\SearchCore\Domain\Model; use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\FacetRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; +use Codappix\SearchCore\Connection\SuggestRequestInterface; /** * Represents a search request used to process an actual search. @@ -46,6 +47,11 @@ class SearchRequest implements SearchRequestInterface */ protected $facets = []; + /** + * @var array + */ + protected $suggests = []; + /** * @var int */ @@ -131,6 +137,26 @@ class SearchRequest implements SearchRequestInterface return $this->facets; } + /** + * Add a suggest to gather in this search request. + * + * @param SuggestRequestInterface $suggest + */ + public function addSuggest(SuggestRequestInterface $suggest) + { + $this->suggests[$suggest->getIdentifier()] = $suggest; + } + + /** + * Returns all configured suggests to fetch in this search request. + * + * @return array + */ + public function getSuggests() + { + return $this->suggests; + } + /** * Define connection to use for this request. * Necessary to allow implementation of execute for interface. diff --git a/Classes/Domain/Model/SuggestRequest.php b/Classes/Domain/Model/SuggestRequest.php new file mode 100644 index 0000000..4ee1047 --- /dev/null +++ b/Classes/Domain/Model/SuggestRequest.php @@ -0,0 +1,66 @@ + + * + * 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 Codappix\SearchCore\Connection\SuggestRequestInterface; + +class SuggestRequest implements SuggestRequestInterface +{ + /** + * @var string + */ + protected $identifier = ''; + + /** + * @var string + */ + protected $field = ''; + + /** + * TODO: Add validation / exception? + * As the suggests come from configuration this might be a good idea to + * help integrators find issues. + * + * @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/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php index c4b66c9..a19cc35 100644 --- a/Classes/Domain/Search/QueryFactory.php +++ b/Classes/Domain/Search/QueryFactory.php @@ -78,6 +78,7 @@ class QueryFactory $this->addBoosts($searchRequest, $query); $this->addFilter($searchRequest, $query); $this->addFacets($searchRequest, $query); + $this->addSuggest($searchRequest, $query); // Use last, as it might change structure of query. // Better approach would be something like DQL to generate query and build result in the end. @@ -222,4 +223,26 @@ class QueryFactory ]); } } + + /** + * @param SearchRequestInterface $searchRequest + * @param array $query + */ + protected function addSuggest(SearchRequestInterface $searchRequest, array &$query) + { + foreach ($searchRequest->getSuggests() as $suggest) { + $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ + 'suggest' => [ + 'suggest' => [ + $suggest->getIdentifier() => [ + 'prefix' => $searchRequest->getSearchTerm(), + 'completion' => [ + 'field' => $suggest->getField(), + ], + ], + ], + ], + ]); + } + } } diff --git a/Classes/Domain/Search/SearchService.php b/Classes/Domain/Search/SearchService.php index 384f6aa..f5c13d6 100644 --- a/Classes/Domain/Search/SearchService.php +++ b/Classes/Domain/Search/SearchService.php @@ -26,6 +26,7 @@ use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchResultInterface; use Codappix\SearchCore\Domain\Model\FacetRequest; +use Codappix\SearchCore\Domain\Model\SuggestRequest; use TYPO3\CMS\Extbase\Object\ObjectManagerInterface; /** @@ -72,6 +73,7 @@ class SearchService $searchRequest->setConnection($this->connection); $this->addSize($searchRequest); $this->addConfiguredFacets($searchRequest); + $this->addConfiguredSuggests($searchRequest); $this->addConfiguredFilters($searchRequest); return $this->connection->search($searchRequest); @@ -115,6 +117,32 @@ class SearchService } } + /** + * Add suggests from configuration to request. + * + * @param SearchRequestInterface $searchRequest + */ + protected function addConfiguredSuggests(SearchRequestInterface $searchRequest) + { + $suggestsConfig = $this->configuration->getIfExists('searching.suggests'); + if ($suggestsConfig === null) { + return; + } + + foreach ($suggestsConfig as $identifier => $suggestConfig) { + if (!isset($suggestConfig['field']) || trim($suggestConfig['field']) === '') { + // TODO: Finish throw + throw new \Exception('message', 1499171142); + } + + $searchRequest->addSuggest($this->objectManager->get( + SuggestRequest::class, + $identifier, + $suggestConfig['field'] + )); + } + } + /** * Add filters from configuration, e.g. flexform or TypoScript. *