From 59a334ea329b04d6b6f995951d50f0ab6a557031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Burnichon?= Date: Mon, 22 Feb 2016 13:56:15 +0100 Subject: [PATCH] New search style --- .../Controller/Api/SearchController.php | 171 ++++++++++++++++++ .../Phrasea/ControllerProvider/Api/V2.php | 9 + .../Elastic/ElasticSearchEngine.php | 1 + .../SearchEngine/SearchEngineResult.php | 50 ++++- .../SearchEngine/SearchEngineResultTest.php | 11 +- .../PhraseanetAuthenticatedWebTestCase.php | 2 + 6 files changed, 233 insertions(+), 11 deletions(-) create mode 100644 lib/Alchemy/Phrasea/Controller/Api/SearchController.php diff --git a/lib/Alchemy/Phrasea/Controller/Api/SearchController.php b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php new file mode 100644 index 0000000000..2a77833753 --- /dev/null +++ b/lib/Alchemy/Phrasea/Controller/Api/SearchController.php @@ -0,0 +1,171 @@ +searchAndFormatEngineResult($request); + + /** @var SearchEngineResult $search_result */ + $ret['search_type'] = $search_result->getOptions()->getSearchType(); + $ret['results'] = []; + + foreach ($this->convertSearchResultToRecords($search_result->getResults()) as $record) { + $ret['results'][] = [ + 'databox_id' => $record->getDataboxId(), + 'record_id' => $record->getRecordId(), + 'collection_id' => $record->getCollectionId(), + 'updated_at' => $record->getUpdated(), + ]; + } + + return Result::create($request, $ret)->createResponse(); + } + + private function searchAndFormatEngineResult(Request $request) + { + $options = SearchEngineOptions::fromRequest($this->app, $request); + + $offsetStart = (int) ($request->get('offset_start') ?: 0); + $perPage = (int) $request->get('per_page') ?: 10; + + $query = (string) $request->get('query'); + $this->getSearchEngine()->resetCache(); + + $search_result = $this->getSearchEngine()->query($query, $offsetStart, $perPage, $options); + + $this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQuery()); + + foreach ($options->getDataboxes() as $databox) { + $colls = array_map(function (\collection $collection) { + return $collection->get_coll_id(); + }, array_filter($options->getCollections(), function (\collection $collection) use ($databox) { + return $collection->get_databox()->get_sbas_id() == $databox->get_sbas_id(); + })); + + $this->getSearchEngineLogger() + ->log($databox, $search_result->getQuery(), $search_result->getTotal(), $colls); + } + + $this->getSearchEngine()->clearCache(); + + $ret = [ + 'offset_start' => $offsetStart, + 'per_page' => $perPage, + 'available_results' => $search_result->getAvailable(), + 'total_results' => $search_result->getTotal(), + 'error' => (string)$search_result->getError(), + 'warning' => (string)$search_result->getWarning(), + 'query_time' => $search_result->getDuration(), + 'search_indexes' => $search_result->getIndexes(), + 'suggestions' => array_map( + function (SearchEngineSuggestion $suggestion) { + return $suggestion->toArray(); + }, + $search_result->getSuggestions()->toArray() + ), + 'facets' => $search_result->getFacets(), + 'results' => [], + ]; + + return [$ret, $search_result]; + } + + /** + * @return SearchEngineInterface + */ + private function getSearchEngine() + { + return $this->app['phraseanet.SE']; + } + + /** + * @return UserManipulator + */ + private function getUserManipulator() + { + return $this->app['manipulator.user']; + } + + /** + * @return SearchEngineLogger + */ + private function getSearchEngineLogger() + { + return $this->app['phraseanet.SE.logger']; + } + + /** + * @param RecordInterface[] $records + * @return array[] + */ + private function groupRecordIdsPerDataboxId($records) + { + $number = 0; + $perDataboxRecordIds = []; + + foreach ($records as $record) { + $databoxId = $record->getDataboxId(); + + if (!isset($perDataboxRecordIds[$databoxId])) { + $perDataboxRecordIds[$databoxId] = []; + } + + $perDataboxRecordIds[$databoxId][$record->getRecordId()] = $number++; + } + + return $perDataboxRecordIds; + } + + /** + * @param RecordInterface[] $records + * @return \record_adapter[] + */ + private function convertSearchResultToRecords($records) + { + Assertion::allIsInstanceOf($records, RecordInterface::class); + + $perDataboxRecordIds = $this->groupRecordIdsPerDataboxId($records); + + $records = []; + + foreach ($perDataboxRecordIds as $databoxId => $recordIds) { + $databox = $this->findDataboxById($databoxId); + + foreach ($databox->getRecordRepository()->findByRecordIds(array_keys($recordIds)) as $record) { + $records[$recordIds[$record->getRecordId()]] = $record; + } + } + + ksort($records); + + return $records; + } +} diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php b/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php index c1076897f4..5973264cce 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Api/V2.php @@ -11,6 +11,7 @@ namespace Alchemy\Phrasea\ControllerProvider\Api; use Alchemy\Phrasea\Application as PhraseaApplication; use Alchemy\Phrasea\Controller\Api\BasketController; +use Alchemy\Phrasea\Controller\Api\SearchController; use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait; use Alchemy\Phrasea\Core\Event\Listener\OAuthListener; use Silex\Application; @@ -35,6 +36,12 @@ class V2 implements ControllerProviderInterface, ServiceProviderInterface ->setJsonBodyHelper($app['json.body_helper']); } ); + + $app['controller.api.v2.search'] = $app->share( + function (PhraseaApplication $app) { + return new SearchController($app); + } + ); } public function boot(Application $app) @@ -63,6 +70,8 @@ class V2 implements ControllerProviderInterface, ServiceProviderInterface ->bind('api_v2_basket_records_reorder'); $this->addBasketMiddleware($app, $controller); + $controllers->match('/search/', 'controller.api.v2.search:searchAction'); + return $controllers; } diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php index beb4be15e6..4807f28b81 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php @@ -310,6 +310,7 @@ class ElasticSearchEngine implements SearchEngineInterface $query['query_string'] = json_encode($params['body']); return new SearchEngineResult( + $options, $results, // ArrayCollection of results json_encode($query), $res['took'], // duration diff --git a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineResult.php b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineResult.php index e7bab5cc76..2378f79da1 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineResult.php +++ b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineResult.php @@ -1,5 +1,4 @@ options = $options; + $this->results = $results; $this->query = $query; $this->duration = (float) $duration; @@ -46,8 +80,14 @@ class SearchEngineResult $this->propositions = $propositions; $this->indexes = $indexes; $this->facets = $facets; + } - return $this; + /** + * @return SearchEngineOptions + */ + public function getOptions() + { + return $this->options; } /** diff --git a/tests/Alchemy/Tests/Phrasea/SearchEngine/SearchEngineResultTest.php b/tests/Alchemy/Tests/Phrasea/SearchEngine/SearchEngineResultTest.php index c72ae14847..fe5b47490a 100644 --- a/tests/Alchemy/Tests/Phrasea/SearchEngine/SearchEngineResultTest.php +++ b/tests/Alchemy/Tests/Phrasea/SearchEngine/SearchEngineResultTest.php @@ -2,6 +2,7 @@ namespace Alchemy\Tests\Phrasea\SearchEngine; +use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Doctrine\Common\Collections\ArrayCollection; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; use Alchemy\Phrasea\SearchEngine\SearchEngineSuggestion; @@ -17,6 +18,7 @@ class SearchEngineResultTest extends \PhraseanetTestCase */ public function testBasic() { + $options = new SearchEngineOptions(); $results = new ArrayCollection([ self::$DI['record_2'] ]); @@ -34,9 +36,7 @@ class SearchEngineResultTest extends \PhraseanetTestCase $propositions = new ArrayCollection(); $indexes = 'new-index'; - $result = new SearchEngineResult($results, $query, $duration, - $offsetStart, $available, $total, $error, $warning, - $suggestions, $propositions, $indexes); + $result = new SearchEngineResult($options, $results, $query, $duration, $offsetStart, $available, $total, $error, $warning, $suggestions, $propositions, $indexes); $this->assertEquals($warning, $result->getWarning()); $this->assertEquals(2, $result->getTotalPages(23)); @@ -55,6 +55,7 @@ class SearchEngineResultTest extends \PhraseanetTestCase public function testWithOffsetStartAtZero() { + $options = new SearchEngineOptions(); $results = new ArrayCollection([ self::$DI['record_2'] ]); @@ -72,9 +73,7 @@ class SearchEngineResultTest extends \PhraseanetTestCase $propositions = new ArrayCollection(); $indexes = 'new-index'; - $result = new SearchEngineResult($results, $query, $duration, - $offsetStart, $available, $total, $error, $warning, - $suggestions, $propositions, $indexes); + $result = new SearchEngineResult($options, $results, $query, $duration, $offsetStart, $available, $total, $error, $warning, $suggestions, $propositions, $indexes); $this->assertEquals(1, $result->getCurrentPage(10)); $this->assertEquals(1, $result->getCurrentPage(25)); diff --git a/tests/classes/PhraseanetAuthenticatedWebTestCase.php b/tests/classes/PhraseanetAuthenticatedWebTestCase.php index d21d2a9b5d..ed839881ba 100644 --- a/tests/classes/PhraseanetAuthenticatedWebTestCase.php +++ b/tests/classes/PhraseanetAuthenticatedWebTestCase.php @@ -3,6 +3,7 @@ use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Model\Entities\ElasticsearchRecord; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; +use Alchemy\Phrasea\SearchEngine\SearchEngineOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineResult; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\DBALException; @@ -205,6 +206,7 @@ abstract class PhraseanetAuthenticatedWebTestCase extends \PhraseanetAuthenticat $elasticsearchRecord->setRecordId($record->get_record_id()); $result = new SearchEngineResult( + new SearchEngineOptions(), new ArrayCollection([$elasticsearchRecord]), // Records '', // Query 0, // Duration