Problem/Motivation

I'm trying to subtly change the behavior of the hierarchical term facet so that when a child term is selected the parent is unselected. I built some custom logic using SearchApiSolrEvents::PRE_QUERY but it's resulting in the facet not displaying correctly, though the results appear to work correctly.

The goal behavior is as follows:
* I click through a term hierarchy, first selecting a top level item in the tree.
* The page reloads to show matching results, i.e. everything tagged with the top-level term.
* I click a child term of the first level item, i.e. narrowing the selection.
* The page reloads to show the parent item is now in an indeterminate state and the results only show items which are tagged with the child term.
* I click a child term of the second level item, i.e. narrowing the selection further
* The page reloads to show the parent and first level items are now in an indeterminate state and the results only show items which are tagged with the grand child term.

Steps to reproduce

Proposed resolution

Remaining tasks

User interface changes

API changes

TBD

Data model changes

n/a

Comments

damienmckenna created an issue.

damienmckenna’s picture

Here's the code I'm using so far:


namespace Drupal\mysite\EventSubscriber;

use Drupal\search_api_solr\Event\PreQueryEvent;
use Drupal\search_api_solr\Event\SearchApiSolrEvents;
use Drupal\taxonomy\Entity\Term;
use Solarium\Component\QueryInterface as SolariumQueryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event before a Search API query gets finally converted into a solarium query.
 */
class SolrEventSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [
      SearchApiSolrEvents::PRE_QUERY => 'preQuery',
    ];
    return $events;
  }

  /**
   * Modify the query before it's executed.
   */
  public function preQuery(PreQueryEvent $event) {
    $search_query = $event->getSearchApiQuery();

    // Only look at the Teachings page.
    if ($search_query->getSearchId() != 'views_page:search_teachings__page') {
      return;
    }

    $solarium_query = $event->getSolariumQuery();
    // Change the query to display both SOLR query and AI-boosted results.
    if ($solarium_query instanceof SolariumQueryInterface) {
      $filter_queries = $solarium_query->getFilterQueries();
      if ($filter_queries) {
        $changed = FALSE;
        $prefix = 'itm_node_topics:';
        /** @var \Solarium\QueryType\Select\Query\FilterQuery $filter_query */
        foreach ($filter_queries as &$filter_query) {
          $options = $filter_query->getOptions();
          if (empty($options['query'])) {
            continue;
          }
          if (str_contains($options['query'], $prefix) === FALSE) {
            continue;
          }

          $topics = [];
          foreach (explode(' ', $options['query']) as $key => $topic) {
            preg_match('/(.*)\"([0-9]*)\"/', $topic, $matches);
            if (!empty($matches[2])) {
              $topics[$matches[2]] = $matches[2];
            }
          }
          // No topics were found in this query, so try the next one.
          if (empty($topics)) {
            break;
          }

          // Loop through all of the topics, if one of them has a parent ID that
          // is in the list of topics then remove that parent item from the
          // topics list.
          $terms = Term::loadMultiple($topics);
          /** @var \Drupal\taxonomy\Entity\Term $term */
          foreach ($terms as $term) {
            $parents = $term->parent->referencedEntities() ?? NULL;
            if (empty($parents)) {
              continue;
            }
            if (in_array($parents[0]->id(), $topics)) {
              unset($topics[$parents[0]->id()]);
              $changed = TRUE;
            }
          }

          // If the topics list was changed, i.e. items were removed, then
          // rebuild the option variable.
          if ($changed) {
            $new_option = [];
            foreach ($topics as $topic) {
              $new_option[] = $prefix . '"' . $topic . '"';
            }
            $options['query'] = '(' . implode(' ', $new_option) . ')';
            $filter_query->setOptions($options);
          }
        }

        // If the arguments were changed, update the query.
        if ($changed) {
          $solarium_query->setFilterQueries($filter_queries);
        }
      }
    }
  }

}

I thought this would work - the results seem to be correct, but the facet now only shows the child and its parent & grandparent terms.

damienmckenna’s picture

I tried to build a process plugin to accomplish this but wasn't able to work out the correct approach.

In the meantime I built the following, which technically works but might not be the best way:


namespace Drupal\mymodule\EventSubscriber;

use Drupal\facets\Event\FacetsEvents;
use Drupal\facets\Event\UrlCreated;
use Drupal\taxonomy\Entity\Term;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Modify Facets.
 */
class FacetsEventSubscriber implements EventSubscriberInterface {

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events = [
      FacetsEvents::URL_CREATED => 'urlCreated',
    ];
    return $events;
  }

  /**
   * Filter out parent terms on Topics facets.
   *
   * @param \Drupal\facets\Event\UrlCreated $event
   *   The event object.
   */
  public function urlCreated(UrlCreated $event): void {
    $facet = $event->getFacet();

    // Only work on the Topics facet.
    if ($facet->id() != 'topics') {
      return;
    }

    $url = $event->getUrl();
    $facetResult = $event->getFacetResult();

    $query_string = $url->getOption('query');
    if (!empty($query_string['f'])) {
      $new_query = [];
      $topics = [];
      $prefix = 'topics';
      foreach ($query_string['f'] as $key => $value) {
        [$facet_name, $term_id] = explode(':', $value);
        if ($facet_name != $prefix) {
          $new_query[] = $value;
        }
        else {
          $topics[$term_id] = $term_id;
        }
      }

      // Only bother with additional logic if there's more than one term.
      if (!empty($topics) && count($topics) > 1) {
        // Loop through the topics, if one of them has a parent ID that is in
        // the list of topics then remove that parent item from the topics list.
        $terms = Term::loadMultiple($topics);

        /** @var \Drupal\taxonomy\Entity\Term $term */
        foreach ($terms as $term) {
          $parents = $term->parent->referencedEntities() ?? NULL;

          // No parents, so just add the facet to the list.
          if (empty($parents)) {
            $new_query[$term->id()] = $prefix . ':' . $term->id();
            continue;
          }

          // If the parent is in the list then remove it from the list - this is
          // the whole purpose for all this logic.
          if (in_array($parents[0]->id(), $topics)) {
            unset($new_query[$parents[0]->id()]);
          }

          // Add the item to the list.
          $new_query[$term->id()] = $prefix . ':' . $term->id();
        }

        // Rebuild the option variable, after sorting it.
        sort($new_query);
        $query_string['f'] = $new_query;
        $url->setOption('query', $query_string);
        $event->setUrl($url);
      }
    }
  }

}