*/
use dokuwiki\Extension\Event;
/**
* Main search helper
*/
class action_plugin_elasticsearch_search extends DokuWiki_Action_Plugin {
/**
* Example array element for search field 'tagging':
* 'tagging' => [ // also used as search query parameter
* 'label' => 'Tag',
* 'fieldPath' => 'tagging', // dot notation in more complex mappings
* 'limit' => '50',
* ]
*
* @var Array
*/
protected static $pluginSearchConfigs;
/**
* Search will be performed on those fields only.
*
* @var string[]
*/
protected $searchFields = [
'title*',
'abstract*',
'content*',
'uri',
];
/**
* Registers a callback function for a given event
*
* @param Doku_Event_Handler $controller DokuWiki's event controller object
* @return void
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_preprocess');
$controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handle_action');
$controller->register_hook('FORM_QUICKSEARCH_OUTPUT', 'BEFORE', $this, 'quicksearch');
}
/**
* allow our custom do command
*
* @param Doku_Event $event
* @param $param
*/
public function handle_preprocess(Doku_Event $event, $param) {
if ($event->data !== 'search') return;
$event->preventDefault();
$event->stopPropagation();
}
/**
* do the actual search
*
* @param Doku_Event $event
* @param $param
*/
public function handle_action(Doku_Event $event, $param) {
if ($event->data !== 'search') return;
$event->preventDefault();
$event->stopPropagation();
global $QUERY;
global $INPUT;
global $ID;
if (empty($QUERY)) $QUERY = $INPUT->str('q');
if (empty($QUERY)) $QUERY = $ID;
// get extended search configurations from plugins
Event::createAndTrigger('PLUGIN_ELASTICSEARCH_FILTERS', self::$pluginSearchConfigs);
/** @var helper_plugin_elasticsearch_client $hlp */
$hlp = plugin_load('helper', 'elasticsearch_client');
$client = $hlp->connect();
$index = $client->getIndex($this->getConf('indexname'));
// store copy of the original query string
$q = $QUERY;
// let plugins manipulate the query
$additions = [];
Event::createAndTrigger('PLUGIN_ELASTICSEARCH_QUERY', $additions);
// if query is empty, return all results
if (empty(trim($QUERY))) $QUERY = '*';
// get fields to use in query
$fields = [];
Event::createAndTrigger('PLUGIN_ELASTICSEARCH_SEARCHFIELDS', $fields);
if ($this->getConf('searchSyntax')) {
array_push($this->searchFields, 'syntax*');
}
// finally define the elastic query
$qstring = new \Elastica\Query\SimpleQueryString($QUERY, array_merge($this->searchFields, $fields));
// restore the original query
$QUERY = $q;
// append additions provided by plugins
if (!empty($additions)) {
$QUERY .= ' ' . implode(' ', $additions);
}
// create the actual search object
$equery = new \Elastica\Query();
$subqueries = new \Elastica\Query\BoolQuery();
$subqueries->addMust($qstring);
$equery->setHighlight(
[
"pre_tags" => ['ELASTICSEARCH_MARKER_IN'],
"post_tags" => ['ELASTICSEARCH_MARKER_OUT'],
"fields" => [
$this->getConf('snippets') => new \stdClass(),
'title' => new \stdClass()]
]
);
// paginate
$equery->setSize($this->getConf('perpage'));
$equery->setFrom($this->getConf('perpage') * ($INPUT->int('p', 1, true) - 1));
// add ACL subqueries
$this->addACLSubqueries($subqueries);
// add language subquery
$this->addLanguageSubquery($subqueries, $this->getLanguageFilter());
// add date subquery
if ($INPUT->has('min')) {
$this->addDateSubquery($subqueries, $INPUT->str('min'));
}
// add namespace filter
if($INPUT->has('ns')) {
$nsSubquery = new \Elastica\Query\BoolQuery();
foreach ($INPUT->arr('ns') as $ns) {
$term = new \Elastica\Query\Term();
$term->setTerm('namespace', $ns);
$nsSubquery->addShould($term);
}
$equery->setPostFilter($nsSubquery);
}
// add aggregations for namespaces
$agg = new \Elastica\Aggregation\Terms('namespace');
$agg->setField('namespace.keyword');
$agg->setSize(25);
$equery->addAggregation($agg);
// add search configurations from other plugins
$this->addPluginConfigurations($equery, $subqueries);
$equery->setQuery($subqueries);
try {
$result = $index->search($equery);
$aggs = $result->getAggregations();
$this->print_intro();
/** @var helper_plugin_elasticsearch_form $hlpform */
$hlpform = plugin_load('helper', 'elasticsearch_form');
$hlpform->tpl($aggs);
$this->print_results($result) && $this->print_pagination($result);
} catch(Exception $e) {
msg('Something went wrong on searching please try again later or ask an admin for help.
' . hsc($e->getMessage()) . '', -1); } } /** * Optionally disable "quick search" * * @param Doku_Event $event */ public function quicksearch(Doku_Event $event) { if (!$this->getConf('disableQuicksearch')) return; /** @var \dokuwiki\Form\Form $form */ $form = $event->data; $pos = $form->findPositionByAttribute('id', 'qsearch__out'); $form->removeElement($pos); $form->removeElement($pos + 1); // div closing tag } /** * @return array */ public static function getRawPluginSearchConfigs() { return self::$pluginSearchConfigs; } /** * Add search configurations supplied by other plugins * * @param \Elastica\Query $equery * @param \Elastica\Query\BoolQuery */ protected function addPluginConfigurations($equery, $subqueries) { global $INPUT; if (!empty(self::$pluginSearchConfigs)) { foreach (self::$pluginSearchConfigs as $param => $config) { // handle search parameter if ($INPUT->has($param)) { $pluginSubquery = new \Elastica\Query\BoolQuery(); foreach($INPUT->arr($param) as $item) { $eterm = new \Elastica\Query\Term(); $eterm->setTerm($param, $item); $pluginSubquery->addShould($eterm); } $subqueries->addMust($pluginSubquery); } // build aggregation for use as filter in advanced search $agg = new \Elastica\Aggregation\Terms($param); $agg->setField($config['fieldPath']); if (isset($config['limit'])) { $agg->setSize($config['limit']); } $equery->addAggregation($agg); } } } /** * Adds date subquery * * @param Elastica\Query\BoolQuery $subqueries * @param string $min Modified at the latest one {year|month|week} ago */ protected function addDateSubquery($subqueries, $min) { if (!in_array($min, ['year', 'month', 'week'])) return; $dateSubquery = new \Elastica\Query\Range( 'modified', ['gte' => date('Y-m-d', strtotime('1 ' . $min . ' ago'))] ); $subqueries->addMust($dateSubquery); } /** * Adds language subquery * * @param Elastica\Query\BoolQuery $subqueries * @param array $langFilter */ protected function addLanguageSubquery($subqueries, $langFilter) { if (empty($langFilter)) return; $langSubquery = new \Elastica\Query\MatchQuery(); $langSubquery->setField('language', implode(',', $langFilter)); $subqueries->addMust($langSubquery); } /** * Languages to be used in the current search, determined by: * 1. $INPUT variables, or 2. translation plugin * * @return array */ protected function getLanguageFilter() { global $ID; global $INPUT; $ns = getNS($ID); $langFilter = $INPUT->arr('lang'); /** @var helper_plugin_translation $transplugin */ $transplugin = plugin_load('helper', 'translation'); // optional translation detection: use current top namespace if it matches translation config if (empty($langFilter) && $transplugin && $this->getConf('detectTranslation') && $ns) { $topNs = strtok($ns, ':'); if (in_array($topNs, $transplugin->translations)) { $langFilter = [$topNs]; $INPUT->set('lang', $langFilter); } } else if (empty($langFilter) && $transplugin) { // select all available translations $INPUT->set('lang', $transplugin->translations); } return $langFilter; } /** * Inserts subqueries based on current user's ACLs, none for superusers * * @param \Elastica\Query\BoolQuery $subqueries */ protected function addACLSubqueries($subqueries) { global $USERINFO; global $INFO; $groups = array_merge(['ALL'], $USERINFO['grps'] ?: []); // no ACL filters for superusers if ($INFO['isadmin']) return; // include if group OR user have read permissions, allows for ACLs such as "block @group except user" $includeSubquery = new \Elastica\Query\BoolQuery(); foreach($groups as $group) { $term = new \Elastica\Query\Term(); $term->setTerm('groups_include', $group); $includeSubquery->addShould($term); } if (isset($_SERVER['REMOTE_USER'])) { $userIncludeSubquery = new \Elastica\Query\BoolQuery(); $term = new \Elastica\Query\Term(); $term->setTerm('users_include', $_SERVER['REMOTE_USER']); $userIncludeSubquery->addMust($term); $includeSubquery->addShould($userIncludeSubquery); } $subqueries->addMust($includeSubquery); // groups exclusion SHOULD be respected, not MUST, since that would not allow for exceptions $groupExcludeSubquery = new \Elastica\Query\BoolQuery(); foreach($groups as $group) { $term = new \Elastica\Query\Term(); $term->setTerm('groups_exclude', $group); $groupExcludeSubquery->addShould($term); } $excludeSubquery = new \Elastica\Query\BoolQuery(); $excludeSubquery->addMustNot($groupExcludeSubquery); $subqueries->addShould($excludeSubquery); // user specific excludes must always be respected if (isset($_SERVER['REMOTE_USER'])) { $term = new \Elastica\Query\Term(); $term->setTerm('users_exclude', $_SERVER['REMOTE_USER']); $subqueries->addMustNot($term); } } /** * Prints the introduction text */ protected function print_intro() { global $QUERY; global $ID; global $lang; // just reuse the standard search page intro: $intro = p_locale_xhtml('searchpage'); // allow use of placeholder in search intro $pagecreateinfo = ''; if (auth_quickaclcheck($ID) >= AUTH_CREATE) { $pagecreateinfo = sprintf($lang['searchcreatepage'], $QUERY); } $intro = str_replace( ['@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'], [hsc(rawurlencode($QUERY)), hsc($QUERY), $pagecreateinfo], $intro ); echo $intro; flush(); } /** * Output the search results * * @param \Elastica\ResultSet $results * @return bool true when results where shown */ protected function print_results($results) { global $lang; // output results $found = $results->getTotalHits(); if(!$found) { echo '