You are here

public function Database::getAutocompleteSuggestions in Search API 8

Retrieves autocompletion suggestions for some user input.

Parameters

\Drupal\search_api\Query\QueryInterface $query: A query representing the base search, with all completely entered words in the user input so far as the search keys.

\Drupal\search_api_autocomplete\SearchInterface $search: An object containing details about the search the user is on, and settings for the autocompletion. See the class documentation for details. Especially $search->getOptions() should be checked for settings, like whether to try and estimate result counts for returned suggestions.

string $incomplete_key: The start of another fulltext keyword for the search, which should be completed. Might be empty, in which case all user input up to now was considered completed. Then, additional keywords for the search could be suggested.

string $user_input: The complete user input for the fulltext search keywords so far.

Return value

\Drupal\search_api_autocomplete\Suggestion\SuggestionInterface[] An array of autocomplete suggestions.

See also

\Drupal\search_api_autocomplete\AutocompleteBackendInterface::getAutocompleteSuggestions()

File

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

Class

Database
Indexes and searches items using the database.

Namespace

Drupal\search_api_db\Plugin\search_api\backend

Code

public function getAutocompleteSuggestions(QueryInterface $query, SearchInterface $search, $incomplete_key, $user_input) {
  $settings = $this->configuration['autocomplete'];

  // If none of the options is checked, the user apparently chose a very
  // roundabout way of telling us they don't want autocompletion.
  if (!array_filter($settings)) {
    return [];
  }
  $index = $query
    ->getIndex();
  $db_info = $this
    ->getIndexDbInfo($index);
  if (empty($db_info['field_tables'])) {
    return [];
  }
  $fields = $this
    ->getFieldInfo($index);
  $suggestions = [];
  $factory = new SuggestionFactory($user_input);
  $passes = [];
  $incomplete_like = NULL;

  // Make the input lowercase as the indexed data is (usually) also all
  // lowercase.
  $incomplete_key = mb_strtolower($incomplete_key);
  $user_input = mb_strtolower($user_input);

  // Decide which methods we want to use.
  if ($incomplete_key && $settings['suggest_suffix']) {
    $passes[] = 1;
    $incomplete_like = $this->database
      ->escapeLike($incomplete_key) . '%';
  }
  if ($settings['suggest_words'] && (!$incomplete_key || strlen($incomplete_key) >= $this->configuration['min_chars'])) {
    $passes[] = 2;
  }
  if (!$passes) {
    return [];
  }

  // We want about half of the suggestions from each enabled method.
  $limit = $query
    ->getOption('limit', 10);
  $limit /= count($passes);
  $limit = ceil($limit);

  // Also collect all keywords already contained in the query so we don't
  // suggest them.
  if ($query
    ->getIndex()
    ->isValidProcessor('tokenizer')) {
    $keys = array_filter(explode(' ', $user_input), 'strlen');
  }
  else {
    $keys = static::splitIntoWords($user_input);
  }
  $keys = array_combine($keys, $keys);
  foreach ($passes as $pass) {
    if ($pass == 2 && $incomplete_key) {
      $query
        ->keys($user_input);
    }

    // To avoid suggesting incomplete words, we have to temporarily disable
    // partial matching. There should be no way we'll save the server during
    // the createDbQuery() call, so this should be safe.
    $configuration = $this->configuration;
    $db_query = NULL;
    try {
      $this->configuration['matching'] = 'words';
      $db_query = $this
        ->createDbQuery($query, $fields);
      $this->configuration = $configuration;

      // We need a list of all current results to match the suggestions
      // against. However, since MySQL doesn't allow using a temporary table
      // multiple times in one query, we regrettably have to do it this way.
      $fulltext_fields = $this
        ->getQueryFulltextFields($query);
      if (count($fulltext_fields) > 1) {
        $all_results = $db_query
          ->execute()
          ->fetchCol();

        // Compute the total number of results so we can later sort out
        // matches that occur too often.
        $total = count($all_results);
      }
      else {
        $table = $this
          ->getTemporaryResultsTable($db_query);
        if (!$table) {
          return [];
        }
        $all_results = $this->database
          ->select($table, 't')
          ->fields('t', [
          'item_id',
        ]);
        $sql = "SELECT COUNT(item_id) FROM {{$table}}";
        $total = $this->database
          ->query($sql)
          ->fetchField();
      }
    } catch (SearchApiException $e) {

      // If the exception was in createDbQuery(), we need to reset the
      // configuration here.
      $this->configuration = $configuration;
      $this
        ->logException($e, '%type while trying to create autocomplete suggestions: @message in %function (line %line of %file).');
      continue;
    }
    $max_occurrences = $this
      ->getConfigFactory()
      ->get('search_api_db.settings')
      ->get('autocomplete_max_occurrences');
    $max_occurrences = max(1, floor($total * $max_occurrences));
    if (!$total) {
      if ($pass == 1) {
        return [];
      }
      continue;
    }

    /** @var \Drupal\Core\Database\Query\SelectInterface|null $word_query */
    $word_query = NULL;
    foreach ($fulltext_fields as $field) {
      if (!isset($fields[$field]) || !$this
        ->getDataTypeHelper()
        ->isTextType($fields[$field]['type'])) {
        continue;
      }
      $field_query = $this->database
        ->select($fields[$field]['table'], 't');
      $field_query
        ->fields('t', [
        'word',
        'item_id',
      ])
        ->condition('t.field_name', $field)
        ->condition('t.item_id', $all_results, 'IN');
      if ($pass == 1) {
        $field_query
          ->condition('t.word', $incomplete_like, 'LIKE')
          ->condition('t.word', $keys, 'NOT IN');
      }
      if (!isset($word_query)) {
        $word_query = $field_query;
      }
      else {
        $word_query
          ->union($field_query);
      }
    }
    if (!$word_query) {
      return [];
    }
    $db_query = $this->database
      ->select($word_query, 't');
    $db_query
      ->addExpression('COUNT(DISTINCT t.item_id)', 'results');
    $db_query
      ->fields('t', [
      'word',
    ])
      ->groupBy('t.word')
      ->having('COUNT(DISTINCT t.item_id) <= :max', [
      ':max' => $max_occurrences,
    ])
      ->orderBy('results', 'DESC')
      ->range(0, $limit);
    $incomp_len = strlen($incomplete_key);
    foreach ($db_query
      ->execute() as $row) {
      $suffix = $pass == 1 ? substr($row->word, $incomp_len) : ' ' . $row->word;
      $suggestions[] = $factory
        ->createFromSuggestionSuffix($suffix, $row->results);
    }
  }
  return $suggestions;
}