Ajouter des résultats à une recherche effectuée avec Search API et ElasticSearch sur Drupal 8

Une boussole

Lors de recherches effectuées sur les contenus d'un site Drupal 8, il peut parfois être utile de pouvoir rajouter certains contenus aux résultats déjà obtenus, et ceci en fonction de ces résultats. Cela peut être du contenu qu'on souhaite mettre en avant quelque soit la recherche, ou faire remonter des landing page génériques qui servent et complètent les contenus trouvés ou encore les auteurs des contenus issus des résultats de la recherche.

Pour ce faire, le module ElasticSearch Connector dispose de deux méthodes intéressantes au niveau du Backend qu'il définit : preQuery() qui permet d'altérer la requête avant qu'elle soit envoyée au moteur d'indexation, et postQuery() qui permet d'altérer les résultats de la requête avant qu'ils soient rendus. C'est bien sûr cette dernière méthode qui nous intéresse plus particulièrement. Pour pouvoir l'utiliser nous allons déclarer une classe qui va étendre le backend de ElasticSearch afin de pouvoir utiliser cette méthode.

Nous pouvons soit altérer le backend par défaut utilisé par ElasticSearch connector comme ci-dessous.

/**
 * Implements hook_search_api_backend_info_alter().
 *
 * Defines custom backend enhancements for supported search api backends.
 */
function my_module_search_api_backend_info_alter(array &$backend_info) {
  // Elastic Search backend.
  // See https://www.drupal.org/node/2906735
  if (isset($backend_info['elasticsearch'])) {
    $backend_info['elasticsearch']['class'] = '\Drupal\my_module\Plugin\search_api\backend\MyModuleSearchApiElasticsearchBackend';
  }
}

Soit tout simplement, après avoir créer notre classe qui étend SearchApiElasticsearchBackend, la sélectionner lorsque nous créons le cluster Elasticsearch depuis l'interface d'administration.

Une fois fait, il ne reste plus qu'à écrire notre Class et d'utiliser la méthode postQuery().

Dans l'exemple ci-dessous nous allons rajouter les utilisateurs auteur des contenus remontés par la requête. A noter que cela est réalisable aussi facilement, c'est aussi parce que l'index créé avec Search API a été configuré pour indexer les types d'entité Node et User. Et donc que notre index dispose de l'entité User comme DataSource. Posons le code tout de suite.

<?php

namespace Drupal\MY_MODULE\Plugin\search_api\backend;

use Drupal\node\NodeInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Query\Condition;
use Drupal\search_api\Query\ConditionGroup;
use Drupal\search_api\Query\QueryInterface;
use Drupal\search_api\Query\ResultSetInterface;
use Drupal\user\Entity\User;
use Drupal\user\UserInterface;
use Drupal\search_api\Query\ResultSet;
use Drupal\elasticsearch_connector\Plugin\search_api\backend\SearchApiElasticsearchBackend;

/**
 * Elasticsearch Search API Backend definition.
 *
 * @SearchApiBackend(
 *   id = "my_modume_elasticsearch",
 *   label = @Translation("My module Elasticsearch"),
 *   description = @Translation("My Module Index items using an Elasticsearch server.")
 * )
 */
class MyModuleSearchApiElasticsearchBackend extends SearchApiElasticsearchBackend {

  /**
   * Allow custom changes before search results are returned for subclasses.
   *
   * @param \Drupal\search_api\Query\ResultSetInterface $results
   *   The results array that will be returned for the search.
   * @param \Drupal\search_api\Query\QueryInterface $query
   *   The \Drupal\search_api\Query\Query object representing the executed
   *   search query.
   * @param object $response
   *   The response object returned by Elastic Search.
   */
  protected function postQuery(ResultSetInterface $results, QueryInterface $query, $response) {
    /** @var \Drupal\search_api\Utility\FieldsHelper $search_api_fields_helper */
    $search_api_fields_helper = \Drupal::service('search_api.fields_helper');
    $index = $query->getIndex();
    $tags = $query->getTags();
    $user_datasource = $query->getIndex()->getDatasource('entity:user');
    $items = $results->getResultItems();

    // Post query only for the search view.
    if (!in_array('views_search', $tags)) {
      return;
    }

    // Facet filters are set only on the node datasource. So users are filtered
    // out if a facet is active. In this case we add all the users which are
    // the owners of the node results. Otherwise, if not conditions then, there
    // is nothing to do.
    $main_condition_group = $query->getConditionGroup();
    $conditions = $main_condition_group->getConditions();
    if (empty($conditions)) {
      return;
    }

    // Store all the user added to the results
    $user_ids_added = [];

    /** @var \Drupal\search_api\Item\ItemInterface $item */
    foreach ($items as $item) {
      $language = $item->getLanguage();
      $owner_uid = $this->getNodeUid($item);
      if (!empty($owner_uid)) {
        $user = User::load($owner_uid);
        if ($user instanceof UserInterface) {
          if (!$user->hasRole('ROLE_ID')) {
            continue;
          }
          if (in_array($owner_uid, $user_ids_added)) {
            continue;
          }
          $new_item = $search_api_fields_helper->createItemFromObject($index, $user->getTypedData(), NULL, $user_datasource);
          $results->addResultItem($new_item);
          $user_ids_added[$owner_uid] = $owner_uid;
        }
      }
    }

    // Sort and group the results by uid.
    // As the users were added to the end, they will sort after all the node
    // they own.
    // We sort the content as below :
    // content one (user 1), content two (user 1), user 1, content tree (user 2), user 2, etc
    // Sort by uid ASC
    $result_items = $results->getResultItems();
    uasort($result_items, array($this, 'sortResultsByUid'));
    $results->setResultItems($result_items);
  }

  /**
   * @param \Drupal\search_api\Item\ItemInterface $item
   *   The item object.
   * @return string
   *   The node owner id.
   */
  protected function getNodeUid(ItemInterface $item) {
    $node = $item->getOriginalObject()->getValue();
    if ($node instanceof NodeInterface) {
      $uid = $node->getOwnerId();
      return $uid;
    }

    return '';
  }

  /**
   * @param \Drupal\search_api\Item\ItemInterface $item
   *   The item object.
   * @return string
   *   The user uid.
   * @throws \Drupal\search_api\SearchApiException
   */
  protected function getUid(ItemInterface $item) {
    $entity = $item->getOriginalObject()->getValue();
    if ($entity instanceof NodeInterface) {
      $uid = $entity->getOwnerId();
      return $uid;
    }
    elseif ($entity instanceof UserInterface) {
      return $entity->id();
    }
    return '';
  }

  /**
   * @param \Drupal\search_api\Item\ItemInterface $a
   * @param \Drupal\search_api\Item\ItemInterface $b
   * @return bool
   * @throws \Drupal\search_api\SearchApiException
   */
  protected function sortResultsByUid(ItemInterface $a, ItemInterface $b) {
    return $this->getUid($a) > $this->getUid($b);
  }

  /**
   * @param \Drupal\search_api\Item\ItemInterface $a
   * @param \Drupal\search_api\Item\ItemInterface $b
   * @return int
   * @throws \Drupal\search_api\SearchApiException
   */
  protected function sortResultsByEntityTypeId(ItemInterface $a, ItemInterface $b) {
    return strnatcmp($a->getDatasource()->getDerivativeId(), $b->getDatasource()->getDerivativeId());
  }

}

La partie la plus significative, qui peut mériter de s'y attarder un peu, est celle permettant d'ajouter ces utilisateurs aux résultats de Search API, et notamment l'usage du service serach_api.fields_helper qui nous facilite grandement la tache.

Après avoir récupéré les informations essentielles de la requête, comme l'index, l'identifiant du datasource pour les entités User, et bien sûr les résultats entre autres.

/** @var \Drupal\search_api\Utility\FieldsHelper $search_api_fields_helper */
$search_api_fields_helper = \Drupal::service('search_api.fields_helper');
$index = $query->getIndex();
$tags = $query->getTags();
$user_datasource = $query->getIndex()->getDatasource('entity:user');
$items = $results->getResultItems();

C'est un jeu d'enfant de créer un nouvel Item des résultats d'une recherche effectuée avec Search API depuis une entité (ici le $user).

$new_item = $search_api_fields_helper->createItemFromObject($index, $user->getTypedData(), NULL, $user_datasource);
$results->addResultItem($new_item);

Comme les "nouveaux" résultats sont ajoutés à la fin de premier jeu de résultats, la partie la plus délicate reste ici de trouver une bonne clé pour réordonner tous ces résultats dans un ensemble cohérent.

 

 

 

 

Ajouter un commentaire