FEATURE: Add filter to search

* Allow to filter results by field values.
This commit is contained in:
Daniel Siepmann 2017-06-06 13:53:08 +02:00
parent 3a2523e1d2
commit 1a41c5e237
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
11 changed files with 323 additions and 26 deletions

View file

@ -53,7 +53,9 @@ services:
install: make install install: make install
script: make functionalTests script:
- make unitTests
- make functionalTests
after_script: after_script:
- make uploadCodeCoverage - make uploadCodeCoverage

View file

@ -21,6 +21,7 @@ namespace Leonmrni\SearchCore\Connection;
*/ */
use TYPO3\CMS\Core\SingletonInterface as Singleton; use TYPO3\CMS\Core\SingletonInterface as Singleton;
use Leonmrni\SearchCore\Domain\Search\QueryFactory;
/** /**
* Outer wrapper to elasticsearch. * Outer wrapper to elasticsearch.
@ -47,6 +48,11 @@ class Elasticsearch implements Singleton, ConnectionInterface
*/ */
protected $documentFactory; protected $documentFactory;
/**
* @var QueryFactory
*/
protected $queryFactory;
/** /**
* @var \TYPO3\CMS\Core\Log\Logger * @var \TYPO3\CMS\Core\Log\Logger
*/ */
@ -67,17 +73,20 @@ class Elasticsearch implements Singleton, ConnectionInterface
* @param Elasticsearch\IndexFactory $indexFactory * @param Elasticsearch\IndexFactory $indexFactory
* @param Elasticsearch\TypeFactory $typeFactory * @param Elasticsearch\TypeFactory $typeFactory
* @param Elasticsearch\DocumentFactory $documentFactory * @param Elasticsearch\DocumentFactory $documentFactory
* @param QueryFactory $queryFactory
*/ */
public function __construct( public function __construct(
Elasticsearch\Connection $connection, Elasticsearch\Connection $connection,
Elasticsearch\IndexFactory $indexFactory, Elasticsearch\IndexFactory $indexFactory,
Elasticsearch\TypeFactory $typeFactory, Elasticsearch\TypeFactory $typeFactory,
Elasticsearch\DocumentFactory $documentFactory Elasticsearch\DocumentFactory $documentFactory,
QueryFactory $queryFactory
) { ) {
$this->connection = $connection; $this->connection = $connection;
$this->indexFactory = $indexFactory; $this->indexFactory = $indexFactory;
$this->typeFactory = $typeFactory; $this->typeFactory = $typeFactory;
$this->documentFactory = $documentFactory; $this->documentFactory = $documentFactory;
$this->queryFactory = $queryFactory;
} }
public function addDocument($documentType, array $document) public function addDocument($documentType, array $document)
@ -146,32 +155,10 @@ class Elasticsearch implements Singleton, ConnectionInterface
{ {
$this->logger->debug('Search for', [$searchRequest->getSearchTerm()]); $this->logger->debug('Search for', [$searchRequest->getSearchTerm()]);
$query = [
'bool' => [
'must' => [
[
'match' => [
'_all' => $searchRequest->getSearchTerm()
],
],
],
],
];
if ($searchRequest->hasFilter()) {
$queryFilter = [];
foreach ($searchRequest->getFilter() as $field => $value) {
$queryFilter[$field] = $value;
}
$query['bool']['filter'] = [
'term' => $queryFilter,
];
}
$search = new \Elastica\Search($this->connection->getClient()); $search = new \Elastica\Search($this->connection->getClient());
$search->addIndex('typo3content'); $search->addIndex('typo3content');
$search->setQuery(new \Elastica\Query(['query' => $query])); $search->setQuery($this->queryFactory->create($this, $searchRequest));
// TODO: Return wrapped result to implement our interface. // TODO: Return wrapped result to implement our interface.
// Also update php doc to reflect the change. // Also update php doc to reflect the change.
return $search->search(); return $search->search();

View file

@ -31,4 +31,14 @@ interface SearchRequestInterface
* @return string * @return string
*/ */
public function getSearchTerm(); public function getSearchTerm();
/**
* @return bool
*/
public function hasFilter();
/**
* @return array
*/
public function getFilter();
} }

View file

@ -0,0 +1,72 @@
<?php
namespace Leonmrni\SearchCore\Domain\Search;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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()
],
],
],
],
];
$queryFilter = [];
if ($searchRequest->hasFilter()) {
foreach ($searchRequest->getFilter() as $field => $value) {
$queryFilter[$field] = $value;
}
$query['bool']['filter'] = [
'term' => $queryFilter,
];
}
return new \Elastica\Query(['query' => $query]);
}
}

View file

@ -23,6 +23,11 @@ functionalTests:
.Build/bin/phpunit --colors --debug -v \ .Build/bin/phpunit --colors --debug -v \
-c Tests/Functional/FunctionalTests.xml -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 uploadCodeCoverage: uploadCodeCoverageToScrutinizer uploadCodeCoverageToCodacy
uploadCodeCoverageToScrutinizer: uploadCodeCoverageToScrutinizer:

View file

@ -43,11 +43,18 @@ abstract class AbstractFunctionalTestCase extends BaseFunctionalTestCase
'host' => getenv('ES_HOST') ?: \Elastica\Connection::DEFAULT_HOST, 'host' => getenv('ES_HOST') ?: \Elastica\Connection::DEFAULT_HOST,
'port' => getenv('ES_PORT') ?: \Elastica\Connection::DEFAULT_PORT, 'port' => getenv('ES_PORT') ?: \Elastica\Connection::DEFAULT_PORT,
]); ]);
$this->cleanUp();
} }
public function tearDown() public function tearDown()
{ {
// Delete everything so next test starts clean. // Delete everything so next test starts clean.
$this->cleanUp();
}
protected function cleanUp()
{
$this->client->getIndex('_all')->delete(); $this->client->getIndex('_all')->delete();
$this->client->getIndex('_all')->clearCache(); $this->client->getIndex('_all')->clearCache();
} }

View file

@ -0,0 +1,61 @@
<?php
namespace Leonmrni\SearchCore\Tests\Functional\Connection\Elasticsearch;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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.');
}
}

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<tt_content>
<uid>5</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>html</CType>
<header>indexed content element with html ctype</header>
<bodytext>Search Word</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<filelink_sorting>0</filelink_sorting>
</tt_content>
<tt_content>
<uid>6</uid>
<pid>1</pid>
<tstamp>1480686370</tstamp>
<crdate>1480686370</crdate>
<hidden>0</hidden>
<sorting>72</sorting>
<CType>header</CType>
<header>indexed content element with header ctype</header>
<bodytext>Search Word</bodytext>
<media>0</media>
<layout>0</layout>
<deleted>0</deleted>
<cols>0</cols>
<starttime>0</starttime>
<endtime>0</endtime>
<colPos>0</colPos>
<filelink_sorting>0</filelink_sorting>
</tt_content>
</dataset>

View file

@ -0,0 +1,27 @@
<?php
namespace Leonmrni\SearchCore\Tests\Unit;
/*
* Copyright (C) 2016 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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
{
}

View file

@ -0,0 +1,56 @@
<?php
namespace Leonmrni\SearchCore\Tests\Unit\Domain\Search;
/*
* Copyright (C) 2017 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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.'
);
}
}

28
Tests/Unit/UnitTests.xml Normal file
View file

@ -0,0 +1,28 @@
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../.Build/vendor/typo3/cms/typo3/sysext/core/Build/UnitTestsBootstrap.php"
colors="true"
convertErrorsToExceptions="false"
convertWarningsToExceptions="false"
forceCoversAnnotation="false"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
verbose="false">
<testsuites>
<testsuite name="unit-tests">
<directory>.</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">../../Classes</directory>
</whitelist>
</filter>
</phpunit>