Merge pull request #92 from Codappix/feature/geo-search

Feature: Support Geo search
This commit is contained in:
Daniel Siepmann 2017-11-02 22:41:00 +01:00 committed by GitHub
commit 87298d8e58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1087 additions and 226 deletions

View file

@ -0,0 +1,69 @@
<?php
namespace Codappix\SearchCore\Configuration;
/*
* 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 Codappix\SearchCore\Connection\SearchRequestInterface;
use TYPO3\CMS\Fluid\View\StandaloneView;
class ConfigurationUtility
{
/**
* Will parse all entries, recursive as fluid template, with request variable set to $searchRequest.
*/
public function replaceArrayValuesWithRequestContent(SearchRequestInterface $searchRequest, array $array) : array
{
array_walk_recursive($array, function (&$value, $key, SearchRequestInterface $searchRequest) {
$template = new StandaloneView();
$template->assign('request', $searchRequest);
$template->setTemplateSource($value);
$value = $template->render();
// As elasticsearch does need some doubles to be send as doubles.
if (is_numeric($value)) {
$value = (float) $value;
}
}, $searchRequest);
return $array;
}
/**
* Will check all entries, whether they have a condition and filter entries out, where condition is false.
* Also will remove condition in the end.
*/
public function filterByCondition(array $entries) : array
{
$entries = array_filter($entries, function ($entry) {
return !is_array($entry)
|| !array_key_exists('condition', $entry)
|| (bool) $entry['condition'] === true
;
});
foreach ($entries as $key => $entry) {
if (is_array($entry) && array_key_exists('condition', $entry)) {
unset($entries[$key]['condition']);
}
}
return $entries;
}
}

View file

@ -25,7 +25,7 @@ namespace Codappix\SearchCore\DataProcessing;
*/ */
class CopyToProcessor implements ProcessorInterface class CopyToProcessor implements ProcessorInterface
{ {
public function processRecord(array $record, array $configuration) public function processRecord(array $record, array $configuration) : array
{ {
$all = []; $all = [];
@ -36,10 +36,6 @@ class CopyToProcessor implements ProcessorInterface
return $record; return $record;
} }
/**
* @param array &$target
* @param array $from
*/
protected function addArray(array &$target, array $from) protected function addArray(array &$target, array $from)
{ {
foreach ($from as $value) { foreach ($from as $value) {

View file

@ -0,0 +1,59 @@
<?php
namespace Codappix\SearchCore\DataProcessing;
/*
* 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.
*/
/**
* Adds a new fields, ready to use as GeoPoint field for Elasticsearch.
*/
class GeoPointProcessor implements ProcessorInterface
{
public function processRecord(array $record, array $configuration) : array
{
if (! $this->canApply($record, $configuration)) {
return $record;
}
$record[$configuration['to']] = [
'lat' => (float) $record[$configuration['lat']],
'lon' => (float) $record[$configuration['lon']],
];
return $record;
}
protected function canApply(array $record, array $configuration) : bool
{
if (!isset($record[$configuration['lat']])
|| !is_numeric($record[$configuration['lat']])
|| trim($record[$configuration['lat']]) === ''
) {
return false;
}
if (!isset($record[$configuration['lon']])
|| !is_numeric($record[$configuration['lon']])
|| trim($record[$configuration['lon']]) === ''
) {
return false;
}
return true;
}
}

View file

@ -29,11 +29,6 @@ interface ProcessorInterface
/** /**
* Processes the given record. * Processes the given record.
* Also retrieves the configuration for this processor instance. * Also retrieves the configuration for this processor instance.
*
* @param array $record
* @param array $configuration
*
* @return array
*/ */
public function processRecord(array $record, array $configuration); public function processRecord(array $record, array $configuration) : array;
} }

View file

@ -92,7 +92,8 @@ class SearchRequest implements SearchRequestInterface
*/ */
public function setFilter(array $filter) public function setFilter(array $filter)
{ {
$this->filter = array_filter(array_map('strval', $filter)); $filter = \TYPO3\CMS\Core\Utility\ArrayUtility::removeArrayEntryByValue($filter, '');
$this->filter = \TYPO3\CMS\Extbase\Utility\ArrayUtility::removeEmptyElementsRecursively($filter);
} }
/** /**

View file

@ -21,10 +21,12 @@ namespace Codappix\SearchCore\Domain\Search;
*/ */
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\ConfigurationUtility;
use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Connection\ConnectionInterface; use Codappix\SearchCore\Connection\ConnectionInterface;
use Codappix\SearchCore\Connection\Elasticsearch\Query; use Codappix\SearchCore\Connection\Elasticsearch\Query;
use Codappix\SearchCore\Connection\SearchRequestInterface; use Codappix\SearchCore\Connection\SearchRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\ArrayUtility; use TYPO3\CMS\Extbase\Utility\ArrayUtility;
class QueryFactory class QueryFactory
@ -40,37 +42,31 @@ class QueryFactory
protected $configuration; protected $configuration;
/** /**
* @param \TYPO3\CMS\Core\Log\LogManager $logManager * @var ConfigurationUtility
* @param ConfigurationContainerInterface $configuration
*/ */
protected $configurationUtility;
public function __construct( public function __construct(
\TYPO3\CMS\Core\Log\LogManager $logManager, \TYPO3\CMS\Core\Log\LogManager $logManager,
ConfigurationContainerInterface $configuration ConfigurationContainerInterface $configuration,
ConfigurationUtility $configurationUtility
) { ) {
$this->logger = $logManager->getLogger(__CLASS__); $this->logger = $logManager->getLogger(__CLASS__);
$this->configuration = $configuration; $this->configuration = $configuration;
$this->configurationUtility = $configurationUtility;
} }
/** /**
* TODO: This is not in scope Elasticsearch, therefore it should not return * TODO: This is not in scope Elasticsearch, therefore it should not return
* \Elastica\Query, but decide to use a more specific QueryFactory like * \Elastica\Query, but decide to use a more specific QueryFactory like
* ElasticaQueryFactory, once the second query is added? * ElasticaQueryFactory, once the second query is added?
*
* @param SearchRequestInterface $searchRequest
*
* @return \Elastica\Query
*/ */
public function create(SearchRequestInterface $searchRequest) public function create(SearchRequestInterface $searchRequest) : \Elastica\Query
{ {
return $this->createElasticaQuery($searchRequest); return $this->createElasticaQuery($searchRequest);
} }
/** protected function createElasticaQuery(SearchRequestInterface $searchRequest) : \Elastica\Query
* @param SearchRequestInterface $searchRequest
*
* @return \Elastica\Query
*/
protected function createElasticaQuery(SearchRequestInterface $searchRequest)
{ {
$query = []; $query = [];
$this->addSize($searchRequest, $query); $this->addSize($searchRequest, $query);
@ -78,6 +74,8 @@ class QueryFactory
$this->addBoosts($searchRequest, $query); $this->addBoosts($searchRequest, $query);
$this->addFilter($searchRequest, $query); $this->addFilter($searchRequest, $query);
$this->addFacets($searchRequest, $query); $this->addFacets($searchRequest, $query);
$this->addFields($searchRequest, $query);
$this->addSort($searchRequest, $query);
// Use last, as it might change structure of 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. // Better approach would be something like DQL to generate query and build result in the end.
@ -87,10 +85,6 @@ class QueryFactory
return new \Elastica\Query($query); return new \Elastica\Query($query);
} }
/**
* @param SearchRequestInterface $searchRequest
* @param array $query
*/
protected function addSize(SearchRequestInterface $searchRequest, array &$query) protected function addSize(SearchRequestInterface $searchRequest, array &$query)
{ {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
@ -99,10 +93,6 @@ class QueryFactory
]); ]);
} }
/**
* @param SearchRequestInterface $searchRequest
* @param array $query
*/
protected function addSearch(SearchRequestInterface $searchRequest, array &$query) protected function addSearch(SearchRequestInterface $searchRequest, array &$query)
{ {
if (trim($searchRequest->getSearchTerm()) === '') { if (trim($searchRequest->getSearchTerm()) === '') {
@ -125,10 +115,6 @@ class QueryFactory
} }
} }
/**
* @param SearchRequestInterface $searchRequest
* @param array $query
*/
protected function addBoosts(SearchRequestInterface $searchRequest, array &$query) protected function addBoosts(SearchRequestInterface $searchRequest, array &$query)
{ {
try { try {
@ -159,9 +145,6 @@ class QueryFactory
]); ]);
} }
/**
* @param array $query
*/
protected function addFactorBoost(array &$query) protected function addFactorBoost(array &$query)
{ {
try { try {
@ -176,38 +159,83 @@ class QueryFactory
} }
} }
/** protected function addFields(SearchRequestInterface $searchRequest, array &$query)
* @param SearchRequestInterface $searchRequest {
* @param array $query try {
*/ $query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
'stored_fields' => GeneralUtility::trimExplode(',', $this->configuration->get('searching.fields.stored_fields'), true),
]);
} catch (InvalidArgumentException $e) {
// Nothing configured
}
try {
$scriptFields = $this->configuration->get('searching.fields.script_fields');
$scriptFields = $this->configurationUtility->replaceArrayValuesWithRequestContent($searchRequest, $scriptFields);
$scriptFields = $this->configurationUtility->filterByCondition($scriptFields);
if ($scriptFields !== []) {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, ['script_fields' => $scriptFields]);
}
} catch (InvalidArgumentException $e) {
// Nothing configured
}
}
protected function addSort(SearchRequestInterface $searchRequest, array &$query)
{
$sorting = $this->configuration->getIfExists('searching.sort') ?: [];
$sorting = $this->configurationUtility->replaceArrayValuesWithRequestContent($searchRequest, $sorting);
$sorting = $this->configurationUtility->filterByCondition($sorting);
if ($sorting !== []) {
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, ['sort' => $sorting]);
}
}
protected function addFilter(SearchRequestInterface $searchRequest, array &$query) protected function addFilter(SearchRequestInterface $searchRequest, array &$query)
{ {
if (! $searchRequest->hasFilter()) { if (! $searchRequest->hasFilter()) {
return; return;
} }
$terms = []; $filter = [];
foreach ($searchRequest->getFilter() as $name => $value) { foreach ($searchRequest->getFilter() as $name => $value) {
$terms[] = [ $filter[] = $this->buildFilter(
$name,
$value,
$this->configuration->getIfExists('searching.mapping.filter.' . $name) ?: []
);
}
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [
'query' => [
'bool' => [
'filter' => $filter,
],
],
]);
}
protected function buildFilter(string $name, $value, array $config) : array
{
if ($config === []) {
return [
'term' => [ 'term' => [
$name => $value, $name => $value,
], ],
]; ];
} }
$query = ArrayUtility::arrayMergeRecursiveOverrule($query, [ $filter = [];
'query' => [
'bool' => [ if (isset($config['fields'])) {
'filter' => $terms, foreach ($config['fields'] as $elasticField => $inputField) {
], $filter[$elasticField] = $value[$inputField];
], }
]); }
return [$config['field'] => $filter];
} }
/**
* @param SearchRequestInterface $searchRequest
* @param array $query
*/
protected function addFacets(SearchRequestInterface $searchRequest, array &$query) protected function addFacets(SearchRequestInterface $searchRequest, array &$query)
{ {
foreach ($searchRequest->getFacets() as $facet) { foreach ($searchRequest->getFacets() as $facet) {

View file

@ -0,0 +1,23 @@
``Codappix\SearchCore\DataProcessing\CopyToProcessor``
======================================================
Will copy contents of fields to other fields.
Possible Options:
``to``
Defines the field to copy the values into. All values not false will be copied at the moment.
Example::
plugin.tx_searchcore.settings.indexing.tt_content.dataProcessing {
1 = Codappix\SearchCore\DataProcessing\CopyToProcessor
1 {
to = all
}
2 = Codappix\SearchCore\DataProcessing\CopyToProcessor
2 {
to = spellcheck
}
}

View file

@ -0,0 +1,27 @@
``Codappix\SearchCore\DataProcessing\GeoPointProcessor``
========================================================
Will create a new field, ready to use as GeoPoint field for Elasticsearch.
Possible Options:
``to``
Defines the field to create as GeoPoint.
``lat``
Defines the field containing the latitude.
``lon``
Defines the field containing the longitude.
Example::
plugin.tx_searchcore.settings.indexing.tt_content.dataProcessing {
1 = Codappix\SearchCore\DataProcessing\GeoPointProcessor
1 {
to = location
lat = lat
lon = lng
}
}
The above example will create a new field ``location`` as GeoPoint with latitude fetched from field
``lat`` and longitude fetched from field ``lng``.

View file

@ -175,8 +175,12 @@ dataProcessing
The following Processor are available: The following Processor are available:
``Codappix\SearchCore\DataProcessing\CopyToProcessor`` .. toctree::
Will copy contents of fields to other fields :maxdepth: 1
:glob:
dataProcessing/CopyToProcessor
dataProcessing/GeoPointProcessor
The following Processor are planned: The following Processor are planned:

View file

@ -41,8 +41,8 @@ facets
.. _filter: .. _filter:
``filter`` filter
""""""""""" ------
Used by: While building search request. Used by: While building search request.
@ -107,10 +107,92 @@ fieldValueFactor
missing = 1 missing = 1
} }
.. _mapping.filter:
mapping.filter
--------------
Allows to configure filter more in depth. If a filter with the given key exists, the TypoScript will
be added.
E.g. you submit a filter in form of:
.. code-block:: html
<f:form.textfield property="filter.distance.location.lat" value="51.168098" />
<f:form.textfield property="filter.distance.location.lon" value="6.381384" />
<f:form.textfield property="filter.distance.distance" value="100km" />
This will create a ``distance`` filter with subproperties. To make this filter actually work, you
can add the following TypoScript, which will be added to the filter::
mapping {
filter {
distance {
field = geo_distance
fields {
distance = distance
location = location
}
}
}
}
``fields`` has a special meaning here. This will actually map the properties of the filter to fields
in elasticsearch. In above example they do match, but you can also use different names in your form.
On the left hand side is the elasticsearch field name, on the right side the one submitted as a
filter.
The ``field``, in above example ``geo_distance``, will be used as the elasticsearch field for
filtering. This way you can use arbitrary filter names and map them to existing elasticsearch fields.
.. _fields:
fields
------
Defines the fields to fetch from elasticsearch. Two sub entries exist:
First ``stored_fields`` which is a list of comma separated fields which actually exist and will be
added. Typically you will use ``_source`` to fetch the whole indexed fields.
Second is ``script_fields``, which allow you to configure scripted fields for elasticsearch.
An example might look like the following::
fields {
script_fields {
distance {
condition = {request.filter.distance.location}
script {
params {
lat = {request.filter.distance.location.lat -> f:format.number()}
lon = {request.filter.distance.location.lon -> f:format.number()}
}
lang = painless
inline = doc["location"].arcDistance(params.lat,params.lon) * 0.001
}
}
}
}
In above example we add a single ``script_field`` called ``distance``. We add a condition when this
field should be added. The condition will be parsed as Fluidtemplate and is casted to bool via PHP.
If the condition is true, or no ``condition`` exists, the ``script_field`` will be added to the
query. The ``condition`` will be removed and everything else is submitted one to one to
elasticsearch, except each property is run through Fluidtemplate, to allow you to use information
from search request, e.g. to insert latitude and longitude from a filter, like in the above example.
.. _sort:
sort
----
Sort is handled like :ref:`fields`.
.. _mode: .. _mode:
``mode`` mode
"""""""" ----
Used by: Controller while preparing action. Used by: Controller while preparing action.

View file

@ -24,6 +24,21 @@ use TYPO3\CMS\Core\Tests\UnitTestCase as CoreTestCase;
abstract class AbstractUnitTestCase extends CoreTestCase abstract class AbstractUnitTestCase extends CoreTestCase
{ {
public function setUp()
{
parent::setUp();
// Disable caching backends to make TYPO3 parts work in unit test mode.
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Core\Cache\CacheManager::class
)->setCacheConfigurations([
'extbase_object' => [
'backend' => \TYPO3\CMS\Core\Cache\Backend\NullBackend::class,
],
]);
}
/** /**
* @return \TYPO3\CMS\Core\Log\LogManager * @return \TYPO3\CMS\Core\Log\LogManager
*/ */

View file

@ -0,0 +1,143 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Configuration;
/*
* 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 Codappix\SearchCore\Configuration\ConfigurationUtility;
use Codappix\SearchCore\Connection\SearchRequestInterface;
use Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class ConfigurationUtilityTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider possibleRequestAndConfigurationForFluidtemplate
*/
public function recursiveEntriesAreProcessedAsFluidtemplate(SearchRequestInterface $searchRequest, array $array, array $expected)
{
$subject = new ConfigurationUtility();
$this->assertSame(
$expected,
$subject->replaceArrayValuesWithRequestContent($searchRequest, $array),
'Entries in array were not parsed as fluid template with search request.'
);
}
public function possibleRequestAndConfigurationForFluidtemplate() : array
{
return [
'Nothing in array' => [
'searchRequest' => new SearchRequest(),
'array' => [],
'expected' => [],
],
'Small array with nothing to replace' => [
'searchRequest' => new SearchRequest(),
'array' => [
'key1' => 'value1',
],
'expected' => [
'key1' => 'value1',
],
],
'Rescursive array with replacements' => [
'searchRequest' => call_user_func(function () {
$request = new SearchRequest();
$request->setFilter([
'distance' => [
'location' => '10',
],
]);
return $request;
}),
'array' => [
'sub1' => [
'sub1.1' => '{request.filter.distance.location}',
'sub1.2' => '{request.nonExisting}',
],
],
'expected' => [
'sub1' => [
// Numberics are casted to double
'sub1.1' => 10.0,
'sub1.2' => null,
],
],
],
];
}
/**
* @test
* @dataProvider possibleConditionEntries
*/
public function conditionsAreHandledAsExpected(array $entries, array $expected)
{
$subject = new ConfigurationUtility();
$this->assertSame(
$expected,
$subject->filterByCondition($entries),
'Conditions were not processed as expected.'
);
}
public function possibleConditionEntries() : array
{
return [
'Nothing in array' => [
'entries' => [],
'expected' => [],
],
'Entries without condition' => [
'entries' => [
'key1' => 'value1',
],
'expected' => [
'key1' => 'value1',
],
],
'Entry with matching condition' => [
'entries' => [
'sub1' => [
'condition' => true,
'sub1.2' => 'something',
],
],
'expected' => [
'sub1' => [
'sub1.2' => 'something',
],
],
],
'Entry with non matching condition' => [
'entries' => [
'sub1' => [
'condition' => false,
'sub1.2' => 'something',
],
],
'expected' => [],
],
];
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\DataProcessing;
/*
* 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 Codappix\SearchCore\DataProcessing\GeoPointProcessor;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class GeoPointProcessorTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider getPossibleRecordConfigurationCombinations
*/
public function geoPointsAreAddedAsConfigured(array $record, array $configuration, array $expectedRecord)
{
$subject = new GeoPointProcessor();
$processedRecord = $subject->processRecord($record, $configuration);
$this->assertSame(
$expectedRecord,
$processedRecord,
'The processor did not return the expected processed record.'
);
}
/**
* @return array
*/
public function getPossibleRecordConfigurationCombinations()
{
return [
'Create new field with existing lat and lng' => [
'record' => [
'lat' => 23.232,
'lng' => 45.43,
],
'configuration' => [
'to' => 'location',
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'lat' => 23.232,
'lng' => 45.43,
'location' => [
'lat' => 23.232,
'lon' => 45.43,
],
],
],
'Do not create new field due to missing configuration' => [
'record' => [
'lat' => 23.232,
'lng' => 45.43,
],
'configuration' => [
'to' => 'location',
],
'expectedRecord' => [
'lat' => 23.232,
'lng' => 45.43,
],
],
'Do not create new field due to missing lat and lon' => [
'record' => [
'lat' => '',
'lng' => '',
],
'configuration' => [
'to' => 'location',
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'lat' => '',
'lng' => '',
],
],
'Do not create new field due to invalid lat and lon' => [
'record' => [
'lat' => 'av',
'lng' => 'dsf',
],
'configuration' => [
'to' => 'location',
'lat' => 'lat',
'lon' => 'lng',
],
'expectedRecord' => [
'lat' => 'av',
'lng' => 'dsf',
],
],
];
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Codappix\SearchCore\Tests\Unit\Domain\Model;
/*
* 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 Codappix\SearchCore\Domain\Model\SearchRequest;
use Codappix\SearchCore\Tests\Unit\AbstractUnitTestCase;
class SearchRequestTest extends AbstractUnitTestCase
{
/**
* @test
* @dataProvider possibleEmptyFilter
*/
public function emptyFilterWillNotBeSet(array $filter)
{
$searchRequest = new SearchRequest();
$searchRequest->setFilter($filter);
$this->assertSame(
[],
$searchRequest->getFilter(),
'Empty filter were set, even if they should not.'
);
}
public function possibleEmptyFilter()
{
return [
'Complete empty Filter' => [
'filter' => [],
],
'Single filter with empty value' => [
'filter' => [
'someFilter' => '',
],
],
'Single filter with empty recursive values' => [
'filter' => [
'someFilter' => [
'someKey' => '',
],
],
],
];
}
/**
* @test
*/
public function filterIsSet()
{
$filter = ['someField' => 'someValue'];
$searchRequest = new SearchRequest();
$searchRequest->setFilter($filter);
$this->assertSame(
$filter,
$searchRequest->getFilter(),
'Filter was not set.'
);
}
}

View file

@ -21,6 +21,7 @@ namespace Codappix\SearchCore\Tests\Unit\Domain\Search;
*/ */
use Codappix\SearchCore\Configuration\ConfigurationContainerInterface; use Codappix\SearchCore\Configuration\ConfigurationContainerInterface;
use Codappix\SearchCore\Configuration\ConfigurationUtility;
use Codappix\SearchCore\Configuration\InvalidArgumentException; use Codappix\SearchCore\Configuration\InvalidArgumentException;
use Codappix\SearchCore\Domain\Model\FacetRequest; use Codappix\SearchCore\Domain\Model\FacetRequest;
use Codappix\SearchCore\Domain\Model\SearchRequest; use Codappix\SearchCore\Domain\Model\SearchRequest;
@ -44,7 +45,8 @@ class QueryFactoryTest extends AbstractUnitTestCase
parent::setUp(); parent::setUp();
$this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock(); $this->configuration = $this->getMockBuilder(ConfigurationContainerInterface::class)->getMock();
$this->subject = new QueryFactory($this->getMockedLogger(), $this->configuration); $configurationUtility = new ConfigurationUtility();
$this->subject = new QueryFactory($this->getMockedLogger(), $this->configuration, $configurationUtility);
} }
/** /**
@ -100,8 +102,6 @@ class QueryFactoryTest extends AbstractUnitTestCase
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$searchRequest->setFilter([ $searchRequest->setFilter([
'field' => '', 'field' => '',
'field1' => 0,
'field2' => false,
]); ]);
$this->assertFalse( $this->assertFalse(
@ -209,10 +209,16 @@ class QueryFactoryTest extends AbstractUnitTestCase
public function minimumShouldMatchIsAddedToQuery() public function minimumShouldMatchIsAddedToQuery()
{ {
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->configuration->expects($this->once()) $this->configuration->expects($this->any())
->method('getIfExists') ->method('getIfExists')
->with('searching.minimumShouldMatch') ->withConsecutive(
->willReturn('50%'); ['searching.minimumShouldMatch'],
['searching.sort']
)
->will($this->onConsecutiveCalls(
'50%',
null
));
$this->configuration->expects($this->any()) $this->configuration->expects($this->any())
->method('get') ->method('get')
->will($this->throwException(new InvalidArgumentException)); ->will($this->throwException(new InvalidArgumentException));
@ -244,14 +250,21 @@ class QueryFactoryTest extends AbstractUnitTestCase
{ {
$searchRequest = new SearchRequest('SearchWord'); $searchRequest = new SearchRequest('SearchWord');
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->withConsecutive(['searching.boost'], ['searching.fieldValueFactor']) ->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls( ->will($this->onConsecutiveCalls(
[ [
'search_title' => 3, 'search_title' => 3,
'search_abstract' => 1.5, 'search_abstract' => 1.5,
], ],
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException) $this->throwException(new InvalidArgumentException)
)); ));
@ -292,10 +305,17 @@ class QueryFactoryTest extends AbstractUnitTestCase
'factor' => '2', 'factor' => '2',
'missing' => '1', 'missing' => '1',
]; ];
$this->configuration->expects($this->exactly(2)) $this->configuration->expects($this->any())
->method('get') ->method('get')
->withConsecutive(['searching.boost'], ['searching.fieldValueFactor']) ->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls( ->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException), $this->throwException(new InvalidArgumentException),
$fieldConfig $fieldConfig
)); ));
@ -343,4 +363,210 @@ class QueryFactoryTest extends AbstractUnitTestCase
'Empty search request does not create expected query.' 'Empty search request does not create expected query.'
); );
} }
/**
* @test
*/
public function storedFieldsAreAddedToQuery()
{
$searchRequest = new SearchRequest();
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
'_source, something,nothing',
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException)
));
$query = $this->subject->create($searchRequest);
$this->assertSame(
['_source', 'something', 'nothing'],
$query->toArray()['stored_fields'],
'Stored fields were not added to query as expected.'
);
}
/**
* @test
*/
public function storedFieldsAreNotAddedToQuery()
{
$searchRequest = new SearchRequest();
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException)
));
$query = $this->subject->create($searchRequest);
$this->assertFalse(
isset($query->toArray()['stored_fields']),
'Stored fields were added to query even if not configured.'
);
}
/**
* @test
*/
public function scriptFieldsAreAddedToQuery()
{
$searchRequest = new SearchRequest('query value');
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
[
'field1' => [
'config' => 'something',
],
'field2' => [
'config' => '{request.query}',
],
],
$this->throwException(new InvalidArgumentException)
));
$query = $this->subject->create($searchRequest);
$this->assertSame(
[
'field1' => [
'config' => 'something',
],
'field2' => [
'config' => 'query value',
],
],
$query->toArray()['script_fields'],
'Script fields were not added to query as expected.'
);
}
/**
* @test
*/
public function scriptFieldsAreNotAddedToQuery()
{
$searchRequest = new SearchRequest();
$this->configuration->expects($this->any())
->method('get')
->withConsecutive(
['searching.boost'],
['searching.fields.stored_fields'],
['searching.fields.script_fields'],
['searching.fieldValueFactor']
)
->will($this->onConsecutiveCalls(
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException),
$this->throwException(new InvalidArgumentException)
));
$query = $this->subject->create($searchRequest);
$this->assertTrue(
!isset($query->toArray()['script_fields']),
'Script fields were added to query even if not configured.'
);
}
/**
* @test
*/
public function sortIsAddedToQuery()
{
$searchRequest = new SearchRequest('query value');
$this->configuration->expects($this->any())
->method('getIfExists')
->withConsecutive(
['searching.minimumShouldMatch'],
['searching.sort']
)
->will($this->onConsecutiveCalls(
null,
[
'field1' => [
'config' => 'something',
],
'field2' => [
'config' => '{request.query}',
],
]
));
$this->configuration->expects($this->any())
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest);
$this->assertSame(
[
'field1' => [
'config' => 'something',
],
'field2' => [
'config' => 'query value',
],
],
$query->toArray()['sort'],
'Sort was not added to query as expected.'
);
}
/**
* @test
*/
public function sortIsNotAddedToQuery()
{
$searchRequest = new SearchRequest('query value');
$this->configuration->expects($this->any())
->method('getIfExists')
->withConsecutive(
['searching.minimumShouldMatch'],
['searching.sort']
)
->will($this->onConsecutiveCalls(
null,
null
));
$this->configuration->expects($this->any())
->method('get')
->will($this->throwException(new InvalidArgumentException));
$query = $this->subject->create($searchRequest);
$this->assertTrue(
!isset($query->toArray()['sort']),
'Sort was added to query even if not configured.'
);
}
} }