1<?php 2/** 3 * 4 * @package solr 5 * @author Gabriel Birke <birke@d-scribe.de> 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 */ 8 9if(!defined('DOKU_INC')) die(); 10if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 11require_once(DOKU_PLUGIN.'action.php'); 12require_once(dirname(__FILE__).'/AddDocument.php'); 13require_once(dirname(__FILE__).'/Pageinfo.php'); 14require_once dirname(__FILE__).'/ConnectionException.php'; 15 16class action_plugin_solr extends DokuWiki_Action_Plugin { 17 18 const PAGING_SIZE = 100; 19 20 /** 21 * Quuery params used in all search requests to Solr 22 * 23 * @var array 24 */ 25 protected $common_params = array( 26 'q.op' => 'AND', 27 'wt' => 'phps', 28 'debugQuery' => 'false', 29 'start' => 0 30 ); 31 32 /** 33 * Query params used in search requests to Solr that highlight snippets 34 * 35 * @var array 36 */ 37 protected $highlight_params = array( 38 'hl' => 'true', 39 'hl.fl' => 'content', 40 'hl.snippets' => 4, 41 'hl.simple.pre' => '!!SOLR_HIGH!!', 42 'hl.simple.post' => '!!END_SOLR_HIGH!!' 43 ); 44 45 public $highlight2html = array( 46 '!!SOLR_HIGH!!' => '<strong class="search_hit">', 47 '!!END_SOLR_HIGH!!' => '</strong>' 48 ); 49 50 protected $allowed_actions = array('solr_search', 'solr_adv_search'); 51 52 /** 53 * return some info 54 */ 55 function getInfo(){ 56 return array( 57 'author' => 'Gabriel Birke', 58 'email' => 'birke@d-scribe.de', 59 'date' => '2011-12-21', 60 'name' => 'Solr (Action component)', 61 'desc' => 'Update the Solr index during the indexing event, show search page.', 62 'url' => 'http://www.d-scribe.de/', 63 ); 64 } 65 66 /** 67 * Register the handlers with the dokuwiki's event controller 68 */ 69 function register(&$controller) { 70 $controller->register_hook('INDEXER_TASKS_RUN', 'BEFORE', $this, 'updateindex'); 71 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'allowsearchpage'); 72 $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'dispatch_search'); 73 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'quicksearch'); 74 $controller->register_hook('IO_WIKIPAGE_WRITE', 'AFTER', $this, 'delete_index'); 75 } 76 77 /** 78 * Update Solr index 79 * 80 * This event handler is called from the lib/exe/indexer.php file. 81 */ 82 function updateindex(&$event, $param) { 83 global $ID; 84 $helper = $this->loadHelper('solr', true); 85 86 // Look for index file, if not modified, return 87 if(!$helper->needs_indexing($ID)){ 88 print "solr_indexer: index for $ID up to date".NL; 89 return; 90 } 91 92 // get index lock 93 $lock = $helper->lock_index(); 94 95 // gather page info 96 $writer = new XmlWriter(); 97 $writer->openMemory(); 98 $info = new Solr_Pageinfo($ID); 99 $doc = new Solr_AddDocument($writer); 100 $doc->start(); 101 $doc->addPage($info->getFields()); 102 $doc->end(); 103 104 // post to SOLR 105 try { 106 $result = $helper->solr_query('update', 'commit=true', 'POST', $writer->outputMemory()); 107 $xml = simplexml_load_string($result); 108 // Check response 109 if($xml->getName() != "response") { 110 print "solr_indexer: Unexpected response:\n$result\n"; 111 } 112 else { 113 print "solr_indexer: index was updated\n"; 114 // update index file 115 $helper->update_idxfile($ID); 116 } 117 } 118 catch(ConnectionException $e) { 119 print "solr_indexer: Request failed: ".$e->getMessage().NL; 120 } 121 122 // release lock 123 @rmdir($lock); 124 125 // Stop event propagation to avoid script timeout 126 $event->preventDefault(); 127 $event->stopPropagation(); 128 } 129 130 /** 131 * Event handler for displaying the search result page 132 */ 133 function dispatch_search(&$event, $param) { 134 // only handle our actions 135 if(!in_array($event->data, $this->allowed_actions)) { 136 return; 137 } 138 $method = 'page_'.$event->data; 139 $this->$method(); 140 141 $event->preventDefault(); 142 $event->stopPropagation(); 143 } 144 145 /** 146 * Display advanced search form and handle the sent form fields 147 */ 148 protected function page_solr_adv_search() { 149 global $QUERY; 150 $helper = $this->loadHelper('solr', true); 151 echo $helper->htmlAdvancedSearchform(); 152 153 // Build search string 154 $q = ''; 155 if(!empty($_REQUEST['search_plus'])) { 156 $val = utf8_stripspecials(utf8_strtolower($_REQUEST['search_plus'])); 157 $q .= $this->search_words($val, '+', '*'); 158 } 159 elseif(!empty($QUERY)) { 160 $val = utf8_stripspecials(utf8_strtolower($QUERY)); 161 $q .= $this->search_words($val, '+', '*'); 162 } 163 if(!empty($_REQUEST['search_exact'])) { 164 $q .= ' +"'.$_REQUEST['search_exact'].'"'; 165 } 166 if(!empty($_REQUEST['search_minus'])) { 167 $val = utf8_stripspecials(utf8_strtolower($_REQUEST['search_minus'])); 168 $q .= $this->search_words($val, '-', '*'); 169 } 170 if(!empty($_REQUEST['search_ns'])) { 171 foreach($_REQUEST['search_ns'] as $ns) { 172 if(($ns = trim($ns)) != '') { 173 $q .= ' idpath:'.strtr($ns, ':','/'); 174 } 175 } 176 } 177 if(!empty($_REQUEST['search_fields'])) { 178 foreach($_REQUEST['search_fields'] as $key => $value) { 179 //$value = utf8_stripspecials(utf8_strtolower($value)); 180 if(!$value) { 181 continue; 182 } 183 $q .= $this->search_words($value, ''.$key.':', '*'); 184 } 185 } 186 $q = trim($q); // remove first space 187 // Don't search with empty params 188 if(!$q) { 189 return; 190 } 191 192 $content_params = array_merge($this->common_params, $this->highlight_params, array( 193 'q' => $q, 194 'rows' => self::PAGING_SIZE, 195 // 'q.op' => 'OR' 196 )); 197 //print("<p>search string: $q</p>"); 198 print $this->locale_xhtml('searchpage'); 199 print '<div class="search_allresults">'; 200 $this->search_query($content_params); 201 print '</div>'; 202 203 } 204 205 protected function search_words($str, $prefix='', $suffix='') { 206 $words = preg_split('/\s+/', $str); 207 $search_words = ''; 208 foreach($words as $w) { 209 $search_words .= ' ' . $prefix . $w . $suffix; 210 } 211 return $search_words; 212 } 213 214 /** 215 * Do a simple search and display search results 216 */ 217 protected function page_solr_search() { 218 global $QUERY; 219 $val = utf8_strtolower($QUERY); 220 $q_title .= $this->search_words($val, 'title:', '*'); 221 $q_text .= $this->search_words($val, '', '*'); 222 223 // Prepare the parameters to be sent to Solr 224 $title_params = array_merge($this->common_params, array('q' => $q_title, 'rows' => self::PAGING_SIZE)); 225 $content_params = array_merge($this->common_params, $this->highlight_params, array( 226 'q' => $q_text, 227 'rows' => self::PAGING_SIZE, 228 'x-dw-query-type' => 'content' // Dummy parameter to make this query identifyable in handlers for the SOLR_QUERY event 229 )); 230 231 // Other plugins can manipulate the parameters 232 trigger_event('SOLR_QUERY_TITLE', $title_params); 233 trigger_event('SOLR_QUERY_CONTENT', $content_params); 234 235 $query_str_title = substr($this->array2paramstr($title_params), 1); 236 $helper = $this->loadHelper('solr', true); 237 238 // Build HTML result 239 print $this->locale_xhtml('searchpage'); 240 flush(); 241 242 //do a search for page titles 243 try { 244 $title_result = unserialize($helper->solr_query('select', $query_str_title)); 245 } 246 catch(ConnectionException $e) { 247 echo $this->getLang('search_failed'); 248 } 249 if(!empty($title_result['response']['docs'])){ 250 print '<div class="search_quickresult">'; 251 print '<h3>'.$this->getLang('quickhits').':</h3>'; 252 $helper->html_render_titles($title_result, 'search_quickhits'); 253 print '<div class="clearer"> </div>'; 254 print '</div>'; 255 } 256 flush(); 257 258 // Output search 259 print '<div class="search_allresults">'; 260 $this->search_query($content_params); 261 print '</div>'; 262 } 263 264 /** 265 * Query Solr and render search result. 266 * 267 * If the result contains more documents than the PAGING_SIZE constant, 268 * do another Solr request with increased 'start' parameter. 269 * 270 * @param array $params Solr Search query params 271 */ 272 protected function search_query($params){ 273 global $QUERY; 274 $helper = $this->loadHelper('solr', true); 275 $start = empty($params['start']) ? 0 : $params['start']; 276 $query_str = substr($this->array2paramstr($params), 1); 277 // Solr query for content 278 try { 279 $content_result = unserialize($helper->solr_query('select', $query_str)); 280 //echo "<pre>";print_r($content_result);echo "</pre>"; 281 } 282 catch(Exception $e) { 283 echo $this->getLang('search_failed'); 284 return; 285 } 286 $q_arr = preg_split('/\s+/', $QUERY); 287 $num_snippets = $this->getConf('num_snippets'); 288 if(!empty($content_result['response']['docs'])){ 289 $num = $start+1; 290 if(!$start) { 291 print '<h3>'.$this->getLang('all_hits').':</h3>'; 292 } 293 foreach($content_result['response']['docs'] as $doc){ 294 $id = $doc['id']; 295 if(auth_quickaclcheck($id) < AUTH_READ) { 296 continue; 297 } 298 $data = array('result' => $content_result, 'id' => $id, 'html' => array()); 299 $data['html']['head'] = html_wikilink(':'.$id, useHeading('navigation')?null:$id, $q_arr); 300 if(!$num_snippets || $num < $num_snippets){ 301 if(!empty($content_result['highlighting'][$id]['content'])){ 302 // Escape <code> and other tags 303 $highlight = htmlspecialchars(implode('... ', $content_result['highlighting'][$id]['content'])); 304 // replace highlight placeholders with HTML 305 $highlight = str_replace( 306 array_keys($this->highlight2html), 307 array_values($this->highlight2html), 308 $highlight 309 ); 310 $data['html']['body'] = '<div class="search_snippet">'.$highlight.'</div>'; 311 } 312 } 313 $num++; 314 // Enable plugins to add data or render result differently. 315 print trigger_event('SOLR_RENDER_RESULT_CONTENT', $data, array($this, '_render_content_search_result')); 316 flush(); 317 } 318 if($content_result['response']['numFound'] > $content_result['response']['start'] + self::PAGING_SIZE) { 319 $params['start'] = $content_result['response']['start'] + self::PAGING_SIZE; 320 $this->search_query($params); 321 } 322 } 323 elseif(!$start) { // if the first search result returned nothing, print nothing found message 324 print '<div class="nothing">'.$this->getLang('nothingfound').'</div>'; 325 } 326 } 327 328 public function _render_content_search_result($data) { 329 return '<div class="search_result">'.implode('', $data['html']).'</div>'; 330 } 331 332 /** 333 * Convert an associative array to a parameter string. 334 * Array values are urlencoded 335 * 336 * @param array $params 337 * @return string 338 */ 339 protected function array2paramstr($params) { 340 $paramstr = ''; 341 foreach($params as $p => $v) { 342 $paramstr .= '&'.$p.'='.rawurlencode($v); 343 } 344 return $paramstr; 345 } 346 347 /** 348 * Allow the solr_search action if the global variable $QUERY is not empty 349 */ 350 public function allowsearchpage(&$event, $param) { 351 global $QUERY; 352 if(!in_array($event->data, $this->allowed_actions)) return; 353 if(!$QUERY && $event->data == 'solr_search') { 354 $event->data = 'show'; 355 return; 356 } 357 $event->preventDefault(); 358 } 359 360 /** 361 * Handle AJAX request for quickly displaying titles 362 */ 363 public function quicksearch(&$event, $params){ 364 if($event->data != 'solr_qsearch') { 365 return; 366 } 367 $q_arr = preg_split('/\s+/', $_REQUEST['q']); 368 $q_title = ''; 369 // construct query string with field name and wildcards 370 foreach($q_arr as $val) { 371 $val = utf8_stripspecials(utf8_strtolower($val)); 372 $q_title .= ' title:'.$val.'*'; 373 } 374 $title_params = array_merge($this->common_params, array('q' => $q_title)); 375 // Other plugins can manipulate the parameters 376 trigger_event('SOLR_QUERY_TITLE', $title_params); 377 378 $query_str_title = substr($this->array2paramstr($title_params), 1); 379 380 $helper = $this->loadHelper('solr', true); 381 382 //do quick pagesearch 383 // Solr query for title 384 try { 385 $title_result = unserialize($helper->solr_query('select', $query_str_title)); 386 //echo "<pre>";print_r($title_result);echo "</pre>"; 387 } 388 catch(ConnectionException $e) { 389 echo $this->getLang('search_failed'); 390 } 391 392 if(!empty($title_result['response']['docs'])){ 393 print '<strong>'.$this->getLang('quickhits').'</strong>'; 394 $helper->html_render_titles($title_result); 395 } 396 flush(); 397 $event->preventDefault(); 398 $event->stopPropagation(); 399 } 400 401 /** 402 * This event handler deletes a page from the Solr index when it is deleted 403 * in the wiki. 404 */ 405 public function delete_index(&$event, $params){ 406 // If a revision is stored, do nothing 407 if(!empty($event->data[3])) { 408 return; 409 } 410 // If non-empty content is saved, do nothing 411 if(!empty($event->data[0][1])) { 412 return; 413 } 414 // create page ID from event data 415 $id = $event->data[1] ? "{$event->data[1]}:{$event->data[2]}" : $event->data[2]; 416 $helper = $this->loadHelper('solr', true); 417 418 // send delete command to Solr 419 $query = $this->array2paramstr(array( 420 'stream.body' => "<delete><id>{$id}</id></delete>", 421 'commit' => "true" 422 )); 423 try { 424 $helper->solr_query('update', $query); 425 } 426 catch(ConnectionException $e) { 427 msg($this->getLang('delete_failed'), -1); 428 dbglog($e->getMessage(), $this->getLang('delete_failed')); 429 } 430 } 431 432} 433