diff --git a/conf/cmi/field.field.node.news_item.field_annif_keywords.yml b/conf/cmi/field.field.node.news_item.field_annif_keywords.yml index 21772cf59..fb462ad05 100644 --- a/conf/cmi/field.field.node.news_item.field_annif_keywords.yml +++ b/conf/cmi/field.field.node.news_item.field_annif_keywords.yml @@ -13,7 +13,7 @@ bundle: news_item label: 'Annif keywords' description: '' required: false -translatable: true +translatable: false default_value: { } default_value_callback: '' settings: diff --git a/public/modules/custom/helfi_annif/src/Plugin/Block/RecommendationsBlock.php b/public/modules/custom/helfi_annif/src/Plugin/Block/RecommendationsBlock.php index 889257d49..1ba6a66ac 100644 --- a/public/modules/custom/helfi_annif/src/Plugin/Block/RecommendationsBlock.php +++ b/public/modules/custom/helfi_annif/src/Plugin/Block/RecommendationsBlock.php @@ -66,7 +66,15 @@ public function build() : array { '#title' => $this->t('You might be interested in'), ]; - $recommendations = $this->recommendationManager->getRecommendations($node); + $recommendations = []; + try { + $recommendations = $this->recommendationManager + ->getRecommendations($node, 3, 'fi'); + } + catch (\Exception $exception) { + $this->logger->error($exception->getMessage()); + } + if (!$recommendations) { return $this->handleNoRecommendations($response); } diff --git a/public/modules/custom/helfi_annif/src/RecommendationManager.php b/public/modules/custom/helfi_annif/src/RecommendationManager.php index c17572b77..e3454c320 100644 --- a/public/modules/custom/helfi_annif/src/RecommendationManager.php +++ b/public/modules/custom/helfi_annif/src/RecommendationManager.php @@ -7,15 +7,12 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerAwareTrait; +use Drupal\Core\Entity\TranslatableInterface; /** * The recommendation manager. */ -class RecommendationManager implements LoggerAwareInterface { - - use LoggerAwareTrait; +class RecommendationManager { /** * The constructor. @@ -34,14 +31,66 @@ public function __construct( /** * Get recommendations for a node. * - * @param \Drupal\Core\Entity\EntityInterface $node + * @param \Drupal\Core\Entity\EntityInterface $entity * The node. + * @param int $limit + * How many recommendations should be returned. + * @param string|null $target_langcode + * Which translation to use to select the recommendations, + * null uses the entity's translation. * * @return array * Array of recommendations. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getRecommendations(EntityInterface $entity, int $limit = 3, string $target_langcode = NULL): array { + $destination_langcode = $entity->language()->getId(); + $target_langcode = $target_langcode ?? $destination_langcode; + if ($entity instanceof TranslatableInterface && !$entity->hasTranslation($target_langcode)) { + $target_langcode = $destination_langcode; + } + + $queryResult = $this->executeQuery($entity, $target_langcode, $destination_langcode, $limit); + if (!$queryResult || !is_array($queryResult)) { + return []; + } + + $this->sortByCreatedAt($queryResult); + $nids = array_column($queryResult, 'nid'); + + $entities = $this->entityManager + ->getStorage($entity->getEntityTypeId()) + ->loadMultiple($nids); + + // Entity query returns the results sorted by nid in ascending order + // while the raw query's results are in correct order. + $entities = $this->sortEntitiesByQueryResult($entities, $queryResult); + + return $this->getTranslations($entities, $destination_langcode); + } + + /** + * Execute the recommendation query. + * + * The recommendations can be unified between the translations + * by always getting the results using the primary language recommendations. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity we want to suggest recommendations for. + * @param string $target_langcode + * What language are we using as a base for the recommendations. + * @param string $destination_langcode + * What is the destination langcode. + * @param int $limit + * How many items to get. + * + * @return array + * Database query result. */ - public function getRecommendations(EntityInterface $node): array { - // @todo #UHF-9964 exclude unwanted keywords and entities and refactor. + private function executeQuery(EntityInterface $entity, string $target_langcode, string $destination_langcode, int $limit) { + // @todo #UHF-9964 exclude unwanted keywords $query = " select n.nid, @@ -55,56 +104,85 @@ public function getRecommendations(EntityInterface $node): array { field_annif_keywords_target_id from node__field_annif_keywords where entity_id = :nid and - langcode = :langcode) - and n.langcode = :langcode - and annif.langcode = :langcode - and nfd.langcode = :langcode + langcode = :target_langcode) + and n.langcode = :target_langcode + and annif.langcode = :destination_langcode + and nfd.langcode = :target_langcode and n.nid != :nid and nfd.created > :timestamp group by n.nid - order by relevancy DESC - limit 3; + order by count(n.nid) DESC + limit {$limit}; "; - $response = []; - try { - $timestamp = strtotime("-1 year", time()); - $results = $this->connection - ->query($query, [ - ':nid' => $node->id(), - ':langcode' => $node->language()->getId(), - ':timestamp' => $timestamp, - ]) - ->fetchAll(); - } - catch (\Exception $e) { - $this->logger->error($e->getMessage()); - return $response; - } - - if (!$results || !is_array($results)) { - return $response; - } + // Cannot add :limit as parameter here, + // must be added directly to the query string above. + return $this->connection + ->query($query, [ + ':nid' => $entity->id(), + ':target_langcode' => $target_langcode, + ':destination_langcode' => $destination_langcode, + ':timestamp' => strtotime("-1 year", time()), + ]) + ->fetchAll(); + } + /** + * Sort query result by created time. + * + * @param array $results + * Query results to sort. + */ + private function sortByCreatedAt(array &$results) : void { usort($results, function ($a, $b) { if ($a->created == $b->created) { return 0; } return ($a->created > $b->created) ? -1 : 1; }); - $nids = array_column($results, 'nid'); + } - try { - $response = $this->entityManager - ->getStorage($node->getEntityTypeId()) - ->loadMultiple($nids); - } - catch (\Exception $e) { - $this->logger->error($e->getMessage()); - return []; + /** + * Entity query changes the result sorting, it must be corrected afterward. + * + * @param array $entities + * Array of entities sorted by id. + * @param array $queryResult + * Array of query results sorted correctly. + * + * @return array + * Correctly sorted array of entities. + */ + private function sortEntitiesByQueryResult(array $entities, array $queryResult) : array { + $results = []; + foreach ($queryResult as $result) { + if (!isset($entities[$result->nid])) { + continue; + } + $results[] = $entities[$result->nid]; } + return $results; + } - return $response; + /** + * Get the translations for the recommended entities. + * + * @param array $entities + * Array of entities. + * @param string $destination_langcode + * Which translation to get. + * + * @return array + * Array of translated entities. + */ + private function getTranslations(array $entities, string $destination_langcode) : array { + $results = []; + foreach ($entities as $entity) { + if ($entity instanceof TranslatableInterface && $entity->hasTranslation($destination_langcode)) { + $results[] = $entity->getTranslation($destination_langcode); + } + } + return $results; } }