You are here

protected function Database::getFacets in Search API 8

Computes facets for a search query.

Parameters

\Drupal\search_api\Query\QueryInterface $query: The search query for which facets should be computed.

\Drupal\Core\Database\Query\SelectInterface $db_query: A database select query which returns all results of that search query.

int|null $result_count: (optional) The total number of results of the search query, if known.

Return value

array An array of facets, as specified by the search_api_facets feature.

1 call to Database::getFacets()
Database::search in modules/search_api_db/src/Plugin/search_api/backend/Database.php
Executes a search on this server.

File

modules/search_api_db/src/Plugin/search_api/backend/Database.php, line 2454

Class

Database
Indexes and searches items using the database.

Namespace

Drupal\search_api_db\Plugin\search_api\backend

Code

protected function getFacets(QueryInterface $query, SelectInterface $db_query, $result_count = NULL) {
  $fields = $this
    ->getFieldInfo($query
    ->getIndex());
  $ret = [];
  foreach ($query
    ->getOption('search_api_facets') as $key => $facet) {
    if (empty($fields[$facet['field']])) {
      $msg = $this
        ->t('Unknown facet field @field.', [
        '@field' => $facet['field'],
      ]);
      $this->warnings[(string) $msg] = 1;
      continue;
    }
    $field = $fields[$facet['field']];
    if (($facet['operator'] ?? 'and') != 'or') {

      // First, check whether this can even possibly have any results.
      if ($result_count !== NULL && $result_count < $facet['min_count']) {
        continue;
      }

      // All the AND facets can use the main query. If we didn't yet create a
      // temporary table for them yet, do so now.
      if (!isset($table)) {
        $table = $this
          ->getTemporaryResultsTable($db_query);
      }
      if ($table) {
        $select = $this->database
          ->select($table, 't');
      }
      else {

        // If no temporary table could be created (most likely due to a
        // missing permission), use a nested query instead.
        $select = $this->database
          ->select(clone $db_query, 't');
      }

      // In case we didn't get the result count passed to the method, we can
      // get it now. (This allows us to skip AND facets with a min_count
      // higher than the result count.)
      if ($result_count === NULL) {
        $result_count = $select
          ->countQuery()
          ->execute()
          ->fetchField();
        if ($result_count < $facet['min_count']) {
          continue;
        }
      }
    }
    else {

      // For OR facets, we need to build a different base query that excludes
      // the facet filters applied to the facet.
      $or_query = clone $query;
      $conditions =& $or_query
        ->getConditionGroup()
        ->getConditions();
      $tag = 'facet:' . $facet['field'];
      foreach ($conditions as $i => $condition) {
        if ($condition instanceof ConditionGroupInterface && $condition
          ->hasTag($tag)) {
          unset($conditions[$i]);
        }
      }
      try {
        $or_db_query = $this
          ->createDbQuery($or_query, $fields);
      } catch (SearchApiException $e) {
        $this
          ->logException($e, '%type while trying to create a facets query: @message in %function (line %line of %file).');
        continue;
      }
      $select = $this->database
        ->select($or_db_query, 't');
    }

    // If "Include missing facet" is disabled, we use an INNER JOIN and add IS
    // NOT NULL for shared tables.
    $is_text_type = $this
      ->getDataTypeHelper()
      ->isTextType($field['type']);
    $alias = $this
      ->getTableAlias($field, $select, TRUE, $facet['missing'] ? 'leftJoin' : 'innerJoin');
    $select
      ->addField($alias, $is_text_type ? 'word' : 'value', 'value');
    if ($is_text_type) {
      $select
        ->condition($alias . '.field_name', $this
        ->getTextFieldName($facet['field']));
    }
    if (!$facet['missing'] && !$is_text_type) {
      $select
        ->isNotNull($alias . '.value');
    }
    $select
      ->addExpression('COUNT(DISTINCT t.item_id)', 'num');
    $select
      ->groupBy('value');
    $select
      ->orderBy('num', 'DESC');
    $select
      ->orderBy('value', 'ASC');
    $limit = $facet['limit'];
    if ((int) $limit > 0) {
      $select
        ->range(0, $limit);
    }
    if ($facet['min_count'] > 1) {
      $select
        ->having('COUNT(DISTINCT t.item_id) >= :count', [
        ':count' => $facet['min_count'],
      ]);
    }
    $terms = [];
    $values = [];
    $has_missing = FALSE;
    foreach ($select
      ->execute() as $row) {
      $terms[] = [
        'count' => $row->num,
        'filter' => $row->value !== NULL ? '"' . $row->value . '"' : '!',
      ];
      if ($row->value !== NULL) {
        $values[] = $row->value;
      }
      else {
        $has_missing = TRUE;
      }
    }

    // If 'Minimum facet count' is set to 0 in the display options for this
    // facet, we need to retrieve all facets, even ones that aren't matched in
    // our search result set above. Here we SELECT all DISTINCT facets, and
    // add in those facets that weren't added above.
    if ($facet['min_count'] < 1) {
      $select = $this->database
        ->select($field['table'], 't');
      $select
        ->addField('t', 'value', 'value');
      $select
        ->distinct();
      if ($values) {
        $select
          ->condition('value', $values, 'NOT IN');
      }
      $select
        ->isNotNull('value');
      foreach ($select
        ->execute() as $row) {
        $terms[] = [
          'count' => 0,
          'filter' => '"' . $row->value . '"',
        ];
      }
      if ($facet['missing'] && !$has_missing) {
        $terms[] = [
          'count' => 0,
          'filter' => '!',
        ];
      }
    }
    $ret[$key] = $terms;
  }
  return $ret;
}