apachesolr_autocomplete.module in Apache Solr Autocomplete 7.2
Same filename and directory in other branches
Alters search forms to suggest terms using Apache Solr using AJAX.
File
apachesolr_autocomplete.moduleView source
<?php
/**
* @file
* Alters search forms to suggest terms using Apache Solr using AJAX.
*/
/**
* Implementation of hook_init().
*/
function apachesolr_autocomplete_init() {
drupal_add_js(drupal_get_path('module', 'apachesolr_autocomplete') . '/apachesolr_autocomplete.js');
// TODO: Use #attached in forms instead of including JS in hook_init()?
#drupal_add_css( drupal_get_path('module', 'apachesolr_autocomplete') . '/apachesolr_autocomplete.css'); // TODO: Remove css?
drupal_add_library('system', 'ui.autocomplete');
}
// TODO: DEBUG.. remove apachesolr_autocomplete_form_alter function.
function apachesolr_autocomplete_form_alter(&$form, $form_state, $form_id) {
dsm("apachesolr_autocomplete_form_alter(): form_id = {$form_id}");
}
/**
* Implementation of hook_form_FORM_ID_alter().
*/
function apachesolr_autocomplete_form_search_form_alter(&$form, $form_state) {
if ($form['module']['#value'] == 'apachesolr_search' || $form['module']['#value'] == 'apachesolr_multisitesearch') {
$element =& $form['basic']['keys'];
#$form['#attached']['js'][] = drupal_get_path('module', 'apachesolr_autocomplete') . '/apachesolr_autocomplete.js';
apachesolr_autocomplete_do_alter($element, $form, 'apachesolr_search_page:core_search');
}
}
/**
* Implementation of hook_form_FORM_ID_alter().
*/
function apachesolr_autocomplete_form_search_block_form_alter(&$form, $form_state) {
$element =& $form['search_block_form'];
apachesolr_autocomplete_do_alter($element, $form, 'apachesolr_search_page:core_search');
}
/**
* Implementation of hook_form_FORM_ID_alter().
*/
function apachesolr_autocomplete_form_apachesolr_search_custom_page_search_form_alter(&$form, $form_state) {
$element =& $form['basic']['keys'];
$context_id = 'apachesolr_search_page:' . $form['#search_page']['page_id'];
apachesolr_autocomplete_do_alter($element, $form, $context_id);
}
/**
* Helper function to do the actual altering of search forms.
*
* @param $element
* The element to alter. Should be passed by reference so that original form
* element will be altered.
* E.g.: apachesolr_autocomplete_do_alter(&$form['xyz'])
* @param $form
* The form being altered.
* @param $context_id
* A string identifying the form. For example: apachesolr_search_page:core_search
*/
function apachesolr_autocomplete_do_alter(&$element, $form, $context_id) {
drupal_set_message("Altering form for context {$context_id}...");
// TODO: DEBUG
// The unique element ID for this form's keyword search element.
$autocomplete_element_id = $form['#id'];
// Create elements if they do not exist.
if (!isset($element['#attributes'])) {
$element['#attributes'] = array();
}
if (!isset($element['#attributes']['class'])) {
$element['#attributes']['class'] = array();
}
array_push($element['#attributes']['class'], 'apachesolr-autocomplete');
// Specify path to autocomplete handler.
$autocomplete_path = 'apachesolr_autocomplete_callback/' . $context_id;
// Add data-apachesolr-autocomplete attribute to element.
$element['#attributes']['data-apachesolr-autocomplete-id'] = array(
$autocomplete_element_id,
);
// Build a settings array.
$settings = array(
'id' => $autocomplete_element_id,
// helps identify the element on jQuery
'path' => url($autocomplete_path),
);
// Add our JS settings to the current page.
drupal_add_js(array(
'apachesolr_autocomplete' => array(
$autocomplete_element_id => $settings,
),
), 'setting');
}
/**
* Implementation of hook_menu().
*/
function apachesolr_autocomplete_menu() {
$items = array();
// Pattern is: apachesolr_autocomplete_callback/[context_id]
$items['apachesolr_autocomplete_callback/%'] = array(
'page callback' => 'apachesolr_autocomplete_callback',
'page arguments' => array(
1,
),
'access callback' => 'user_access',
'access arguments' => array(
'search content',
),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Returns all the available engines to handle autocomplete.
*
* TODO: This should call a hook, whose implementations should return the info.
* @return array
*/
function apachesolr_autocomplete_engines() {
$suggestion_engines = array(
'word_completion' => array(
'enabled' => TRUE,
),
'additional_term' => array(
'enabled' => TRUE,
'spellcheck_suggest' => TRUE,
'suggest_keywords' => TRUE,
),
);
return $suggestion_engines;
}
/**
* Return the default config for the current context.
* @param $context_id
* A string identifying the form. For example: apachesolr_search_page:core_search
* @return array
*/
function apachesolr_autocomplete_get_default_context($context_id) {
$suggestions_to_return = 5;
// For search pages.
if (strpos($context_id, 'apachesolr_search_page:') === 0) {
$search_page_id = substr($context_id, 23);
}
else {
$search_page_id = 'core_search';
}
$search_page = apachesolr_search_page_load($search_page_id);
if (!$search_page) {
return FALSE;
}
// Include any settings provided by apachesolr_search, if defined.
$params = apachesolr_search_conditions_default($search_page);
// Set various parameters we'll use in autocomplete.
$params['facet'] = 'true';
$params['facet.field'] = array(
'spell',
);
// We ask for $suggestions_to_return * 5 facets, because we want
// not-too-frequent terms (will be filtered below). 5 is just my best guess.
$params['facet.limit'] = $suggestions_to_return * 5;
$params['facet.mincount'] = 1;
$params['start'] = 0;
$params['rows'] = 0;
$solr = apachesolr_get_solr($search_page['env_id']);
// Return the context.
$context = array(
'context_id' => $context_id,
'search_page' => $search_page,
'solr' => $solr,
'apachesolr_params' => $params,
'engines' => apachesolr_autocomplete_engines(),
'suggestions_to_return' => 5,
'spellcheck_suggest' => TRUE,
'cache_max_age' => 60,
);
return $context;
}
/**
* Callback for url apachesolr_autocomplete/autocomplete.
* @param $context_id
* A string identifying the form. For example: apachesolr_search_page:core_search
*/
function apachesolr_autocomplete_callback($context_id) {
// Get user entry
$keys = isset($_GET['term']) ? $_GET['term'] : '';
if (!$keys) {
// Exit quickly.
drupal_json_output(array());
exit;
}
// TODO: Get more user entry, like state of 'retain current filters' and current filters, etc.
// Get the configuration for the context_id
$context = apachesolr_autocomplete_get_default_context($context_id);
// Exit quickly if no context exists.
if (!$context) {
drupal_json_output(array());
exit;
}
// TODO: Override the context with any UI-based configuration.
// TODO: Override the context with user entry, like state of 'retain current filters' and current filters, etc. See .autocomplete(... source:...) on on apachesolr_autocomplete.js
// Allow other modules to alter the context.
drupal_alter('apachesolr_autocomplete_context', $context);
// Run suggestion engines.
$suggestions = apachesolr_autocomplete_invoke($keys, $context);
// Output caching headers
#header("Cache-Control: public, max-age=" . $context['cache_max_age']); // TODO: DEBUG.
drupal_json_output(array_values($suggestions));
#print_r($context); // TODO: DEBUG.
#print_r($suggestions); // TODO: DEBUG.
exit;
}
/**
* Given a set of keys, invoke all the available autocomplete engines and build an array of suggestions.
* @param $keys
* String to autocomplete on.
* @param $context
* Configuration to build the autocomplete.
* @return array
* List of suggestions.
*/
function apachesolr_autocomplete_invoke($keys, $context) {
$suggestions = array();
// Process each enabled engine.
foreach ($context['engines'] as $engine_id => $engine) {
if ($engine['enabled'] === TRUE) {
$callback = 'apachesolr_autocomplete_suggest_' . $engine_id;
$results = $callback($keys, $context);
drupal_alter('apachesolr_autocomplete_suggestions', $results, $engine_id);
if ($results) {
$results = apachesolr_autocomplete_fix_indexes($results);
$suggestions = array_merge($suggestions, $results);
}
}
}
return $suggestions;
}
/**
* Convert the keys in an array from '*foo' to 'foo'
* @param $results
* An array to convert.
* @return array
* The converted array.
*/
function apachesolr_autocomplete_fix_indexes($results) {
$new = array();
foreach ($results as $key => $value) {
$new_key = substr($key, 1);
$new[$new_key] = $value;
$new[$new_key]['id'] = $new_key;
}
return $new;
}
/**
* Themes each returned suggestion.
* TODO: Move theming functions into JS.
*/
function theme_apachesolr_autocomplete_highlight($variables) {
static $first = TRUE;
$html = '';
$html .= '<div class="apachesolr_autocomplete suggestion">';
$html .= '<strong>' . drupal_substr($variables['suggestion'], 0, strlen($variables['keys'])) . '</strong>' . drupal_substr($variables['suggestion'], strlen($variables['keys']));
$html .= '</div>';
if ($variables['count'] && $variables['show_counts']) {
if ($first) {
$html .= "<div class='apachesolr_autocomplete message' style='float:right'>";
$html .= t('!count results', array(
'!count' => $variables['count'],
));
$html .= "</div><br style='clear:both'>";
$first = false;
}
else {
$html .= "<div class='apachesolr_autocomplete message count'>" . $variables['count'] . "</div><br style='clear:both'>";
}
}
return $html;
}
/**
* Themes the spellchecker's suggestion.
* TODO: Move theming functions into JS.
*/
function theme_apachesolr_autocomplete_spellcheck($variables) {
return '<span class="apachesolr_autocomplete message">' . t('Did you mean') . ':</span> ' . $variables['suggestion'];
}
/**
* Helper function that suggests ways to complete partial words.
*
* For example, if $keys = "lea", this might return suggestions like:
* lean, learn, lease.
* The suggested terms are returned in order of frequency (most frequent first).
*
*/
function apachesolr_autocomplete_suggest_word_completion($keys, $context) {
/**
* Split $keys into two:
* $first_part will contain all complete words (delimited by spaces). Can be empty.
* $last_part is the (assumed incomplete) last word. If this is empty, don't suggest.
* Example:
* $keys = "learning dis" : $first_part = "learning", $last_part = "dis"
*/
preg_match('/^(:?(.* |))([^ ]+)$/', $keys, $matches);
$first_part = @$matches[2];
// Make sure $last_part contains meaningful characters
$last_part = preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', '', @$matches[3]);
if ($last_part == '') {
return array();
}
// Ask Solr to return facets that begin with $last_part; these will be the suggestions.
$params = $context['apachesolr_params'];
$params['facet.prefix'] = $last_part;
// Get array of themed suggestions.
$result = apachesolr_autocomplete_suggest($context['solr'], $first_part, $params, $context['suggestions_to_return']);
if (!$result) {
return array();
}
// Concatenate the original terms with the returned suggestion.
$tmp = array();
foreach ($result['suggestions'] as $key => $value) {
$key = substr($key, 1);
$value['value'] = $first_part . $value['value'];
$tmp["*" . $value['value']] = $value;
}
return $tmp;
}
/**
* Helper function that suggests additional terms to search for.
*
* For example, if $keys = "learn", this might return suggestions like:
* learn student, learn school, learn mathematics.
* The suggested terms are returned in order of frequency (most frequent first).
*/
function apachesolr_autocomplete_suggest_additional_term($keys, $context) {
$keys = trim($keys);
$keys = check_plain($keys);
if ($keys == '') {
return array();
}
// Return no suggestions when $keys consists of only word delimiters
if (drupal_strlen(preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', '', $keys)) < 1) {
return array();
}
// Initialize arrays
$suggestions = array();
// Get array of suggestions.
$result = apachesolr_autocomplete_suggest($context['solr'], $keys, $context['apachesolr_params'], $context['suggestions_to_return']);
if ($result && $context['engines']['additional_term']['suggest_keywords']) {
if (isset($result['suggestions']) && sizeof($result['suggestions'])) {
// Concatenate the original terms with the returned suggestion.
$tmp = array();
foreach ($result['suggestions'] as $key => $value) {
$key = substr($key, 1);
$value['value'] = $keys . ' ' . $value['value'];
$tmp["*{$keys} {$key}"] = $value;
}
$suggestions = array_merge($suggestions, $tmp);
}
}
// Suggest using the spellchecker.
if ($context['engines']['additional_term']['spellcheck_suggest']) {
if (isset($result['response']->spellcheck) && isset($result['response']->spellcheck->suggestions)) {
$spellcheck_suggestions = get_object_vars($result['response']->spellcheck->suggestions);
$replacements = array();
foreach ($spellcheck_suggestions as $word => $value) {
$replacements[$word] = $value->suggestion[0];
}
if (count($replacements)) {
$new_keywords = strtr($keys, $replacements);
if ($new_keywords != $keys) {
// Place spellchecker suggestion before others
$suggestion = array(
'value' => $new_keywords,
'theme' => 'spellchecker',
);
$suggestions = array_merge(array(
'*' . $new_keywords => $suggestion,
), $suggestions);
}
}
}
}
return $suggestions;
}
function apachesolr_autocomplete_suggest(DrupalApacheSolrServiceInterface $solr, $term, $params, $suggestions_to_return = 5) {
$matches = array();
$suggestions = array();
$term = trim($term);
// We need the keys array to make sure we don't suggest words that are already
// in the search terms.
$keys_array = explode(' ', $term);
$keys_array = array_filter($keys_array);
$query = apachesolr_drupal_query("apachesolr", $params, '', '', $solr);
// Add spellcheck parameters.
apachesolr_search_add_spellcheck_params($query);
// Add fields to search in (qf)
apachesolr_search_add_boost_params($query);
// Allow modules to modify the query object.
drupal_alter('apachesolr_autocomplete_query', $query);
if (!$query) {
return array();
}
// Query Solr
$response = $query
->search($term);
// Loop through requested facet.fields and get returned suggestions.
foreach ($params['facet.field'] as $field) {
#echo "Returned facets:\n"; // TODO: DEBUG
#print_r((array)$response->facet_counts->facet_fields->{$field}); // TODO: DEBUG
foreach ($response->facet_counts->facet_fields->{$field} as $terms => $count) {
$terms = preg_replace('/[_-]+/', ' ', $terms);
foreach (explode(' ', $terms) as $term) {
// Trim the $term.
$term = trim(preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', '', $term));
if ($term) {
if (isset($matches[$term])) {
$matches[$term] += $count;
}
else {
$matches[$term] = $count;
}
}
}
}
}
if (sizeof($matches) > 0) {
// Eliminate suggestions that are stopwords or are already in the query.
$matches_clone = $matches;
$stopwords = apachesolr_autocomplete_get_stopwords($solr);
foreach ($matches_clone as $term => $count) {
if (strlen($term) > 3 && !in_array($term, $stopwords) && !array_search($term, $keys_array)) {
// Longer strings get higher ratings.
#$matches_clone[$term] += strlen($term);
}
else {
unset($matches_clone[$term]);
unset($matches[$term]);
}
}
// Don't suggest terms that are too frequent (in >90% of results).
$max_occurrence = $response->response->numFound * 0.9;
foreach ($matches_clone as $match => $count) {
if ($count > $max_occurrence) {
unset($matches_clone[$match]);
}
}
// The $count in this array is actually a score. We want the highest ones first.
arsort($matches_clone);
// Shorten the array to the right ones.
$matches_clone = array_slice($matches_clone, 0, $suggestions_to_return, TRUE);
// Add current search as suggestion if results > 0
if ($response->response->numFound > 0 && $term != '') {
// Add * to array element key to force into a string, else PHP will
// renumber keys that look like numbers on the returned array.
$suggestions['*' . $term] = array(
'value' => $term,
'count' => $response->response->numFound,
);
}
// Build suggestions using returned facets
foreach ($matches_clone as $match => $count) {
if ($term != $match) {
$suggestion = trim($match);
// On cases where there are more than 3 keywords, omit displaying
// the count because of the mm settings in solrconfig.xml
if (substr_count($suggestion, ' ') >= 2) {
$count = FALSE;
}
if ($suggestion != '') {
// Add * to array element key to force into a string, else PHP will
// renumber keys that look like numbers on the returned array.
$suggestions['*' . $suggestion] = array(
'value' => $suggestion,
'count' => $count,
);
}
}
}
}
return array(
'suggestions' => $suggestions,
'response' => &$response,
);
}
/**
* Gets the current stopwords list configured in Solr.
*/
function apachesolr_autocomplete_get_stopwords(DrupalApacheSolrServiceInterface $solr) {
static $words = array(), $cached_flag = false;
// Try static cache.
if ($cached_flag) {
return $words;
}
// Try Drupal cache.
$cid = 'apachesolr_autocomplete_stopwords';
$cached = cache_get($cid);
if ($cached && $cached->expire > REQUEST_TIME) {
return $cached->data;
}
try {
$response = $solr
->makeServletRequest('admin/file', array(
'file' => 'stopwords.txt',
));
} catch (Exception $e) {
$cached_flag = TRUE;
return $words;
}
foreach (explode("\n", $response->data) as $line) {
if (drupal_substr($line, 0, 1) == "#") {
continue;
}
if ($word = trim($line)) {
$words[] = $word;
}
}
// Cache in Drupal cache for 10 minutes.
cache_set($cid, $words, 'cache', REQUEST_TIME + 600);
$cached_flag = TRUE;
return $words;
}
/**
* Alter the apachesolr.module "advanced settings" form.
* TODO: Move this off onto each engine.
*/
function apachesolr_autocomplete_form_apachesolr_settings_alter(&$form, $form_state) {
$form['advanced']['apachesolr_autocomplete_suggest_keywords'] = array(
'#type' => 'checkbox',
'#title' => t('Enable additional keyword suggestions on the autocomplete widget'),
'#description' => t('Suggest words to add to the currently typed-in words. E.g.: typing "blue" might suggest "blue bike" or "blue shirt".'),
);
$form['advanced']['apachesolr_autocomplete_suggest_spellcheck'] = array(
'#type' => 'checkbox',
'#title' => t('Enable spellchecker suggestions on the autocomplete widget'),
'#description' => t('Suggest corrections to the currently typed-in words. E.g.: typing "rec" or "redd" might suggest "red".'),
);
$form['advanced']['apachesolr_autocomplete_counts'] = array(
'#type' => 'checkbox',
'#title' => t('Enable counts in autocomplete widget suggestions'),
'#description' => t('WARNING: Counts shown alongside suggestions might be lower than the actual result count due to stemming and minimum match (mm) settings in solrconfig.xml.'),
);
}
Functions
Name | Description |
---|---|
apachesolr_autocomplete_callback | Callback for url apachesolr_autocomplete/autocomplete. |
apachesolr_autocomplete_do_alter | Helper function to do the actual altering of search forms. |
apachesolr_autocomplete_engines | Returns all the available engines to handle autocomplete. |
apachesolr_autocomplete_fix_indexes | Convert the keys in an array from '*foo' to 'foo' |
apachesolr_autocomplete_form_alter | |
apachesolr_autocomplete_form_apachesolr_search_custom_page_search_form_alter | Implementation of hook_form_FORM_ID_alter(). |
apachesolr_autocomplete_form_apachesolr_settings_alter | Alter the apachesolr.module "advanced settings" form. TODO: Move this off onto each engine. |
apachesolr_autocomplete_form_search_block_form_alter | Implementation of hook_form_FORM_ID_alter(). |
apachesolr_autocomplete_form_search_form_alter | Implementation of hook_form_FORM_ID_alter(). |
apachesolr_autocomplete_get_default_context | Return the default config for the current context. |
apachesolr_autocomplete_get_stopwords | Gets the current stopwords list configured in Solr. |
apachesolr_autocomplete_init | Implementation of hook_init(). |
apachesolr_autocomplete_invoke | Given a set of keys, invoke all the available autocomplete engines and build an array of suggestions. |
apachesolr_autocomplete_menu | Implementation of hook_menu(). |
apachesolr_autocomplete_suggest | |
apachesolr_autocomplete_suggest_additional_term | Helper function that suggests additional terms to search for. |
apachesolr_autocomplete_suggest_word_completion | Helper function that suggests ways to complete partial words. |
theme_apachesolr_autocomplete_highlight | Themes each returned suggestion. TODO: Move theming functions into JS. |
theme_apachesolr_autocomplete_spellcheck | Themes the spellchecker's suggestion. TODO: Move theming functions into JS. |