You are here

public function SearchApiDbService::getAutocompleteSuggestions in Search API Database Search 7

Implements SearchApiAutocompleteInterface::getAutocompleteSuggestions().

File

./service.inc, line 2102
Contains SearchApiDbService.

Class

SearchApiDbService
Indexes and searches items using the database.

Code

public function getAutocompleteSuggestions(SearchApiQueryInterface $query, SearchApiAutocompleteSearch $search, $incomplete_key, $user_input) {
  $settings = isset($this->options['autocomplete']) ? $this->options['autocomplete'] : array();
  $settings += array(
    'suggest_suffix' => TRUE,
    'suggest_words' => TRUE,
  );

  // If none of these options is checked, the user apparently chose a very
  // roundabout way of telling us he doesn't want autocompletion.
  if (!array_filter($settings)) {
    return array();
  }
  $index = $query
    ->getIndex();
  if (empty($this->options['indexes'][$index->machine_name])) {
    throw new SearchApiException(t('Unknown index @id.', array(
      '@id' => $index->machine_name,
    )));
  }
  $fields = $this
    ->getFieldInfo($index);
  $suggestions = array();
  $passes = array();

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

  // Decide which methods we want to use.
  if ($incomplete_key && $settings['suggest_suffix']) {
    $tokenizer_active = static::isTokenizerActive($index);
    $processed_key = $this
      ->splitKeys($incomplete_key, $tokenizer_active);
    if ($processed_key) {

      // In case the $incomplete_key turned out to be more than one word, add
      // all but the last one to the user input.
      if (is_array($processed_key)) {
        unset($processed_key['#conjunction']);
        $incomplete_key = array_pop($processed_key);
        if ($processed_key) {
          $user_input .= ' ' . implode(' ', $processed_key);
        }
        $processed_key = $incomplete_key;
      }
      $passes[] = 1;
      $incomplete_like = $this->connection
        ->escapeLike($processed_key) . '%';
    }
  }
  if ($settings['suggest_words'] && (!$incomplete_key || strlen($incomplete_key) >= $this->options['min_chars'])) {
    $passes[] = 2;
  }
  if (!$passes) {
    return array();
  }

  // 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.
  $keys = drupal_map_assoc(preg_split('/[^\\p{L}\\p{N}]+/u', $user_input, -1, PREG_SPLIT_NO_EMPTY));
  if ($incomplete_key) {
    $keys[$incomplete_key] = $incomplete_key;
  }
  foreach ($passes as $pass) {
    if ($pass == 2 && $incomplete_key) {
      $query
        ->keys($user_input);
    }

    // To avoid suggesting incomplete words, we have to temporarily disable
    // the "partial_matches" option. (There should be no way we'll save the
    // server during the createDbQuery() call, so this should be safe.)
    $options = $this->options;
    $this->options['partial_matches'] = FALSE;
    $db_query = $this
      ->createDbQuery($query, $fields);
    $this->options = $options;

    // Add additional tags and metadata.
    $db_query
      ->addTag('search_api_db_autocomplete');
    $db_query
      ->addMetaData('search_api_db_autocomplete', array(
      'search' => $search,
      'incomplete_key' => $incomplete_key,
      'user_input' => $user_input,
      'pass' => $pass,
    ));
    $text_fields = array();
    foreach ($query
      ->getFields() as $field) {
      if (isset($fields[$field]) && search_api_is_text_type($fields[$field]['type'])) {
        $text_fields[] = $field;
      }
    }
    if (empty($text_fields)) {
      return array();
    }

    // For each text field that will be searched, store the item IDs in a
    // temporary table. This is unfortunately necessary since MySQL doesn't
    // allow using a temporary table multiple times in a single query.
    $all_results = array();
    $total = NULL;
    $first_temp_table = TRUE;
    foreach ($text_fields as $field) {
      $table = $this
        ->getTemporaryResultsTable($db_query);
      if (!$table) {
        return array();
      }
      if ($first_temp_table) {

        // For subsequent temporary tables, just use a plain SELECT over the
        // first to fill them, instead of the (potentially very complex)
        // search query.
        $first_temp_table = FALSE;
        $db_query = $this->connection
          ->select($table)
          ->fields($table, array(
          'item_id',
        ));
      }
      $all_results[$field] = $this->connection
        ->select($table, 't')
        ->fields('t', array(
        'item_id',
      ));
      if ($total === NULL) {
        $total = $this->connection
          ->query("SELECT COUNT(item_id) FROM {{$table}}")
          ->fetchField();
      }
    }
    $max_occurrences = max(1, floor($total * variable_get('search_api_db_autocomplete_max_occurrences', 0.9)));
    if (!$total) {
      if ($pass == 1) {
        return NULL;
      }
      continue;
    }
    $word_query = NULL;
    foreach ($text_fields as $field) {
      $field_query = $this->connection
        ->select($fields[$field]['table'], 't')
        ->fields('t', array(
        'word',
        'item_id',
      ))
        ->condition('item_id', $all_results[$field], 'IN')
        ->condition('field_name', $this
        ->getTextFieldName($field));
      if ($pass == 1) {
        $field_query
          ->condition('word', $incomplete_like, 'LIKE')
          ->condition('word', $keys, 'NOT IN');
      }
      if (!isset($word_query)) {
        $word_query = $field_query;
      }
      else {
        $word_query
          ->union($field_query);
      }
    }
    if (!$word_query) {
      return array();
    }
    $db_query = $this->connection
      ->select($word_query, 't');
    $db_query
      ->addExpression('COUNT(DISTINCT item_id)', 'results');
    $db_query
      ->fields('t', array(
      'word',
    ))
      ->groupBy('word')
      ->having('COUNT(DISTINCT item_id) <= :max', array(
      ':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[] = array(
        'suggestion_suffix' => $suffix,
        'results' => $row->results,
      );
    }
  }
  return $suggestions;
}