Skip to content

Commit

Permalink
Merge pull request #641 from City-of-Helsinki/UHF-10176
Browse files Browse the repository at this point in the history
UHF-10176 unify recommendations between translations
  • Loading branch information
rpnykanen authored Jun 14, 2024
2 parents 31b0a15 + 829dda7 commit 98dc551
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ bundle: news_item
label: 'Annif keywords'
description: ''
required: false
translatable: true
translatable: false
default_value: { }
default_value_callback: ''
settings:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
164 changes: 121 additions & 43 deletions public/modules/custom/helfi_annif/src/RecommendationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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;
}

}

0 comments on commit 98dc551

Please sign in to comment.