diff --git a/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php b/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php index dabd53853c..c98a66cb26 100644 --- a/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php +++ b/lib/Alchemy/Phrasea/Controller/Admin/SearchEngineController.php @@ -10,37 +10,62 @@ namespace Alchemy\Phrasea\Controller\Admin; -use Alchemy\Phrasea\SearchEngine\ConfigurationPanelInterface; +use Alchemy\Phrasea\Controller\Controller; +use Alchemy\Phrasea\SearchEngine\Elastic\ElasticSearchSettingFormType; +use Alchemy\Phrasea\SearchEngine\Elastic\GlobalElasticOptions; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -class SearchEngineController +class SearchEngineController extends Controller { - /** - * @var ConfigurationPanelInterface - */ - private $configurationPanel; - - public function __construct(ConfigurationPanelInterface $configurationPanel) - { - $this->configurationPanel = $configurationPanel; - } - /** * @param Request $request * @return Response */ - public function getConfigurationPanelAction(Request $request) + public function formConfigurationPanelAction(Request $request) { - return $this->configurationPanel->get($request); + $options = $this->getElasticSearchOptions(); + $form = $this->getConfigurationForm($options); + + $form->handleRequest($request); + + if ($form->isValid()) { + $this->saveElasticSearchOptions($form->getData()); + + return $this->app->redirectPath('admin_searchengine_form'); + } + + return $this->render('admin/search-engine/elastic-search.html.twig', [ + 'form' => $form->createView(), + ]); } /** - * @param Request $request - * @return Response + * @return GlobalElasticOptions */ - public function postConfigurationPanelAction(Request $request) + private function getElasticSearchOptions() { - return $this->configurationPanel->post($request); + return $this->app['elasticsearch.options']; + } + + /** + * @param GlobalElasticOptions $configuration + * @return void + */ + private function saveElasticSearchOptions(GlobalElasticOptions $configuration) + { + $this->getConf()->set(['main', 'search-engine', 'options'], $configuration->toArray()); + } + + /** + * @param GlobalElasticOptions $options + * @return FormInterface + */ + private function getConfigurationForm(GlobalElasticOptions $options) + { + return $this->app->form(new ElasticSearchSettingFormType(), $options, [ + 'action' => $this->app->url('admin_searchengine_form'), + ]); } } diff --git a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php index 3dec948592..66ad090fcb 100644 --- a/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php +++ b/lib/Alchemy/Phrasea/Controller/Api/V1Controller.php @@ -356,7 +356,7 @@ class V1Controller extends Controller 'engine' => [ 'type' => $searchEngine->getName(), 'status' => $SEStatus, - 'configuration' => $searchEngine->getConfigurationPanel()->getConfiguration(), + 'configuration' => $conf->get(['main', 'searchengine', 'options']), ], ], 'binary' => [ diff --git a/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php b/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php index f430f5f6e0..fee6e9d70e 100644 --- a/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php +++ b/lib/Alchemy/Phrasea/Controller/Prod/MoveCollectionController.php @@ -9,7 +9,6 @@ */ namespace Alchemy\Phrasea\Controller\Prod; -use Alchemy\Phrasea\Application; use Alchemy\Phrasea\Controller\Controller; use Alchemy\Phrasea\Controller\RecordsRequest; use Symfony\Component\HttpFoundation\Request; diff --git a/lib/Alchemy/Phrasea/ControllerProvider/Admin/SearchEngine.php b/lib/Alchemy/Phrasea/ControllerProvider/Admin/SearchEngine.php index fd477bdb57..ec83bdeaa7 100644 --- a/lib/Alchemy/Phrasea/ControllerProvider/Admin/SearchEngine.php +++ b/lib/Alchemy/Phrasea/ControllerProvider/Admin/SearchEngine.php @@ -24,9 +24,7 @@ class SearchEngine implements ControllerProviderInterface, ServiceProviderInterf public function register(Application $app) { $app['controller.admin.search-engine'] = $app->share(function (PhraseaApplication $app) { - /** @var SearchEngineInterface $searchEngine */ - $searchEngine = $app['search_engine']; - return new SearchEngineController($searchEngine->getConfigurationPanel()); + return new SearchEngineController($app); }); } @@ -39,11 +37,9 @@ class SearchEngine implements ControllerProviderInterface, ServiceProviderInterf /** @var ControllerCollection $controllers */ $controllers = $app['controllers_factory']; - $controllers->get('/', 'controller.admin.search-engine:getConfigurationPanelAction') - ->bind('admin_searchengine_get'); - - $controllers->post('/', 'controller.admin.search-engine:postConfigurationPanelAction') - ->bind('admin_searchengine_post'); + $controllers->match('/', 'controller.admin.search-engine:formConfigurationPanelAction') + ->method('GET|POST') + ->bind('admin_searchengine_form'); return $controllers; } diff --git a/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php b/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php index 4216ce1b6b..e08df3188a 100644 --- a/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php +++ b/lib/Alchemy/Phrasea/Core/Provider/SearchEngineServiceProvider.php @@ -12,6 +12,7 @@ namespace Alchemy\Phrasea\Core\Provider; use Alchemy\Phrasea\Controller\LazyLocator; +use Alchemy\Phrasea\SearchEngine\Elastic\GlobalElasticOptions; use Alchemy\Phrasea\SearchEngine\SearchEngineLogger; use Alchemy\Phrasea\Exception\InvalidArgumentException; use Alchemy\Phrasea\SearchEngine\SearchEngineInterface; @@ -53,11 +54,14 @@ class SearchEngineServiceProvider implements ServiceProviderInterface if ($type !== SearchEngineInterface::TYPE_ELASTICSEARCH) { throw new InvalidArgumentException(sprintf('Invalid search engine type "%s".', $type)); } + /** @var GlobalElasticOptions $options */ + $options = $app['elasticsearch.options']; + return new ElasticSearchEngine( $app, $app['search_engine.structure'], $app['elasticsearch.client'], - $app['elasticsearch.options']['index'], + $options->getIndexName(), $app['locales.available'], $app['elasticsearch.facets_response.factory'] ); @@ -132,11 +136,13 @@ class SearchEngineServiceProvider implements ServiceProviderInterface /* Low-level elasticsearch services */ $app['elasticsearch.client'] = $app->share(function($app) { + /** @var GlobalElasticOptions $options */ $options = $app['elasticsearch.options']; - $clientParams = ['hosts' => [sprintf('%s:%s', $options['host'], $options['port'])]]; + $clientParams = ['hosts' => [sprintf('%s:%s', $options->getHost(), $options->getPort())]]; // Create file logger for debug if ($app['debug']) { + /** @var Logger $logger */ $logger = new $app['monolog.logger.class']('search logger'); $logger->pushHandler(new RotatingFileHandler($app['log.path'].DIRECTORY_SEPARATOR.'elasticsearch.log', 2), Logger::INFO); @@ -148,22 +154,16 @@ class SearchEngineServiceProvider implements ServiceProviderInterface }); $app['elasticsearch.options'] = $app->share(function($app) { - $options = $app['conf']->get(['main', 'search-engine', 'options'], []); + $options = GlobalElasticOptions::fromArray($app['conf']->get(['main', 'search-engine', 'options'], [])); - $indexName = sprintf('phraseanet_%s', str_replace( - array('/', '.'), array('', ''), - $app['conf']->get(['main', 'key']) - )); + if (empty($options->getIndexName())) { + $options->setIndexName(strtolower(sprintf('phraseanet_%s', str_replace( + array('/', '.'), array('', ''), + $app['conf']->get(['main', 'key']) + )))); + } - $defaults = [ - 'host' => '127.0.0.1', - 'port' => 9200, - 'index' => strtolower($indexName), - 'shards' => 3, - 'replicas' => 0 - ]; - - return array_replace($defaults, $options); + return $options; }); @@ -175,7 +175,7 @@ class SearchEngineServiceProvider implements ServiceProviderInterface $logger->pushHandler(new \Monolog\Handler\ErrorLogHandler()); return new Thesaurus( $app['elasticsearch.client'], - $app['elasticsearch.options']['index'], + $app['elasticsearch.options'], $logger ); }); diff --git a/lib/Alchemy/Phrasea/SearchEngine/AbstractConfigurationPanel.php b/lib/Alchemy/Phrasea/SearchEngine/AbstractConfigurationPanel.php deleted file mode 100644 index 4875279693..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/AbstractConfigurationPanel.php +++ /dev/null @@ -1,57 +0,0 @@ -conf = $conf; - } - - /** - * @param \databox[] $databoxes - * @return array - */ - public function getAvailableDateFields(array $databoxes) - { - $date_fields = []; - - foreach ($databoxes as $databox) { - /** @var \databox_field $field */ - foreach ($databox->get_meta_structure() as $field) { - if ($field->get_type() !== \databox_field::TYPE_DATE) { - continue; - } - - $date_fields[] = $field->get_name(); - } - } - - return $date_fields; - } - - /** - * {@inheritdoc} - */ - public function saveConfiguration(array $configuration) - { - $this->conf->set(['main', 'search-engine', 'options'], $configuration); - - return $this; - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/ConfigurationPanelInterface.php b/lib/Alchemy/Phrasea/SearchEngine/ConfigurationPanelInterface.php deleted file mode 100644 index 5c29e1b4a2..0000000000 --- a/lib/Alchemy/Phrasea/SearchEngine/ConfigurationPanelInterface.php +++ /dev/null @@ -1,65 +0,0 @@ -app = $app; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'elastic-search-engine'; - } - - /** - * {@inheritdoc} - */ - public function get(Request $request) - { - return $this->app['twig']->render('admin/search-engine/elastic-search.html.twig', ['configuration' => $this->getConfiguration()]); - } - - /** - * {@inheritdoc} - */ - public function post(Request $request) - { - $configuration = $this->getConfiguration(); - - $configuration['host'] = $request->request->get('host'); - $configuration['port'] = $request->request->get('port'); - - $this->saveConfiguration($configuration); - - return $this->app->redirectPath('admin_searchengine_get'); - } - - /** - * {@inheritdoc} - */ - public function getConfiguration() - { - return $this->conf->get(['main', 'search-engine', 'options'], []); - } -} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php index dff0d31cfe..f6f10946cd 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchEngine.php @@ -100,18 +100,6 @@ class ElasticSearchEngine implements SearchEngineInterface return $ret; } - /** - * {@inheritdoc} - */ - public function getConfigurationPanel() - { - if (!$this->configurationPanel) { - $this->configurationPanel = new ConfigurationPanel($this->app, $this->app['conf']); - } - - return $this->configurationPanel; - } - /** * {@inheritdoc} */ diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchSettingFormType.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchSettingFormType.php new file mode 100644 index 0000000000..a950aa92b9 --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/ElasticSearchSettingFormType.php @@ -0,0 +1,53 @@ +add('host', 'text', [ + 'label' => 'ElasticSearch server host', + ]) + ->add('port', 'integer', [ + 'label' => 'ElasticSearch service port', + 'constraints' => new Range(['min' => 1, 'max' => 65535]), + ]) + ->add('indexName', 'text', [ + 'label' => 'ElasticSearch index name', + 'constraints' => new NotBlank(), + ]) + ->add('shards', 'integer', [ + 'label' => 'Number of shards', + 'constraints' => new Range(['min' => 1]), + ]) + ->add('replicas', 'integer', [ + 'label' => 'Number of replicas', + 'constraints' => new Range(['min' => 0]), + ]) + ->add('minScore', 'integer', [ + 'label' => 'Thesaurus Min score', + 'constraints' => new Range(['min' => 0]), + ]) + ->add('save', 'submit') + ; + } + + public function getName() + { + return 'elastic_settings'; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/GlobalElasticOptions.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/GlobalElasticOptions.php new file mode 100644 index 0000000000..e8a6a486da --- /dev/null +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/GlobalElasticOptions.php @@ -0,0 +1,165 @@ + '127.0.0.1', + 'port' => 9200, + 'index' => '', + 'shards' => 3, + 'replicas' => 0, + 'minScore' => 4, + ], $options); + + $self = new self(); + $self->setHost($options['host']); + $self->setPort($options['port']); + $self->setIndexName($options['index']); + $self->setShards($options['shards']); + $self->setReplicas($options['replicas']); + $self->setMinScore($options['minScore']); + + return $self; + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'host' => $this->host, + 'port' => $this->port, + 'index' => $this->indexName, + 'shards' => $this->shards, + 'replicas' => $this->replicas, + 'minScore' => $this->minScore, + ]; + } + + /** + * @param string $host + */ + public function setHost($host) + { + $this->host = $host; + } + + /** + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * @param int $port + */ + public function setPort($port) + { + $this->port = (int)$port; + } + + /** + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * @param int $minScore + */ + public function setMinScore($minScore) + { + $this->minScore = (int)$minScore; + } + + /** + * @return int + */ + public function getMinScore() + { + return $this->minScore; + } + + /** + * @param string $indexName + */ + public function setIndexName($indexName) + { + $this->indexName = $indexName; + } + + /** + * @return string + */ + public function getIndexName() + { + return $this->indexName; + } + + /** + * @param int $shards + */ + public function setShards($shards) + { + $this->shards = (int)$shards; + } + + /** + * @return int + */ + public function getShards() + { + return $this->shards; + } + + /** + * @param int $replicas + */ + public function setReplicas($replicas) + { + $this->replicas = (int)$replicas; + } + + /** + * @return int + */ + public function getReplicas() + { + return $this->replicas; + } +} diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer.php index 8fc215b295..edc89e5494 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Indexer.php @@ -32,6 +32,7 @@ class Indexer /** @var \Elasticsearch\Client */ private $client; + /** @var GlobalElasticOptions */ private $options; private $appbox; /** @var LoggerInterface|null */ @@ -48,7 +49,7 @@ class Indexer const DEFAULT_REFRESH_INTERVAL = '1s'; const REFRESH_INTERVAL_KEY = 'index.refresh_interval'; - public function __construct(Client $client, array $options, TermIndexer $termIndexer, RecordIndexer $recordIndexer, appbox $appbox, LoggerInterface $logger = null) + public function __construct(Client $client, GlobalElasticOptions $options, TermIndexer $termIndexer, RecordIndexer $recordIndexer, appbox $appbox, LoggerInterface $logger = null) { $this->client = $client; $this->options = $options; @@ -64,9 +65,9 @@ class Indexer public function createIndex($withMapping = true) { $params = array(); - $params['index'] = $this->options['index']; - $params['body']['settings']['number_of_shards'] = $this->options['shards']; - $params['body']['settings']['number_of_replicas'] = $this->options['replicas']; + $params['index'] = $this->options->getIndexName(); + $params['body']['settings']['number_of_shards'] = $this->options->getShards(); + $params['body']['settings']['number_of_replicas'] = $this->options->getReplicas(); $params['body']['settings']['analysis'] = $this->getAnalysis();; if ($withMapping) { @@ -80,7 +81,7 @@ class Indexer public function updateMapping() { $params = array(); - $params['index'] = $this->options['index']; + $params['index'] = $this->options->getIndexName(); $params['type'] = RecordIndexer::TYPE_NAME; $params['body'][RecordIndexer::TYPE_NAME] = $this->recordIndexer->getMapping(); $params['body'][TermIndexer::TYPE_NAME] = $this->termIndexer->getMapping(); @@ -91,13 +92,13 @@ class Indexer public function deleteIndex() { - $params = array('index' => $this->options['index']); + $params = array('index' => $this->options->getIndexName()); $this->client->indices()->delete($params); } public function indexExists() { - $params = array('index' => $this->options['index']); + $params = array('index' => $this->options->getIndexName()); return $this->client->indices()->exists($params); } @@ -132,7 +133,7 @@ class Indexer } // Optimize index - $params = array('index' => $this->options['index']); + $params = array('index' => $this->options->getIndexName()); $this->client->indices()->optimize($params); }); @@ -203,7 +204,7 @@ class Indexer try { // Prepare the bulk operation $bulk = new BulkOperation($this->client, $this->logger); - $bulk->setDefaultIndex($this->options['index']); + $bulk->setDefaultIndex($this->options->getIndexName()); $bulk->setAutoFlushLimit(1000); // Do the work $work($bulk); @@ -233,7 +234,7 @@ class Indexer private function getSetting($name) { - $index = $this->options['index']; + $index = $this->options->getIndexName(); $params = array(); $params['index'] = $index; $params['name'] = $name; @@ -245,7 +246,7 @@ class Indexer private function setSetting($name, $value) { - $index = $this->options['index']; + $index = $this->options->getIndexName(); $params = array(); $params['index'] = $index; $params['body'][$name] = $value; diff --git a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Thesaurus.php b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Thesaurus.php index 6d6545a01d..3ef451a9e8 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/Elastic/Thesaurus.php +++ b/lib/Alchemy/Phrasea/SearchEngine/Elastic/Thesaurus.php @@ -22,16 +22,17 @@ use Psr\Log\LoggerInterface; class Thesaurus { + /** @var Client */ private $client; - private $index; + /** @var GlobalElasticOptions */ + private $options; + /** @var LoggerInterface */ private $logger; - const MIN_SCORE = 4; - - public function __construct(Client $client, $index, LoggerInterface $logger) + public function __construct(Client $client, GlobalElasticOptions $options, LoggerInterface $logger) { $this->client = $client; - $this->index = $index; + $this->options = $options; $this->logger = $logger; } @@ -136,7 +137,7 @@ class Thesaurus // Search request $params = array(); - $params['index'] = $this->index; + $params['index'] = $this->options->getIndexName(); $params['type'] = TermIndexer::TYPE_NAME; $params['body']['query'] = $query; $params['body']['aggs'] = $aggs; @@ -144,7 +145,7 @@ class Thesaurus // inexact concepts. // We also need to disable TF/IDF on terms, and try to boost score only // when the search match nearly all tokens of term's value field. - $params['body']['min_score'] = self::MIN_SCORE; + $params['body']['min_score'] = $this->options->getMinScore(); // No need to get any hits since we extract data from aggs $params['body']['size'] = 0; diff --git a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineInterface.php b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineInterface.php index faf2596cb8..286466797a 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineInterface.php +++ b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineInterface.php @@ -40,11 +40,6 @@ interface SearchEngineInterface */ public function getStatus(); - /** - * @return ConfigurationPanelInterface - */ - public function getConfigurationPanel(); - /** * @return array an array of field names */ diff --git a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineOptions.php b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineOptions.php index 8fea09ba77..52317c699b 100644 --- a/lib/Alchemy/Phrasea/SearchEngine/SearchEngineOptions.php +++ b/lib/Alchemy/Phrasea/SearchEngine/SearchEngineOptions.php @@ -408,7 +408,7 @@ class SearchEngineOptions }, $value); } if (in_array($key, ['collections', 'business_fields'])) { - $value = array_map(function ($collection) { + $value = array_map(function (\collection $collection) { return $collection->get_base_id(); }, $value); } diff --git a/templates/web/admin/search-engine/elastic-search.html.twig b/templates/web/admin/search-engine/elastic-search.html.twig index 638ecb9f62..3f347614d4 100644 --- a/templates/web/admin/search-engine/elastic-search.html.twig +++ b/templates/web/admin/search-engine/elastic-search.html.twig @@ -1,11 +1,3 @@