array('path' => url('apachesolr_autocomplete'))), 'setting'); } } /** * 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']['inline']['keys']; apachesolr_autocomplete_do_alter($element); } } /** * 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); } /** * Implementation of hook_form_FORM_ID_alter(). */ function apachesolr_autocomplete_form_search_theme_form_alter(&$form, $form_state) { $element = &$form['search_theme_form']; apachesolr_autocomplete_do_alter($element); } /** * 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']) */ function apachesolr_autocomplete_do_alter(&$element) { if (apachesolr_autocomplete_variable_get_widget() == 'custom') { // Create elements if they do not exist. if (!isset($element['#attributes'])) { $element['#attributes'] = array(); } if (!isset($element['#attributes']['class'])) { $element['#attributes']['class'] = ''; } $element['#attributes']['class'] .= ' apachesolr-autocomplete unprocessed'; } else { $element['#autocomplete_path'] = 'apachesolr_autocomplete'; } } /** * Implementation of hook_menu(). */ function apachesolr_autocomplete_menu() { $items = array(); $items['apachesolr_autocomplete'] = array( 'page callback' => 'apachesolr_autocomplete_callback', 'access callback' => 'user_access', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); return $items; } /** * Callback for url apachesolr_autocomplete/autocomplete. * @param $keys * The user-entered query. */ function apachesolr_autocomplete_callback($keys = '') { if (apachesolr_autocomplete_variable_get_widget() == 'custom') { // Keys for custom widget come from $_GET. $keys = $_GET['query']; } $suggestions = array(); $suggestions = array_merge($suggestions, apachesolr_autocomplete_suggest_word_completion($keys, 5)); $suggestions = array_merge($suggestions, apachesolr_autocomplete_suggest_additional_term($keys, 5)); $result = array(); if (apachesolr_autocomplete_variable_get_widget() == 'custom') { // Place suggestions into new array for returning as JSON. foreach ($suggestions as $key => $display) { $result[] = array( "key" => substr($key,1), "display" => $display ); } } else { foreach ($suggestions as $key => $display) { $result[substr($key,1)] = $display; } } drupal_json($result); exit(); } /** * Implementation of hook_theme(). */ function apachesolr_autocomplete_theme() { return array( 'apachesolr_autocomplete_highlight' => array( 'file' => 'apachesolr_autocomplete.module', 'arguments' => array( 'keys' => NULL, 'suggestion' => NULL, 'count' => NULL, ), ), 'apachesolr_autocomplete_spellcheck' => array( 'file' => 'apachesolr_autocomplete.module', 'arguments' => array( 'suggestion' => NULL, ), ), ); } /** * Themes each returned suggestion. */ function theme_apachesolr_autocomplete_highlight($keys, $suggestion, $count = 0) { static $first = true; $html = ''; $html .= '
'; $html .= '' . drupal_substr($suggestion, 0, strlen($keys)) . '' . drupal_substr($suggestion, strlen($keys)); $html .= '
'; if ($count) { if ($first) { $html .= "
"; $html .= t('!count results', array('!count' => $count)); $html .= "

"; $first = false; } else { $html .= "
$count

"; } } return $html; } /** * Themes the spellchecker's suggestion. */ function theme_apachesolr_autocomplete_spellcheck($suggestion) { return '' . t('Did you mean') .': ' . $suggestion; } /** * Helper function that suggests ways to complete partial words. * * For example, if $keys = "learn", this might return suggestions like: * learn, learning, learner, learnability. * The suggested terms are returned in order of frequency (most frequent first). * */ function apachesolr_autocomplete_suggest_word_completion($keys, $suggestions_to_return = 5) { /** * 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_SEARCH_EXCLUDE . ']+/u', '', $matches[3]); if ($last_part == '') { return array(); } // Ask Solr to return facets that begin with $last_part; these will be the suggestions. $params = array( 'facet' => 'true', 'facet.field' => array('spell'), 'facet.prefix' => $last_part, 'facet.limit' => $suggestions_to_return * 5, 'facet.mincount' => 1, 'start' => 0, 'rows' => 0, ); // Get array of themed suggestions. $result = apachesolr_autocomplete_suggest($first_part, $params, 'apachesolr_autocomplete_highlight', $keys, $suggestions_to_return); if ($result['suggestions']) { return $result['suggestions']; } else { return array(); } } /** * 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, $suggestions_to_return = 5) { $keys = trim($keys); if ($keys == '') { return array(); } // Return no suggestions when $keys consists of only word delimiters if (drupal_strlen(preg_replace('/[' . PREG_CLASS_SEARCH_EXCLUDE . ']+/u', '', $keys)) < 1) { return array(); } // Ask Solr to return facets from the 'spell' field to use as suggestions. $params = array( 'facet' => 'true', '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. 'facet.limit' => $suggestions_to_return * 5, 'facet.mincount' => 1, 'start' => 0, 'rows' => 0, ); // Initialize arrays $suggestions = array(); $replacements = array(); // Get array of themed suggestions. $result = apachesolr_autocomplete_suggest($keys, $params, 'apachesolr_autocomplete_highlight', $keys, $suggestions_to_return); if ($result['suggestions']) { $suggestions = array_merge($suggestions, $result['suggestions']); } // Suggest using the spellchecker if ($result['response']->spellcheck && $result['response']->spellcheck->suggestions) { $spellcheck_suggestions = get_object_vars($result['response']->spellcheck->suggestions); 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 $suggestions = array_merge(array('*' . $new_keywords => theme('apachesolr_autocomplete_spellcheck', $new_keywords)), $suggestions); } } } return $suggestions; } function apachesolr_autocomplete_suggest($keys, $params, $theme_callback, $orig_keys, $suggestions_to_return = 5) { $matches = array(); $suggestions = array(); $keys = trim($keys); // We need the keys array to make sure we don't suggest words that are already // in the search terms. $keys_array = explode(' ', $keys); $keys_array = array_filter($keys_array); // Query Solr for $keys so that suggestions will always return results. $query = apachesolr_drupal_query($keys); // Allow other modules to modify query. apachesolr_modify_query($query, $params, 'apachesolr_autocomplete'); if (!$query) { return array(); } $params += apachesolr_search_spellcheck_params($query); // Try to contact Solr. try { $solr = apachesolr_get_solr(); } catch (Exception $e) { watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR); return array(); } // Query Solr $response = $solr->search($keys, $params['start'], $params['rows'], $params); foreach ($params['facet.field'] as $field) { foreach ($response->facet_counts->facet_fields->{$field} as $terms => $count) { $terms = preg_replace('/[_-]+/', ' ', $terms); foreach (explode(' ', $terms) as $term) { if ($term = trim(preg_replace('/['. PREG_CLASS_SEARCH_EXCLUDE .']+/u', '', $term))) { $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(); 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_occurence = $response->response->numFound * 0.90; foreach ($matches_clone as $match => $count) { if ($count > $max_occurence) { 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 && $keys != '') { // Add * to array element key to force into a string, else PHP will // renumber keys that look like numbers on the returned array. $suggestions['*' . $keys] = theme('apachesolr_autocomplete_highlight', $keys, $keys, $response->response->numFound); } // Build suggestions using returned facets foreach ($matches_clone as $match => $count) { if ($keys != $match) { $suggestion = trim($keys . ' ' . $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 = 0; } 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] = theme('apachesolr_autocomplete_highlight', $orig_keys, $suggestion, $count); } } } } return array( 'suggestions' => $suggestions, 'response' => &$response ); } /** * Gets the current stopwords list configured in Solr. */ function apachesolr_autocomplete_get_stopwords() { static $words = array(), $flag = false; if ($flag) { return $words; } $stopwords_url = "/admin/file/?file=stopwords.txt"; $host = variable_get('apachesolr_host', 'localhost'); $port = variable_get('apachesolr_port', 8983); $path = variable_get('apachesolr_path', '/solr'); $url = "http://{$host}:{$port}{$path}{$stopwords_url}"; $result = drupal_http_request($url); if ($result->code != 200) { return array(); } $words = array(); foreach (explode("\n", $result->data) as $line) { if (drupal_substr($line, 0, 1) == "#") { continue; } if ($word = trim($line)) { $words[] = $word; } } $flag = true; return $words; } /** * Wrapper around variable_get() for variable apachesolr_autocomplete_widget. */ function apachesolr_autocomplete_variable_get_widget() { return variable_get('apachesolr_autocomplete_widget', 'custom'); } /** * Alter the apachesolr.module "advanced settings" form. */ function apachesolr_autocomplete_form_apachesolr_settings_alter(&$form, $form_state) { $form['advanced']['apachesolr_autocomplete_widget'] = array( '#type' => 'radios', '#title' => t('Autocomplete widget to use'), '#description' => t('The custom widget provides instant search upon selection, whereas the Drupal widget needs the user to hit Enter or click on the Search button. If you are having problems, try switching to the default Drupal autocomplete widget.'), '#options' => array('custom' => t('Custom autocomplete widget'), 'drupal' => t('Drupal core autocomplete widget')), '#default_value' => apachesolr_autocomplete_variable_get_widget(), ); }