Merge branch 'feature/filter' into feature/restructure-configuration

This commit is contained in:
Daniel Siepmann 2017-06-29 08:45:36 +02:00
commit 4c7bc8b9f5
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
17 changed files with 469 additions and 6 deletions

View file

@ -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 language: php
php: php:
@ -40,11 +49,12 @@ matrix:
services: services:
- mysql - mysql
- elasticsearch
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)
@ -148,10 +157,11 @@ class Elasticsearch implements Singleton, ConnectionInterface
$search = new \Elastica\Search($this->connection->getClient()); $search = new \Elastica\Search($this->connection->getClient());
$search->addIndex('typo3content'); $search->addIndex('typo3content');
$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('"' . $searchRequest->getSearchTerm() . '"'); 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

@ -32,14 +32,19 @@ class SearchRequest implements SearchRequestInterface
* *
* @var string * @var string
*/ */
protected $query; protected $query = '';
/**
* @var array
*/
protected $filter = [];
/** /**
* @param string $query * @param string $query
*/ */
public function __construct($query) public function __construct($query)
{ {
$this->query = $query; $this->query = (string) $query;
} }
/** /**
@ -57,4 +62,28 @@ class SearchRequest implements SearchRequestInterface
{ {
return $this->query; 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;
}
} }

View file

@ -0,0 +1,66 @@
<?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()
],
],
],
],
];
if ($searchRequest->hasFilter()) {
$query['bool']['filter'] = ['term' => $searchRequest->getFilter()];
}
return new \Elastica\Query(['query' => $query]);
}
}

0
Configuration/TypoScript/constants.txt Executable file → Normal file
View file

0
Configuration/TypoScript/setup.txt Executable file → Normal file
View file

View file

@ -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.

View file

@ -7,6 +7,7 @@ Table of Contents
:maxdepth: 1 :maxdepth: 1
:glob: :glob:
features
installation installation
configuration configuration
usage usage

View file

@ -37,3 +37,30 @@ Searching / Frontend Plugin
To provide a search interface you can insert the frontend Plugin as normal content element of type To provide a search interface you can insert the frontend Plugin as normal content element of type
plugin. The plugin is named *Search Core*. 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
<f:form name="searchRequest" object="{searchRequest}">
<f:form.textfield property="query" />
<f:form.submit value="search" />
</f:form>
.. _usage_searching_filter:
Filter
""""""
Thanks to extbase mapping, filter are added to the form:
.. code-block:: html
:emphasize-lines: 3
<f:form name="searchRequest" object="{searchRequest}">
<f:form.textfield property="query" />
<f:form.textfield property="filter.exampleName" value="the value to match" />
<f:form.submit value="search" />
</f:form>

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,99 @@
<?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.'
);
}
/**
* @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.'
);
}
}

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>