Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UHF-10176 unify recommendations between translations #641

Merged
merged 16 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}

}