Skip to content
Snippets Groups Projects
Unverified Commit bb8ef867 authored by Michael Große's avatar Michael Große
Browse files

feat(search): add search assistance for simple queries

This add some search assistance to simple, single-word search queries
which may be restricted to a single namespace.

Further improvements:
* better styling
* trigger events for other plugins
* set namespaces directly from fulltext search results
* some more config options
parent 427ed988
No related branches found
No related tags found
No related merge requests found
......@@ -62,17 +62,183 @@ class Search extends Ui
{
global $lang;
$searchForm = (new Form())->attrs(['method' => 'get']);
$searchForm->setHiddenField('do', 'search');
$Indexer = idx_get_indexer();
$parsedQuery = ft_queryParser($Indexer, $query);
$searchForm->addFieldsetOpen();
$searchForm->addTextInput('id', '')->val($query);
$searchForm = (new Form())->attrs(['method' => 'get'])->addClass('search-results-form');
$searchForm->setHiddenField('do', 'search');
$searchForm->addFieldsetOpen()->addClass('search-results-form__fieldset');
$searchForm->addTextInput('id')->val($query);
$searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
if ($this->isSearchAssistanceAvailable($parsedQuery)) {
$this->addSearchAssistanceElements($searchForm, $parsedQuery);
} else {
$searchForm->addClass('search-results-form--no-assistance');
$searchForm->addTagOpen('span')->addClass('search-results-form__no-assistance-message');
$searchForm->addHTML('FIXME Your query is too complex. Search assistance is unavailable. See <a href="https://doku.wiki/search">doku.wiki/search</a> for more help.');
$searchForm->addTagClose('span');
}
$searchForm->addFieldsetClose();
return $searchForm->toHTML();
}
/**
* Decide if the given query is simple enough to provide search assistance
*
* @param array $parsedQuery
*
* @return bool
*/
protected function isSearchAssistanceAvailable(array $parsedQuery)
{
if (count($parsedQuery['words']) > 1) {
return false;
}
if (!empty($parsedQuery['not'])) {
return false;
}
if (!empty($parsedQuery['phrases'])) {
return false;
}
if (!empty($parsedQuery['notns'])) {
return false;
}
if (count($parsedQuery['ns']) > 1) {
return false;
}
return true;
}
/**
* Add the elements to be used for search assistance
*
* @param Form $searchForm
* @param array $parsedQuery
*/
protected function addSearchAssistanceElements(Form $searchForm, array $parsedQuery)
{
$matchType = '';
$searchTerm = null;
if (count($parsedQuery['words']) === 1) {
$searchTerm = $parsedQuery['words'][0];
$firstChar = $searchTerm[0];
$lastChar = substr($searchTerm, -1);
$matchType = 'exact';
if ($firstChar === '*') {
$matchType = 'starts';
}
if ($lastChar === '*') {
$matchType = 'ends';
}
if ($firstChar === '*' && $lastChar === '*') {
$matchType = 'contains';
}
$searchTerm = trim($searchTerm, '*');
}
$searchForm->addTextInput(
'searchTerm',
'',
$searchForm->findPositionByAttribute('type', 'submit')
)
->val($searchTerm)
->attr('style', 'display: none;');
$searchForm->addButton('toggleAssistant', 'toggle search assistant')
->attr('type', 'button')
->id('search-results-form__show-assistance-button')
->addClass('search-results-form__show-assistance-button');
$searchForm->addTagOpen('div')
->addClass('js-advancedSearchOptions')
->attr('style', 'display: none;');
$searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
$searchForm->addRadioButton('matchType', 'exact Match FM')->val('exact')->attr('checked',
$matchType === 'exact' ?: null);
$searchForm->addRadioButton('matchType', 'starts with FM')->val('starts')->attr('checked',
$matchType === 'starts' ?: null);
$searchForm->addRadioButton('matchType', 'ends with FM')->val('ends')->attr('checked',
$matchType === 'ends' ?: null);
$searchForm->addRadioButton('matchType', 'contains FM')->val('contains')->attr('checked',
$matchType === 'contains' ?: null);
$searchForm->addTagClose('div');
$this->addNamespaceSelector($searchForm, $parsedQuery);
$searchForm->addTagClose('div');
}
/**
* Add the elements for the namespace selector
*
* @param Form $searchForm
* @param array $parsedQuery
*/
protected function addNamespaceSelector(Form $searchForm, array $parsedQuery)
{
$baseNS = empty($parsedQuery['ns']) ? '' : $parsedQuery['ns'][0];
$namespaces = [];
$searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
if ($baseNS) {
$searchForm->addRadioButton('namespace', '(no namespace FIXME)')->val('');
$parts = [$baseNS => count($this->fullTextResults)];
$upperNameSpace = $baseNS;
while ($upperNameSpace = getNS($upperNameSpace)) {
$parts[$upperNameSpace] = 0;
}
$namespaces = array_reverse($parts);
};
$namespaces = array_merge($namespaces, $this->getAdditionalNamespacesFromResults($baseNS));
foreach ($namespaces as $extraNS => $count) {
$label = $extraNS . ($count ? " ($count)" : '');
$namespaceCB = $searchForm->addRadioButton('namespace', $label)->val($extraNS);
if ($extraNS === $baseNS) {
$namespaceCB->attr('checked', true);
}
}
$searchForm->addTagClose('div');
}
/**
* Parse the full text results for their top namespaces below the given base namespace
*
* @param string $baseNS the namespace within which was searched, empty string for root namespace
*
* @return array an associative array with namespace => #number of found pages, sorted descending
*/
protected function getAdditionalNamespacesFromResults($baseNS)
{
$namespaces = [];
$baseNSLength = strlen($baseNS);
foreach ($this->fullTextResults as $page => $numberOfHits) {
$namespace = getNS($page);
if (!$namespace) {
continue;
}
if ($namespace === $baseNS) {
continue;
}
$firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
$subtopNS = substr($namespace, 0, $firstColon);
if (empty($namespaces[$subtopNS])) {
$namespaces[$subtopNS] = 0;
}
$namespaces[$subtopNS] += 1;
}
arsort($namespaces);
return $namespaces;
}
/**
* Build the intro text for the search page
*
......
......@@ -47,6 +47,7 @@ function js_out(){
DOKU_INC.'lib/scripts/cookie.js',
DOKU_INC.'lib/scripts/script.js',
DOKU_INC.'lib/scripts/qsearch.js',
DOKU_INC.'lib/scripts/search.js',
DOKU_INC.'lib/scripts/tree.js',
DOKU_INC.'lib/scripts/index.js',
DOKU_INC.'lib/scripts/textselection.js',
......
jQuery(function () {
'use strict';
const $searchForm = jQuery('.search-results-form');
if (!$searchForm.length) {
return;
}
if (!$searchForm.find('#search-results-form__show-assistance-button').length){
return;
}
const $toggleAssistanceButton = $searchForm.find('#search-results-form__show-assistance-button');
const $queryInput = $searchForm.find('[name="id"]');
const $termInput = $searchForm.find('[name="searchTerm"]');
$toggleAssistanceButton.on('click', function () {
jQuery('.js-advancedSearchOptions').toggle();
$queryInput.toggle();
$termInput.toggle();
});
const $matchTypeSwitcher = $searchForm.find('[name="matchType"]');
const $namespaceSwitcher = $searchForm.find('[name="namespace"]');
const $refiningElements = $termInput.add($matchTypeSwitcher).add($namespaceSwitcher);
$refiningElements.on('input change', function () {
$queryInput.val(
rebuildQuery(
$termInput.val(),
$matchTypeSwitcher.filter(':checked').val(),
$namespaceSwitcher.filter(':checked').val()
)
);
});
/**
* Rebuild the search query from the parts
*
* @param {string} searchTerm the word which is to be searched
* @param {enum} matchType the type of matching that is to be done
* @param {string} namespace list of namespaces to which to limit the search
*
* @return {string} the query string for the actual search
*/
function rebuildQuery(searchTerm, matchType, namespace) {
let query = '';
switch (matchType) {
case 'contains':
query = '*' + searchTerm + '*';
break;
case 'starts':
query = '*' + searchTerm;
break;
case 'ends':
query = searchTerm + '*';
break;
default:
query = searchTerm;
}
if (namespace && namespace.length) {
query += ' @' + namespace;
}
return query;
}
});
......@@ -12,6 +12,22 @@
margin-bottom: 1.4em;
}
.search-results-form .search-results-form__fieldset {
width: 80vw;
}
.search-results-form__show-assistance-button {
float: right;
}
.search-results-form__no-assistance-message {
color: grey;
float: right;
font-size: 80%;
margin-top: -0.3em;
}
/*____________ matching pagenames ____________*/
.dokuwiki div.search_quickresult {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment