diff --git a/.travis.yml b/.travis.yml index 79bf8f9..3253c3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,12 @@ +sudo: true + +addons: + apt: + packages: + - oracle-java8-set-default +before_install: + - curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.0.deb && sudo dpkg -i --force-confnew elasticsearch-5.2.0.deb && sudo service elasticsearch start + language: php php: @@ -40,11 +49,12 @@ matrix: services: - mysql - - elasticsearch install: make install -script: make functionalTests +script: + - make unitTests + - make functionalTests after_script: - make uploadCodeCoverage diff --git a/Classes/Connection/Elasticsearch.php b/Classes/Connection/Elasticsearch.php index 560ca07..386e19c 100644 --- a/Classes/Connection/Elasticsearch.php +++ b/Classes/Connection/Elasticsearch.php @@ -21,6 +21,7 @@ namespace Leonmrni\SearchCore\Connection; */ use TYPO3\CMS\Core\SingletonInterface as Singleton; +use Leonmrni\SearchCore\Domain\Search\QueryFactory; /** * Outer wrapper to elasticsearch. @@ -47,6 +48,11 @@ class Elasticsearch implements Singleton, ConnectionInterface */ protected $documentFactory; + /** + * @var QueryFactory + */ + protected $queryFactory; + /** * @var \TYPO3\CMS\Core\Log\Logger */ @@ -67,17 +73,20 @@ class Elasticsearch implements Singleton, ConnectionInterface * @param Elasticsearch\IndexFactory $indexFactory * @param Elasticsearch\TypeFactory $typeFactory * @param Elasticsearch\DocumentFactory $documentFactory + * @param QueryFactory $queryFactory */ public function __construct( Elasticsearch\Connection $connection, Elasticsearch\IndexFactory $indexFactory, Elasticsearch\TypeFactory $typeFactory, - Elasticsearch\DocumentFactory $documentFactory + Elasticsearch\DocumentFactory $documentFactory, + QueryFactory $queryFactory ) { $this->connection = $connection; $this->indexFactory = $indexFactory; $this->typeFactory = $typeFactory; $this->documentFactory = $documentFactory; + $this->queryFactory = $queryFactory; } public function addDocument($documentType, array $document) @@ -148,10 +157,11 @@ class Elasticsearch implements Singleton, ConnectionInterface $search = new \Elastica\Search($this->connection->getClient()); $search->addIndex('typo3content'); + $search->setQuery($this->queryFactory->create($this, $searchRequest)); // TODO: Return wrapped result to implement our interface. // Also update php doc to reflect the change. - return $search->search('"' . $searchRequest->getSearchTerm() . '"'); + return $search->search(); } /** diff --git a/Classes/Connection/SearchRequestInterface.php b/Classes/Connection/SearchRequestInterface.php index ecf7a74..603c02f 100644 --- a/Classes/Connection/SearchRequestInterface.php +++ b/Classes/Connection/SearchRequestInterface.php @@ -31,4 +31,14 @@ interface SearchRequestInterface * @return string */ public function getSearchTerm(); + + /** + * @return bool + */ + public function hasFilter(); + + /** + * @return array + */ + public function getFilter(); } diff --git a/Classes/Domain/Model/SearchRequest.php b/Classes/Domain/Model/SearchRequest.php index 1de2f71..f1ad18e 100644 --- a/Classes/Domain/Model/SearchRequest.php +++ b/Classes/Domain/Model/SearchRequest.php @@ -32,14 +32,19 @@ class SearchRequest implements SearchRequestInterface * * @var string */ - protected $query; + protected $query = ''; + + /** + * @var array + */ + protected $filter = []; /** * @param string $query */ public function __construct($query) { - $this->query = $query; + $this->query = (string) $query; } /** @@ -57,4 +62,28 @@ class SearchRequest implements SearchRequestInterface { return $this->query; } + + /** + * @param array $filter + */ + public function setFilter(array $filter) + { + $this->filter = array_map('strval', $filter); + } + + /** + * @return bool + */ + public function hasFilter() + { + return count($this->filter) > 0; + } + + /** + * @return array + */ + public function getFilter() + { + return $this->filter; + } } diff --git a/Classes/Domain/Search/QueryFactory.php b/Classes/Domain/Search/QueryFactory.php new file mode 100644 index 0000000..31c50df --- /dev/null +++ b/Classes/Domain/Search/QueryFactory.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 Leonmrni\SearchCore\Connection\ConnectionInterface; +use Leonmrni\SearchCore\Connection\Elasticsearch\Query; +use Leonmrni\SearchCore\Connection\SearchRequestInterface; + +class QueryFactory +{ + /** + * @param ConnectionInterface $connection + * @param SearchRequestInterface $searchRequest + * + * @return \Elastica\Query + */ + public function create( + ConnectionInterface $connection, + SearchRequestInterface $searchRequest + ) { + return $this->createElasticaQuery($searchRequest); + } + + /** + * @param SearchRequestInterface $searchRequest + * @return \Elastica\Query + */ + protected function createElasticaQuery(SearchRequestInterface $searchRequest) + { + $query = [ + 'bool' => [ + 'must' => [ + [ + 'match' => [ + '_all' => $searchRequest->getSearchTerm() + ], + ], + ], + ], + ]; + + if ($searchRequest->hasFilter()) { + $query['bool']['filter'] = ['term' => $searchRequest->getFilter()]; + } + + return new \Elastica\Query(['query' => $query]); + } +} diff --git a/Configuration/TypoScript/constants.txt b/Configuration/TypoScript/constants.txt old mode 100755 new mode 100644 diff --git a/Configuration/TypoScript/setup.txt b/Configuration/TypoScript/setup.txt old mode 100755 new mode 100644 diff --git a/Documentation/source/features.rst b/Documentation/source/features.rst new file mode 100644 index 0000000..c715512 --- /dev/null +++ b/Documentation/source/features.rst @@ -0,0 +1,41 @@ +.. _features: + +Features +======== + +The following features are currently provided: + +.. _features_indexing: + +Indexing +-------- + +Indexing data to Elasticsearch is provided. The extension delivers an indexer for TCA with zero +configuration needs. Still it's possible to configure the indexer. + +Own indexer are not possible yet, but will. + +.. _features_search: + +Searching +--------- + +Currently all fields are searched for a single search input. + +Also multiple filter are supported. Filtering results by fields for string contents. + +.. _features_planned: + +Planned +--------- + +The following features are currently planned and will be integrated: + +#. Mapping Configuration + Allowing to configure the whole mapping, to define type of input, e.g. integer, keyword. + + +#. Facets / Aggregates + Based on the mapping configuration, facets will be configurable and fetched. Therefore mapping is + required and we will adjust the result set to be of a custom model providing all information in a + more clean way. diff --git a/Documentation/source/index.rst b/Documentation/source/index.rst index dfda13b..5427fc9 100644 --- a/Documentation/source/index.rst +++ b/Documentation/source/index.rst @@ -7,6 +7,7 @@ Table of Contents :maxdepth: 1 :glob: + features installation configuration usage diff --git a/Documentation/source/usage.rst b/Documentation/source/usage.rst index 1fc0e51..453b17d 100644 --- a/Documentation/source/usage.rst +++ b/Documentation/source/usage.rst @@ -37,3 +37,30 @@ Searching / Frontend Plugin To provide a search interface you can insert the frontend Plugin as normal content element of type plugin. The plugin is named *Search Core*. + +Please provide your own template, the extension will not deliver a useful template for now. + +The extbase mapping is used, this way you can create a form: + +.. code-block:: html + + + + + + +.. _usage_searching_filter: + +Filter +"""""" + +Thanks to extbase mapping, filter are added to the form: + +.. code-block:: html + :emphasize-lines: 3 + + + + + + diff --git a/Makefile b/Makefile index 589dd7d..a84afea 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,11 @@ functionalTests: .Build/bin/phpunit --colors --debug -v \ -c Tests/Functional/FunctionalTests.xml +unitTests: + TYPO3_PATH_WEB=$(TYPO3_WEB_DIR) \ + .Build/bin/phpunit --colors --debug -v \ + -c Tests/Unit/UnitTests.xml + uploadCodeCoverage: uploadCodeCoverageToScrutinizer uploadCodeCoverageToCodacy uploadCodeCoverageToScrutinizer: diff --git a/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php b/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php index cdd8ddf..363ad6d 100644 --- a/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php +++ b/Tests/Functional/Connection/Elasticsearch/AbstractFunctionalTestCase.php @@ -43,11 +43,18 @@ abstract class AbstractFunctionalTestCase extends BaseFunctionalTestCase 'host' => getenv('ES_HOST') ?: \Elastica\Connection::DEFAULT_HOST, 'port' => getenv('ES_PORT') ?: \Elastica\Connection::DEFAULT_PORT, ]); + + $this->cleanUp(); } public function tearDown() { // Delete everything so next test starts clean. + $this->cleanUp(); + } + + protected function cleanUp() + { $this->client->getIndex('_all')->delete(); $this->client->getIndex('_all')->clearCache(); } diff --git a/Tests/Functional/Connection/Elasticsearch/FilterTest.php b/Tests/Functional/Connection/Elasticsearch/FilterTest.php new file mode 100644 index 0000000..88ae37f --- /dev/null +++ b/Tests/Functional/Connection/Elasticsearch/FilterTest.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\Domain\Index\IndexerFactory; +use Leonmrni\SearchCore\Domain\Model\SearchRequest; +use Leonmrni\SearchCore\Domain\Search\SearchService; +use TYPO3\CMS\Extbase\Object\ObjectManager; + +class FilterTest extends AbstractFunctionalTestCase +{ + protected function getDataSets() + { + return array_merge( + parent::getDataSets(), + ['Tests/Functional/Fixtures/Searching/Filter.xml'] + ); + } + + /** + * @test + */ + public function itsPossibleToFilterResultsByASingleField() + { + \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class) + ->get(IndexerFactory::class) + ->getIndexer('tt_content') + ->indexAllDocuments() + ; + + $searchService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(ObjectManager::class) + ->get(SearchService::class); + $searchRequest = new SearchRequest('Search Word'); + + $result = $searchService->search($searchRequest); + $this->assertSame(2, count($result), 'Did not receive both indexed elements without filter.'); + + $searchRequest->setFilter(['CType' => 'html']); + $result = $searchService->search($searchRequest); + $this->assertSame('5', $result[0]->getData()['uid'], 'Did not get the expected result entry.'); + $this->assertSame(1, count($result), 'Did not receive the single filtered element.'); + } +} diff --git a/Tests/Functional/Fixtures/Searching/Filter.xml b/Tests/Functional/Fixtures/Searching/Filter.xml new file mode 100644 index 0000000..103a4da --- /dev/null +++ b/Tests/Functional/Fixtures/Searching/Filter.xml @@ -0,0 +1,42 @@ + + + + 5 + 1 + 1480686370 + 1480686370 + 0 + 72 + html +
indexed content element with html ctype
+ Search Word + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +
+ + + 6 + 1 + 1480686370 + 1480686370 + 0 + 72 + header +
indexed content element with header ctype
+ Search Word + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 +
+
diff --git a/Tests/Unit/AbstractUnitTestCase.php b/Tests/Unit/AbstractUnitTestCase.php new file mode 100644 index 0000000..f3281c1 --- /dev/null +++ b/Tests/Unit/AbstractUnitTestCase.php @@ -0,0 +1,27 @@ + + * + * 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\Tests\UnitTestCase as CoreTestCase; + +abstract class AbstractUnitTestCase extends CoreTestCase +{ +} diff --git a/Tests/Unit/Domain/Search/QueryFactoryTest.php b/Tests/Unit/Domain/Search/QueryFactoryTest.php new file mode 100644 index 0000000..83f031e --- /dev/null +++ b/Tests/Unit/Domain/Search/QueryFactoryTest.php @@ -0,0 +1,99 @@ + + * + * 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; +use Leonmrni\SearchCore\Domain\Model\SearchRequest; +use Leonmrni\SearchCore\Domain\Search\QueryFactory; +use Leonmrni\SearchCore\Tests\Unit\AbstractUnitTestCase; + +class QueryFactoryTest extends AbstractUnitTestCase +{ + protected $subject; + + public function setUp() + { + parent::setUp(); + + $this->subject = new QueryFactory; + } + + /** + * @test + */ + public function creatonOfQueryWorksInGeneral() + { + $connection = $this->getMockBuilder(Connection\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + $searchRequest = new SearchRequest('SearchWord'); + + $query = $this->subject->create($connection, $searchRequest); + $this->assertInstanceOf( + \Elastica\Query::class, + $query, + 'Factory did not create the expected instance.' + ); + } + + /** + * @test + */ + public function filterIsAddedToQuery() + { + $connection = $this->getMockBuilder(Connection\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + $searchRequest = new SearchRequest('SearchWord'); + $searchRequest->setFilter(['field' => 'content']); + + $query = $this->subject->create($connection, $searchRequest); + $this->assertSame( + ['field' => 'content'], + $query->toArray()['query']['bool']['filter']['term'], + 'Filter was not added to query.' + ); + } + + /** + * @test + */ + public function userInputIsAlwaysString() + { + $connection = $this->getMockBuilder(Connection\Elasticsearch::class) + ->disableOriginalConstructor() + ->getMock(); + $searchRequest = new SearchRequest(10); + $searchRequest->setFilter(['field' => 20]); + + $query = $this->subject->create($connection, $searchRequest); + $this->assertSame( + '10', + $query->toArray()['query']['bool']['must'][0]['match']['_all'], + 'Search word was not escaped as expected.' + ); + $this->assertSame( + '20', + $query->toArray()['query']['bool']['filter']['term']['field'], + 'Search word was not escaped as expected.' + ); + } +} diff --git a/Tests/Unit/UnitTests.xml b/Tests/Unit/UnitTests.xml new file mode 100644 index 0000000..6456405 --- /dev/null +++ b/Tests/Unit/UnitTests.xml @@ -0,0 +1,28 @@ + + + + + . + + + + + + ../../Classes + + +