xref: /plugin/elasticsearch/action/search.php (revision 9a2288a153ca3399fabc000d567179a2e410eefe)
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