1<?php 2/** 3 * DokuWiki Plugin elasticsearch (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class action_plugin_elasticsearch_search extends DokuWiki_Action_Plugin { 13 14 /** 15 * Registers a callback function for a given event 16 * 17 * @param Doku_Event_Handler $controller DokuWiki's event controller object 18 * @return void 19 */ 20 public function register(Doku_Event_Handler $controller) { 21 22 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_preprocess'); 23 $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handle_action'); 24 25 } 26 27 /** 28 * allow our custom do command 29 * 30 * @param Doku_Event $event 31 * @param $param 32 */ 33 public function handle_preprocess(Doku_Event &$event, $param) { 34 if($event->data != 'elasticsearch') return; 35 $event->preventDefault(); 36 $event->stopPropagation(); 37 } 38 39 /** 40 * do the actual search 41 * 42 * @param Doku_Event $event 43 * @param $param 44 */ 45 public function handle_action(Doku_Event &$event, $param) { 46 if($event->data != 'elasticsearch') return; 47 $event->preventDefault(); 48 $event->stopPropagation(); 49 global $QUERY; 50 global $INPUT; 51 global $USERINFO; 52 53 /** @var helper_plugin_elasticsearch_client $hlp */ 54 $hlp = plugin_load('helper', 'elasticsearch_client'); 55 56 /** @var helper_plugin_elasticsearch_form $hlpform */ 57 $hlpform = plugin_load('helper', 'elasticsearch_form'); 58 59 $client = $hlp->connect(); 60 $index = $client->getIndex($this->getConf('indexname')); 61 62 // define the query string 63 $qstring = new \Elastica\Query\SimpleQueryString($QUERY); 64 65 // create the actual search object 66 $equery = new \Elastica\Query(); 67 $equery->setQuery($qstring); 68 $equery->setHighlight( 69 array( 70 "pre_tags" => array('ELASTICSEARCH_MARKER_IN'), 71 "post_tags" => array('ELASTICSEARCH_MARKER_OUT'), 72 "fields" => array( 73 $this->getConf('snippets') => new \StdClass(), 74 'title' => new \StdClass()) 75 ) 76 ); 77 78 // paginate 79 $equery->setSize($this->getConf('perpage')); 80 $equery->setFrom($this->getConf('perpage') * ($INPUT->int('p', 1, true) - 1)); 81 82 $subqueries = new \Elastica\Query\BoolQuery(); 83 84 // add group subquery 85 $groups = array('all'); 86 if(isset($USERINFO['grps'])) { 87 $groups = array_merge($groups, $USERINFO['grps']); 88 } 89 $groupSubquery = new \Elastica\Query\BoolQuery(); 90 foreach($groups as $group) { 91 $group = str_replace('-', '', strtolower($group)); 92 $term = new \Elastica\Query\Term(); 93 $term->setTerm('groups', $group); 94 $groupSubquery->addShould($term); 95 } 96 $subqueries->addMust($groupSubquery); 97 98 // add namespace filter 99 if($INPUT->has('ns')) { 100 $nsSubquery = new \Elastica\Query\BoolQuery(); 101 foreach($INPUT->arr('ns') as $ns) { 102 $term = new \Elastica\Query\Term(); 103 $term->setTerm('namespace', $ns); 104 $nsSubquery->addShould($term); 105 } 106 $subqueries->addMust($nsSubquery); 107 } 108 109 // set all filters 110 // FIXME is filtering the right thing here? 111 $equery->setPostFilter($subqueries); 112 113 // add aggregations for namespaces 114 $agg = new \Elastica\Aggregation\Terms('namespace'); 115 $agg->setField('namespace.keyword'); 116 $agg->setSize(25); 117 $equery->addAggregation($agg); 118 119 try { 120 $result = $index->search($equery); 121 $aggs = $result->getAggregations(); 122 123 $this->print_intro(); 124 $hlpform->tpl($aggs['namespace']['buckets']); 125 $this->print_results($result) && $this->print_pagination($result); 126 } catch(Exception $e) { 127 msg('Something went wrong on searching please try again later or ask an admin for help.<br /><pre>' . hsc($e->getMessage()) . '</pre>', -1); 128 } 129 } 130 131 /** 132 * Prints the introduction text 133 */ 134 protected function print_intro() { 135 global $QUERY; 136 global $ID; 137 global $lang; 138 139 // just reuse the standard search page intro: 140 $intro = p_locale_xhtml('searchpage'); 141 // allow use of placeholder in search intro 142 $pagecreateinfo = ''; 143 if (auth_quickaclcheck($ID) >= AUTH_CREATE) { 144 $pagecreateinfo = sprintf($lang['searchcreatepage'], $QUERY); 145 } 146 $intro = str_replace( 147 array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'), 148 array(hsc(rawurlencode($QUERY)), hsc($QUERY), $pagecreateinfo), 149 $intro 150 ); 151 echo $intro; 152 flush(); 153 } 154 155 /** 156 * Output the search results 157 * 158 * @param \Elastica\ResultSet $results 159 * @return bool true when results where shown 160 */ 161 protected function print_results($results) { 162 global $lang; 163 164 // output results 165 $found = $results->getTotalHits(); 166 167 if(!$found) { 168 echo '<h2>' . $lang['nothingfound'] . '</h2>'; 169 return (bool)$found; 170 } 171 172 echo '<dl class="search_results">'; 173 echo '<h2>' . sprintf($this->getLang('totalfound'), $found) . '</h2>'; 174 foreach($results as $row) { 175 176 /** @var Elastica\Result $row */ 177 $page = $row->getSource()['uri']; 178 if(!page_exists($page) || auth_quickaclcheck($page) < AUTH_READ) continue; 179 180 // get highlighted title 181 $title = str_replace( 182 array('ELASTICSEARCH_MARKER_IN', 'ELASTICSEARCH_MARKER_OUT'), 183 array('<strong class="search_hit">', '</strong>'), 184 hsc(join(' … ', (array) $row->getHighlights()['title'])) 185 ); 186 if(!$title) $title = hsc($row->getSource()['title']); 187 if(!$title) $title = hsc(p_get_first_heading($page)); 188 if(!$title) $title = hsc($page); 189 190 // get highlighted snippet 191 $snippet = str_replace( 192 array('ELASTICSEARCH_MARKER_IN', 'ELASTICSEARCH_MARKER_OUT'), 193 array('<strong class="search_hit">', '</strong>'), 194 hsc(join(' … ', (array) $row->getHighlights()[$this->getConf('snippets')])) 195 ); 196 if(!$snippet) $snippet = hsc($row->getSource()['abstract']); // always fall back to abstract 197 198 echo '<dt>'; 199 echo '<a href="'.wl($page).'" class="wikilink1" title="'.hsc($page).'">'; 200 echo $title; 201 echo '</a>'; 202 echo '</dt>'; 203 204 // meta 205 echo '<dd class="meta">'; 206 if($row->getSource()['namespace']) { 207 echo '<span class="ns">' . $this->getLang('ns') . ' ' . hsc($row->getSource()['namespace']) . '</span>'; 208 } 209 if($row->getSource()['creator']) { 210 echo ' <span class="author">' . $this->getLang('author') . ' ' . hsc($row->getSource()['creator']) . '</span>'; 211 } 212 if($row->getSource()['modified']) { 213 $lastmod = strtotime($row->getSource()['modified']); 214 echo ' <span class="">' . $lang['lastmod'] . ' ' . dformat($lastmod) . '</span>'; 215 } 216 echo '</dd>'; 217 218 // snippets 219 echo '<dd class="snippet">'; 220 echo $snippet; 221 echo '</dd>'; 222 223 } 224 echo '</dl>'; 225 226 return (bool) $found; 227 } 228 229 /** 230 * @param \Elastica\ResultSet $result 231 */ 232 protected function print_pagination($result) { 233 global $INPUT; 234 global $QUERY; 235 236 $all = $result->getTotalHits(); 237 $pages = ceil($all / $this->getConf('perpage')); 238 $cur = $INPUT->int('p', 1, true); 239 240 if($pages < 2) return; 241 242 // which pages to show 243 $toshow = array(1, 2, $cur, $pages, $pages - 1); 244 if($cur - 1 > 1) $toshow[] = $cur - 1; 245 if($cur + 1 < $pages) $toshow[] = $cur + 1; 246 $toshow = array_unique($toshow); 247 // fill up to seven, if possible 248 if(count($toshow) < 7) { 249 if($cur < 4) { 250 if($cur + 2 < $pages && count($toshow) < 7) $toshow[] = $cur + 2; 251 if($cur + 3 < $pages && count($toshow) < 7) $toshow[] = $cur + 3; 252 if($cur + 4 < $pages && count($toshow) < 7) $toshow[] = $cur + 4; 253 } else { 254 if($cur - 2 > 1 && count($toshow) < 7) $toshow[] = $cur - 2; 255 if($cur - 3 > 1 && count($toshow) < 7) $toshow[] = $cur - 3; 256 if($cur - 4 > 1 && count($toshow) < 7) $toshow[] = $cur - 4; 257 } 258 } 259 sort($toshow); 260 $showlen = count($toshow); 261 262 echo '<ul class="elastic_pagination">'; 263 if($cur > 1) { 264 echo '<li class="prev">'; 265 echo '<a href="' . wl('', http_build_query(array('q' => $QUERY, 'do' => 'elasticsearch', 'ns' => $INPUT->arr('ns'), 'p' => ($cur-1)))) . '">'; 266 echo '«'; 267 echo '</a>'; 268 echo '</li>'; 269 } 270 271 for($i = 0; $i < $showlen; $i++) { 272 if($toshow[$i] == $cur) { 273 echo '<li class="cur">' . $toshow[$i] . '</li>'; 274 } else { 275 echo '<li>'; 276 echo '<a href="' . wl('', http_build_query(array('q' => $QUERY, 'do' => 'elasticsearch', 'ns' => $INPUT->arr('ns'), 'p' => $toshow[$i]))) . '">'; 277 echo $toshow[$i]; 278 echo '</a>'; 279 echo '</li>'; 280 } 281 282 // show seperator when a jump follows 283 if(isset($toshow[$i + 1]) && $toshow[$i + 1] - $toshow[$i] > 1) { 284 echo '<li class="sep">…</li>'; 285 } 286 } 287 288 if($cur < $pages) { 289 echo '<li class="next">'; 290 echo '<a href="' . wl('', http_build_query(array('q' => $QUERY, 'do' => 'elasticsearch', 'ns' => $INPUT->arr('ns'), 'p' => ($cur+1)))) . '">'; 291 echo '»'; 292 echo '</a>'; 293 echo '</li>'; 294 } 295 296 echo '</ul>'; 297 } 298 299} 300