comparison sites/all/modules/custom/solrsearch_autocomplete/solrsearch_autocomplete.module @ 0:015d06b10d37 default tip

initial
author dwinter
date Wed, 31 Jul 2013 13:49:13 +0200
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:015d06b10d37
1 <?php
2
3 /**
4 * @file
5 * Alters search forms to suggest terms using Apache Solr using AJAX.
6 * Thanks to:
7 * robertDouglass who contributed some of the code.
8 * sch4lly for contributing to D7 version
9 */
10
11 /**
12 * Implementation of hook_init().
13 */
14 function solrsearch_autocomplete_init() {
15
16 drupal_add_css( drupal_get_path('module', 'solrsearch_autocomplete') . '/solrsearch_autocomplete.css');
17
18 // If using custom JS widget, include files and settings.
19 if (solrsearch_autocomplete_variable_get_widget() == 'custom') {
20 // Add custom autocomplete files
21 drupal_add_js(drupal_get_path('module', 'solrsearch_autocomplete') .'/solrsearch_autocomplete.js');
22 drupal_add_js(drupal_get_path('module', 'solrsearch_autocomplete') .'/jquery-autocomplete/jquery.autocomplete.js');
23 drupal_add_css( drupal_get_path('module', 'solrsearch_autocomplete') .'/jquery-autocomplete/jquery.autocomplete.css');
24 // Specify path to autocomplete handler.
25 drupal_add_js(array('solrsearch_autocomplete' => array('path' => url('solrsearch_autocomplete'))), 'setting');
26 }
27 }
28
29 /**
30 * Implementation of hook_form_FORM_ID_alter().
31 */
32 function solrsearch_autocomplete_form_search_form_alter(&$form, $form_state) {
33
34 if ($form['module']['#value'] == 'solrsearch_search' || $form['module']['#value'] == 'solrsearch_multisitesearch') {
35 $element = &$form['basic']['keys'];
36 solrsearch_autocomplete_do_alter($element);
37 }
38 }
39
40 /**
41 * Implementation of hook_form_FORM_ID_alter().
42 */
43 function solrsearch_autocomplete_form_solrsearch_search_block_form_alter(&$form, $form_state) {
44
45 $element = &$form['solrsearch_search_block_form'];
46 solrsearch_autocomplete_do_alter($element,"title_s,title,author");
47 }
48
49 /**
50 * Implementation of hook_form_FORM_ID_alter().
51 */
52 function solrsearch_autocomplete_form_solrsearch_search_author_block_form_alter(&$form, $form_state) {
53
54 $element = &$form['solrsearch_search_author_block_form'];
55
56 solrsearch_autocomplete_do_alter($element,"author");
57 }
58
59 /**
60 * Implementation of hook_form_FORM_ID_alter().
61 */
62 function solrsearch_autocomplete_form_solrsearch_search_title_block_form_alter(&$form, $form_state) {
63
64 $element = &$form['solrsearch_search_title_block_form'];
65
66 solrsearch_autocomplete_do_alter($element,"title_s,title");
67 }
68 /**
69 * Helper function to do the actual altering of search forms.
70 *
71 * @param $element
72 * The element to alter. Should be passed by reference so that original form
73 * element will be altered.
74 * E.g.: solrsearch_autocomplete_do_alter(&$form['xyz'])
75 */
76 function solrsearch_autocomplete_do_alter(&$element,$field_name) {
77
78 if (solrsearch_autocomplete_variable_get_widget() == 'custom') {
79 // Create elements if they do not exist.
80 if (!isset($element['#attributes'])) {
81 $element['#attributes'] = array();
82 }
83 if (!isset($element['#attributes']['class'])) {
84 $element['#attributes']['class'] = array();
85 }
86 array_push($element['#attributes']['class'], 'solrsearch-autocomplete', 'unprocessed','solrsearch-autocomplete.field.'.$field_name);
87
88 }
89 else {
90 $element['#autocomplete_path'] = 'solrsearch_autocomplete';
91 }
92 }
93
94 /**
95 * Implementation of hook_menu().
96 */
97 function solrsearch_autocomplete_menu() {
98 $items = array();
99
100 $items['solrsearch_autocomplete'] = array(
101 'page callback' => 'solrsearch_autocomplete_callback',
102 'access callback' => 'user_access',
103 'access arguments' => array('search content'),
104 'type' => MENU_CALLBACK,
105 );
106 return $items;
107 }
108
109 /**
110 * Callback for url solrsearch_autocomplete/autocomplete.
111 * @param $keys
112 * The user-entered query.
113 */
114 function solrsearch_autocomplete_callback($keys = '', $field_name='') {
115
116
117 if (solrsearch_autocomplete_variable_get_widget() == 'custom') {
118 // Keys for custom widget come from $_GET.
119 $keys = $_GET['query'];
120 $field_names =$_GET['fieldName'];
121 }
122
123 /*
124 $res = split("\:",$keys);
125
126 if (count($res) == 2){
127 $keys=$res[1];
128 $field_name=$res[0];
129 } else {
130 $keys=$res[0];
131 $field_name="";
132 }
133 */
134
135 $suggestions = array();
136
137 foreach (explode(",",$field_names) as $field_name) {
138
139 $suggestions = array_merge($suggestions, solrsearch_autocomplete_suggest_word_completion($keys, 5,$field_name));
140 if (solrsearch_autocomplete_variable_get_suggest_keywords() || solrsearch_autocomplete_variable_get_suggest_spellcheck()) {
141 $suggestions = array_merge($suggestions, solrsearch_autocomplete_suggest_additional_term($keys, 5,$field_name));
142
143 }
144 }
145 $result = array();
146 if (solrsearch_autocomplete_variable_get_widget() == 'custom') {
147 // Place suggestions into new array for returning as JSON.
148 foreach ($suggestions as $key => $display) {
149 $result[] = array(
150 "key" => substr($key,1),
151 "display" => $display
152 );
153 }
154 }
155 else {
156 foreach ($suggestions as $key => $display) {
157 $result[substr($key,1)] =$display;
158 }
159 }
160 drupal_json_output($result);
161 exit();
162 }
163
164 /**
165 * Implementation of hook_theme().
166 */
167 function solrsearch_autocomplete_theme() {
168 return array(
169 'solrsearch_autocomplete_highlight' => array(
170 'file' => 'solrsearch_autocomplete.module',
171 'arguments' => array(
172 'keys' => NULL,
173 'suggestion' => NULL,
174 'count' => NULL,
175 ),
176 ),
177 'solrsearch_autocomplete_spellcheck' => array(
178 'file' => 'solrsearch_autocomplete.module',
179 'arguments' => array(
180 'suggestion' => NULL,
181 ),
182 ),
183 );
184 }
185
186 /**
187 * Themes each returned suggestion.
188 */
189 function theme_solrsearch_autocomplete_highlight($variables) {
190 static $first = true;
191 $html = '';
192 $html .= '<div class="solrsearch_autocomplete suggestion">';
193 $html .= '<strong>' . drupal_substr($variables['suggestion'], 0, strlen($variables['keys'])) . '</strong>' . drupal_substr($variables['suggestion'], strlen($variables['keys']));
194 $html .= '</div>';
195 if ($variables['count'] && $variables['show_counts']) {
196 if ($first) {
197 $html .= "<div class='solrsearch_autocomplete message' style='float:right'>";
198 $html .= t('!count results', array('!count' => $variables['count']));
199 $html .= "</div><br style='clear:both'>";
200 $first = false;
201 } else {
202 $html .= "<div class='solrsearch_autocomplete message count'>" . $variables['count'] . "</div><br style='clear:both'>";
203 }
204 }
205 return $html;
206 }
207
208 /**
209 * Themes the spellchecker's suggestion.
210 */
211 function theme_solrsearch_autocomplete_spellcheck($variables) {
212 return '<span class="solrsearch_autocomplete message">' . t('Did you mean') .':</span> ' . $variables['suggestion'];
213 }
214
215 /**
216 * Return the basic set of parameters for the Solr query.
217 *
218 * @param $suggestions_to_return
219 * Number of facets to return.
220 * @return array
221 */
222 function solrsearch_autocomplete_basic_params($suggestions_to_return,$facet_name) {
223 return array(
224 'facet' => 'true',
225 'facet.field' => array($facet_name),
226 // We ask for $suggestions_to_return * 5 facets, because we want
227 // not-too-frequent terms (will be filtered below). 5 is just my best guess.
228 'facet.limit' => $suggestions_to_return * 5,
229 'facet.mincount' => 1,
230 'start' => 0,
231 'rows' => 0,
232 );
233 }
234
235 /**
236 * Helper function that suggests ways to complete partial words.
237 *
238 * For example, if $keys = "learn", this might return suggestions like:
239 * learn, learning, learner, learnability.
240 * The suggested terms are returned in order of frequency (most frequent first).
241 *
242 */
243 function solrsearch_autocomplete_suggest_word_completion($keys, $suggestions_to_return = 5, $facet_name="author") {
244 /**
245 * Split $keys into two:
246 * $first_part will contain all complete words (delimited by spaces). Can be empty.
247 * $last_part is the (assumed incomplete) last word. If this is empty, don't suggest.
248 * Example:
249 * $keys = "learning dis" : $first_part = "learning", $last_part = "dis"
250 */
251 preg_match('/^(:?(.* |))([^ ]+)$/', $keys, $matches);
252 $first_part = @$matches[2];
253 // Make sure $last_part contains meaningful characters
254 $last_part = preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', '', @$matches[3]);
255 if ($last_part == '') {
256 return array();
257 }
258 // Ask Solr to return facets that begin with $last_part; these will be the suggestions.
259 $params = solrsearch_autocomplete_basic_params($suggestions_to_return,$facet_name);
260 $params['facet.prefix'] = $last_part;
261 // Get array of themed suggestions.
262 $result = solrsearch_autocomplete_suggest($first_part, $params, 'solrsearch_autocomplete_highlight', $keys, $suggestions_to_return);
263 if ($result && $result['suggestions']) {
264 return $result['suggestions'];
265 } else {
266 return array();
267 }
268 }
269
270 /**
271 * Helper function that suggests additional terms to search for.
272 *
273 * For example, if $keys = "learn", this might return suggestions like:
274 * learn student, learn school, learn mathematics.
275 * The suggested terms are returned in order of frequency (most frequent first).
276 */
277 function solrsearch_autocomplete_suggest_additional_term($keys, $suggestions_to_return = 5, $facet_name="author") {
278 $keys = trim($keys);
279 $keys = check_plain($keys);
280 if ($keys == '') {
281 return array();
282 }
283 // Return no suggestions when $keys consists of only word delimiters
284 if (drupal_strlen(preg_replace('/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']+/u', '', $keys)) < 1) {
285 return array();
286 }
287
288 // Ask Solr to return facets from the 'spell' field to use as suggestions.
289 $params = solrsearch_autocomplete_basic_params($suggestions_to_return,$facet_name);
290
291 // Initialize arrays
292 $suggestions = array();
293 $replacements = array();
294
295 // Get array of themed suggestions.
296 $result = solrsearch_autocomplete_suggest($keys, $params, 'solrsearch_autocomplete_highlight', $keys, $suggestions_to_return);
297 if ($result && solrsearch_autocomplete_variable_get_suggest_keywords()) {
298 if (isset($result['suggestions']) && sizeof($result['suggestions'])) {
299 $suggestions = array_merge($suggestions, $result['suggestions']);
300 }
301 }
302
303 // Suggest using the spellchecker
304 if (solrsearch_autocomplete_variable_get_suggest_spellcheck()) {
305 if (isset($result['response']->spellcheck) && isset($result['response']->spellcheck->suggestions)) {
306 $spellcheck_suggestions = get_object_vars($result['response']->spellcheck->suggestions);
307 foreach($spellcheck_suggestions as $word => $value) {
308 $replacements[$word] = $value->suggestion[0];
309 }
310 if (count($replacements)) {
311 $new_keywords = strtr($keys, $replacements);
312 if ($new_keywords != $keys) {
313 // Place spellchecker suggestion before others
314 $suggestions = array_merge(array('*' . $new_keywords => theme('solrsearch_autocomplete_spellcheck', array('suggestion' => $new_keywords))), $suggestions);
315 }
316 }
317 }
318 }
319
320 return $suggestions;
321 }
322
323
324 function solrsearch_autocomplete_suggest($keys, $params, $theme_callback, $orig_keys, $suggestions_to_return = 5) {
325 $matches = array();
326 $suggestions = array();
327 $keys = trim($keys);
328 $show_counts = solrsearch_autocomplete_variable_get_counts();
329
330 // We need the keys array to make sure we don't suggest words that are already
331 // in the search terms.
332 $keys_array = explode(' ', $keys);
333 $keys_array = array_filter($keys_array);
334
335 // Query Solr for $keys so that suggestions will always return results.
336 $query = solrsearch_drupal_query($keys);
337
338 // This hook allows modules to modify the query and params objects.
339 drupal_alter('solrsearch_query', $query);
340 if (!$query) {
341 return array();
342 }
343 solrsearch_search_add_spellcheck_params($query);
344 foreach ($params as $param => $paramValue) {
345 $query->addParam($param, $paramValue);
346 }
347 solrsearch_search_add_boost_params($query);
348
349 // Query Solr
350 $response = $query->search($keys);
351 // Loop through requested fields and get suggestions.
352 foreach ($params['facet.field'] as $field) {
353 foreach ($response->facet_counts->facet_fields->{$field} as $terms => $count) {
354
355
356
357 $terms = preg_replace('/[_-]+/', ' ', $terms);
358 $term=$terms;
359 if ($term) {
360
361 if (isset($matches[$term])) {
362 $matches[$term] += $count;
363 }
364 else {
365 $matches[$term] = $count;
366 }
367 }
368
369 }
370 }
371
372 if (sizeof($matches) > 0) {
373 // Eliminate suggestions that are stopwords or are already in the query.
374 $matches_clone = $matches;
375 $stopwords = solrsearch_autocomplete_get_stopwords();
376 foreach ($matches_clone as $term => $count) {
377 if ((strlen($term) > 3) && !in_array($term, $stopwords) && !array_search($term, $keys_array)) {
378 // Longer strings get higher ratings.
379 #$matches_clone[$term] += strlen($term);
380 }
381 else {
382 unset($matches_clone[$term]);
383 unset($matches[$term]);
384 }
385 }
386
387 // Don't suggest terms that are too frequent (in >90% of results).
388 $max_occurence = $response->response->numFound * 0.90;
389 foreach ($matches_clone as $match => $count) {
390 if ($count > $max_occurence) {
391 unset($matches_clone[$match]);
392 }
393 }
394
395 // The $count in this array is actually a score. We want the highest ones first.
396 arsort($matches_clone);
397
398 // Shorten the array to the right ones.
399 $matches_clone = array_slice($matches_clone, 0, $suggestions_to_return, TRUE);
400
401 // Add current search as suggestion if results > 0
402 if ($response->response->numFound > 0 && $keys != '') {
403 // Add * to array element key to force into a string, else PHP will
404 // renumber keys that look like numbers on the returned array.
405 $suggestions['*' . $keys] = theme('solrsearch_autocomplete_highlight', array('keys' => $keys, 'suggestion' => $keys, 'count' => $response->response->numFound, 'show_counts' => $show_counts));
406 }
407
408 // Build suggestions using returned facets
409 foreach ($matches_clone as $match => $count) {
410 if ($keys != $match) {
411 $suggestion = trim($keys . ' ' . $match);
412 // On cases where there are more than 3 keywords, omit displaying
413 // the count because of the mm settings in solrconfig.xml
414 if (substr_count($suggestion, ' ') >= 2) {
415 $count = 0;
416 }
417 if ($suggestion != '') {
418 // Add * to array element key to force into a string, else PHP will
419 // renumber keys that look like numbers on the returned array.
420 $suggestions['*' . $suggestion] = theme('solrsearch_autocomplete_highlight', array('keys' => $orig_keys, 'suggestion' => $suggestion, 'count' => $count, 'show_counts' => $show_counts));
421 }
422 }
423 }
424 }
425
426 return array(
427 'suggestions' => $suggestions,
428 'response' => &$response
429 );
430 }
431
432 /**
433 * Gets the current stopwords list configured in Solr.
434 */
435 function solrsearch_autocomplete_get_stopwords() {
436 static $words = array(), $flag = false;
437 if ($flag) {
438 return $words;
439 }
440 $stopwords_url = "/admin/file/?file=stopwords.txt";
441 $host = variable_get('solrsearch_host', 'localhost');
442 $port = variable_get('solrsearch_port', 8983);
443 $path = variable_get('solrsearch_path', '/solr');
444 $url = "http://{$host}:{$port}{$path}{$stopwords_url}";
445 $result = drupal_http_request($url);
446 if ($result->code != 200) {
447 return array();
448 }
449 $words = array();
450 foreach (explode("\n", $result->data) as $line) {
451 if (drupal_substr($line, 0, 1) == "#") {
452 continue;
453 }
454 if ($word = trim($line)) {
455 $words[] = $word;
456 }
457 }
458 $flag = true;
459 return $words;
460 }
461
462 /**
463 * Wrapper around variable_get() for variable solrsearch_autocomplete_widget.
464 */
465 function solrsearch_autocomplete_variable_get_widget() {
466 return variable_get('solrsearch_autocomplete_widget', 'custom');
467 }
468
469 /**
470 * Wrapper around variable_get() for variable solrsearch_autocomplete_suggest_keywords.
471 */
472 function solrsearch_autocomplete_variable_get_suggest_keywords() {
473 return variable_get('solrsearch_autocomplete_suggest_keywords', 1);
474 }
475
476 /**
477 * Wrapper around variable_get() for variable solrsearch_autocomplete_suggest_spellcheck.
478 */
479 function solrsearch_autocomplete_variable_get_suggest_spellcheck() {
480 return variable_get('solrsearch_autocomplete_suggest_spellcheck', 1);
481 }
482
483 /**
484 * Wrapper around variable_get() for variable solrsearch_autocomplete_counts.
485 */
486 function solrsearch_autocomplete_variable_get_counts() {
487 return variable_get('solrsearch_autocomplete_counts', TRUE);
488 }
489
490 /**
491 * Alter the solrsearch.module "advanced settings" form.
492 */
493 function solrsearch_autocomplete_form_solrsearch_settings_alter(&$form, $form_state) {
494 $form['advanced']['solrsearch_autocomplete_widget'] = array(
495 '#type' => 'radios',
496 '#title' => t('Autocomplete widget to use'),
497 '#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.'),
498 '#options' => array('custom' => t('Custom autocomplete widget'), 'drupal' => t('Drupal core autocomplete widget')),
499 '#default_value' => solrsearch_autocomplete_variable_get_widget(),
500 );
501 $form['advanced']['solrsearch_autocomplete_suggest_keywords'] = array(
502 '#type' => 'checkbox',
503 '#title' => t('Enable additional keyword suggestions on the autocomplete widget'),
504 '#description' => t('Suggest words to add to the currently typed-in words. E.g.: typing "blue" might suggest "blue bike" or "blue shirt".'),
505 '#default_value' => solrsearch_autocomplete_variable_get_suggest_keywords(),
506 );
507 $form['advanced']['solrsearch_autocomplete_suggest_spellcheck'] = array(
508 '#type' => 'checkbox',
509 '#title' => t('Enable spellchecker suggestions on the autocomplete widget'),
510 '#description' => t('Suggest corrections to the currently typed-in words. E.g.: typing "rec" or "redd" might suggest "red".'),
511 '#default_value' => solrsearch_autocomplete_variable_get_suggest_spellcheck(),
512 );
513 $form['advanced']['solrsearch_autocomplete_counts'] = array(
514 '#type' => 'checkbox',
515 '#title' => t('Enable counts in autocomplete widget suggestions'),
516 '#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.'),
517 '#default_value' => solrsearch_autocomplete_variable_get_counts(),
518 );
519 }