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;
}