diff --git a/modules/helfi_paragraphs_news_list/config/install/external_entities.external_entity_type.helfi_news_neighbourhoods.yml b/modules/helfi_paragraphs_news_list/config/install/external_entities.external_entity_type.helfi_news_neighbourhoods.yml index fd5cedd73..6a5d05086 100644 --- a/modules/helfi_paragraphs_news_list/config/install/external_entities.external_entity_type.helfi_news_neighbourhoods.yml +++ b/modules/helfi_paragraphs_news_list/config/install/external_entities.external_entity_type.helfi_news_neighbourhoods.yml @@ -19,6 +19,8 @@ field_mapper_config: value: '$._source.name[0]' tid: value: '$._source.tid[0]' + location: + value: '$._source.field_location' storage_client_id: helfi_news_neighbourhoods storage_client_config: { } persistent_cache_max_age: 86400 diff --git a/modules/helfi_paragraphs_news_list/helfi_paragraphs_news_list.install b/modules/helfi_paragraphs_news_list/helfi_paragraphs_news_list.install index 755e6f684..9a2c49b2e 100644 --- a/modules/helfi_paragraphs_news_list/helfi_paragraphs_news_list.install +++ b/modules/helfi_paragraphs_news_list/helfi_paragraphs_news_list.install @@ -135,3 +135,11 @@ function helfi_paragraphs_news_list_update_9009() : void { \Drupal::service('helfi_platform_config.config_update_helper') ->update('helfi_paragraphs_news_list'); } + +/** + * UHF-11002: Update news list external entities. + */ +function helfi_paragraphs_news_list_update_9010() : void { + \Drupal::service('helfi_platform_config.config_update_helper') + ->update('helfi_paragraphs_news_list'); +} diff --git a/modules/helfi_paragraphs_news_list/src/ElasticExternalEntityBase.php b/modules/helfi_paragraphs_news_list/src/ElasticExternalEntityBase.php index 31c02892a..9a7d84935 100644 --- a/modules/helfi_paragraphs_news_list/src/ElasticExternalEntityBase.php +++ b/modules/helfi_paragraphs_news_list/src/ElasticExternalEntityBase.php @@ -81,7 +81,7 @@ protected function request( array $parameters, ) : array { try { - return $this->client->search($parameters)->asArray(); + return $this->client->search($parameters)?->asArray() ?? []; } catch (ElasticsearchException | TransportException $e) { Error::logException($this->logger, $e); @@ -135,6 +135,78 @@ protected function getFieldMapping(string $field) : string { return $field; } + /** + * Get callback that builds elasticsearch query fragment for given operator. + * + * @param ?string $op + * Query operation. + * + * @return callable + * Handler. + */ + protected function getOperatorCallback(?string $op): callable { + return match($op) { + 'IN' => static function (array $value, string $fieldName) : array { + $inGroup = []; + foreach ($value as $v) { + $inGroup[] = ['term' => [$fieldName => $v]]; + } + return [ + 'query' => [ + 'bool' => [ + 'must' => [ + ['bool' => ['should' => $inGroup]], + ], + ], + ], + ]; + }, + 'CONTAINS' => static function (string $value, string $fieldName) : array { + return [ + 'query' => [ + 'bool' => [ + 'must' => [ + [ + 'regexp' => [ + $fieldName => [ + 'value' => $value . '.*', + 'case_insensitive' => TRUE, + ], + ], + ], + ], + ], + ], + ]; + }, + 'GEO_DISTANCE_SORT' => static function (array $value, string $fieldName) : array { + [$coordinates, $options] = $value; + + return [ + 'sort' => [ + [ + '_geo_distance' => [ + $fieldName => $coordinates, + ...$options, + ], + ], + ], + ]; + }, + default => static function (string|int|null $value, string $fieldName) : array { + return [ + 'query' => [ + 'bool' => [ + 'must' => [ + ['term' => [$fieldName => $value]], + ], + ], + ], + ]; + }, + }; + } + /** * Builds the elastic query for given parameters. * @@ -147,7 +219,10 @@ protected function getFieldMapping(string $field) : string { * The query. */ protected function buildQuery(array $parameters, array $sorts) : array { - $query = []; + $body = [ + 'sort' => [], + 'query' => [], + ]; foreach ($parameters as $parameter) { ['field' => $field, 'value' => $value, 'operator' => $op] = $parameter; @@ -156,29 +231,9 @@ protected function buildQuery(array $parameters, array $sorts) : array { if (!$value) { continue; } - $callback = match($op) { - 'IN' => function (array $value, string $fieldName) : array { - $inGroup = []; - foreach ($value as $v) { - $inGroup[] = ['term' => [$fieldName => $v]]; - } - return ['bool' => ['should' => $inGroup]]; - }, - 'CONTAINS' => function (string $value, string $fieldName) : array { - return [ - 'regexp' => [ - $fieldName => [ - 'value' => $value . '.*', - 'case_insensitive' => TRUE, - ], - ], - ]; - }, - default => function (string|int|null $value, string $fieldName) : array { - return ['term' => [$fieldName => $value]]; - }, - }; - $query['bool']['must'][] = $callback($value, $fieldName); + + $callback = $this->getOperatorCallback($op); + $body = array_merge_recursive($body, $callback($value, $fieldName)); } $sortQuery = []; @@ -189,12 +244,13 @@ protected function buildQuery(array $parameters, array $sorts) : array { $sortQuery[$fieldName] = ['order' => strtolower($direction)]; } + $body = array_merge_recursive($body, [ + 'sort' => $sortQuery, + ]); + return [ 'index' => $this->index, - 'body' => [ - 'sort' => $sortQuery, - 'query' => $query, - ], + 'body' => $body, ]; } diff --git a/modules/helfi_paragraphs_news_list/src/Plugin/ExternalEntities/StorageClient/NewsNeighbourhoods.php b/modules/helfi_paragraphs_news_list/src/Plugin/ExternalEntities/StorageClient/NewsNeighbourhoods.php index 4b3502364..86c54cd28 100644 --- a/modules/helfi_paragraphs_news_list/src/Plugin/ExternalEntities/StorageClient/NewsNeighbourhoods.php +++ b/modules/helfi_paragraphs_news_list/src/Plugin/ExternalEntities/StorageClient/NewsNeighbourhoods.php @@ -20,4 +20,14 @@ final class NewsNeighbourhoods extends TermBase { */ protected string $vid = 'news_neighbourhoods'; + /** + * {@inheritdoc} + */ + protected function getFieldMapping(string $field) : string { + return match($field) { + 'location' => 'field_location', + default => parent::getFieldMapping($field), + }; + } + } diff --git a/modules/helfi_paragraphs_news_list/tests/src/Kernel/ExternalEntityStorage/NewsNeighbourhoodsStorageClientTest.php b/modules/helfi_paragraphs_news_list/tests/src/Kernel/ExternalEntityStorage/NewsNeighbourhoodsStorageClientTest.php index 328c899a3..605e2337c 100644 --- a/modules/helfi_paragraphs_news_list/tests/src/Kernel/ExternalEntityStorage/NewsNeighbourhoodsStorageClientTest.php +++ b/modules/helfi_paragraphs_news_list/tests/src/Kernel/ExternalEntityStorage/NewsNeighbourhoodsStorageClientTest.php @@ -4,6 +4,9 @@ namespace Drupal\Tests\helfi_paragraphs_news_list\Kernel\ExternalEntityStorage; +use Drupal\external_entities\Entity\Query\External\Query; +use Elastic\Elasticsearch\Client; + /** * Tests news tags storage client. * @@ -25,4 +28,70 @@ protected function getVid(): string { return 'news_neighbourhoods'; } + /** + * Tests geo_distance sorting. + */ + public function testGeoDistanceQuery(): void { + $client = $this->prophesize(Client::class); + // Test geo distance sort. + $client->search([ + 'index' => 'news_terms', + 'body' => [ + 'sort' => [ + [ + '_geo_distance' => [ + 'field_location' => [ + 'lat' => 48.8584, + 'lon' => 2.2945, + ], + 'unit' => 'km', + 'order' => 'asc', + 'distance_type' => 'plane', + 'mode' => 'min', + 'ignore_unmapped' => FALSE, + ], + ], + ], + 'query' => [ + 'bool' => [ + 'must' => [ + ['term' => ['vid' => $this->getVid()]], + ], + ], + ], + ], + ]) + ->shouldBeCalled() + ->willReturn($this->createElasticsearchResponse([])); + + $query = $this->getSut($client->reveal()) + ->getQuery(); + + $this->assertInstanceOf(Query::class, $query); + + // Drupal query interface is not quite flexible enough to support all the + // options and parameters geo_distance sort needs, so the implementation + // uses setParameter from external_entities Query class. + $query->setParameter('location', [ + [ + 'lat' => 48.8584, + 'lon' => 2.2945, + ], + [ + 'unit' => 'km', + // Geo distance sort direction. + 'order' => 'asc', + // 'arc' is more accurate, but within + // a city it should not matter. + 'distance_type' => 'plane', + // What to do in case a field has several geo points. + 'mode' => 'min', + // Unmapped field cause the search to fail. + 'ignore_unmapped' => FALSE, + ], + ], 'GEO_DISTANCE_SORT'); + + $query->accessCheck(FALSE)->execute(); + } + }