Merge pull request #2593 from dalf/update-autocomplete

Update autocomplete
This commit is contained in:
Alexandre Flament 2021-03-04 10:51:09 +01:00 committed by GitHub
commit aac37f288f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 497 additions and 222 deletions

View file

@ -32,7 +32,7 @@ def ask(query):
results = [] results = []
query_parts = list(filter(None, query.query.split())) query_parts = list(filter(None, query.query.split()))
if query_parts[0] not in answerers_by_keywords: if not query_parts or query_parts[0] not in answerers_by_keywords:
return results return results
for answerer in answerers_by_keywords[query_parts[0]]: for answerer in answerers_by_keywords[query_parts[0]]:

View file

@ -20,97 +20,20 @@ from lxml import etree
from json import loads from json import loads
from urllib.parse import urlencode from urllib.parse import urlencode
from requests import RequestException
from searx import settings from searx import settings
from searx.languages import language_codes
from searx.engines import (
categories, engines, engine_shortcuts
)
from searx.poolrequests import get as http_get from searx.poolrequests import get as http_get
from searx.exceptions import SearxEngineResponseException
def get(*args, **kwargs): def get(*args, **kwargs):
if 'timeout' not in kwargs: if 'timeout' not in kwargs:
kwargs['timeout'] = settings['outgoing']['request_timeout'] kwargs['timeout'] = settings['outgoing']['request_timeout']
kwargs['raise_for_httperror'] = True
return http_get(*args, **kwargs) return http_get(*args, **kwargs)
def searx_bang(full_query):
'''check if the searchQuery contain a bang, and create fitting autocompleter results'''
# check if there is a query which can be parsed
if len(full_query.getQuery()) == 0:
return []
results = []
# check if current query stats with !bang
first_char = full_query.getQuery()[0]
if first_char == '!' or first_char == '?':
if len(full_query.getQuery()) == 1:
# show some example queries
# TODO, check if engine is not avaliable
results.append(first_char + "images")
results.append(first_char + "wikipedia")
results.append(first_char + "osm")
else:
engine_query = full_query.getQuery()[1:]
# check if query starts with categorie name
for categorie in categories:
if categorie.startswith(engine_query):
results.append(first_char + '{categorie}'.format(categorie=categorie))
# check if query starts with engine name
for engine in engines:
if engine.startswith(engine_query.replace('_', ' ')):
results.append(first_char + '{engine}'.format(engine=engine.replace(' ', '_')))
# check if query starts with engine shortcut
for engine_shortcut in engine_shortcuts:
if engine_shortcut.startswith(engine_query):
results.append(first_char + '{engine_shortcut}'.format(engine_shortcut=engine_shortcut))
# check if current query stats with :bang
elif first_char == ':':
if len(full_query.getQuery()) == 1:
# show some example queries
results.append(":en")
results.append(":en_us")
results.append(":english")
results.append(":united_kingdom")
else:
engine_query = full_query.getQuery()[1:]
for lc in language_codes:
lang_id, lang_name, country, english_name = map(str.lower, lc)
# check if query starts with language-id
if lang_id.startswith(engine_query):
if len(engine_query) <= 2:
results.append(':{lang_id}'.format(lang_id=lang_id.split('-')[0]))
else:
results.append(':{lang_id}'.format(lang_id=lang_id))
# check if query starts with language name
if lang_name.startswith(engine_query) or english_name.startswith(engine_query):
results.append(':{lang_name}'.format(lang_name=lang_name))
# check if query starts with country
if country.startswith(engine_query.replace('_', ' ')):
results.append(':{country}'.format(country=country.replace(' ', '_')))
# remove duplicates
result_set = set(results)
# remove results which are already contained in the query
for query_part in full_query.query_parts:
if query_part in result_set:
result_set.remove(query_part)
# convert result_set back to list
return list(result_set)
def dbpedia(query, lang): def dbpedia(query, lang):
# dbpedia autocompleter, no HTTPS # dbpedia autocompleter, no HTTPS
autocomplete_url = 'https://lookup.dbpedia.org/api/search.asmx/KeywordSearch?' autocomplete_url = 'https://lookup.dbpedia.org/api/search.asmx/KeywordSearch?'
@ -204,3 +127,14 @@ backends = {'dbpedia': dbpedia,
'qwant': qwant, 'qwant': qwant,
'wikipedia': wikipedia 'wikipedia': wikipedia
} }
def search_autocomplete(backend_name, query, lang):
backend = backends.get(backend_name)
if backend is None:
return []
try:
return backend(query, lang)
except (RequestException, SearxEngineResponseException):
return []

View file

@ -1,162 +1,330 @@
#!/usr/bin/env python # SPDX-License-Identifier: AGPL-3.0-or-later
'''
searx is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
searx is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with searx. If not, see < http://www.gnu.org/licenses/ >.
(C) 2014 by Thomas Pointhuber, <thomas.pointhuber@gmx.at>
'''
from abc import abstractmethod, ABC
import re import re
from searx.languages import language_codes from searx.languages import language_codes
from searx.engines import categories, engines, engine_shortcuts from searx.engines import categories, engines, engine_shortcuts
from searx.external_bang import get_bang_definition_and_autocomplete
from searx.search import EngineRef from searx.search import EngineRef
from searx.webutils import VALID_LANGUAGE_CODE from searx.webutils import VALID_LANGUAGE_CODE
class QueryPartParser(ABC):
__slots__ = "raw_text_query", "enable_autocomplete"
@staticmethod
@abstractmethod
def check(raw_value):
"""Check if raw_value can be parsed"""
def __init__(self, raw_text_query, enable_autocomplete):
self.raw_text_query = raw_text_query
self.enable_autocomplete = enable_autocomplete
@abstractmethod
def __call__(self, raw_value):
"""Try to parse raw_value: set the self.raw_text_query properties
return True if raw_value has been parsed
self.raw_text_query.autocomplete_list is also modified
if self.enable_autocomplete is True
"""
def _add_autocomplete(self, value):
if value not in self.raw_text_query.autocomplete_list:
self.raw_text_query.autocomplete_list.append(value)
class TimeoutParser(QueryPartParser):
@staticmethod
def check(raw_value):
return raw_value[0] == '<'
def __call__(self, raw_value):
value = raw_value[1:]
found = self._parse(value) if len(value) > 0 else False
if self.enable_autocomplete and not value:
self._autocomplete()
return found
def _parse(self, value):
if not value.isdigit():
return False
raw_timeout_limit = int(value)
if raw_timeout_limit < 100:
# below 100, the unit is the second ( <3 = 3 seconds timeout )
self.raw_text_query.timeout_limit = float(raw_timeout_limit)
else:
# 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )
self.raw_text_query.timeout_limit = raw_timeout_limit / 1000.0
return True
def _autocomplete(self):
for suggestion in ['<3', '<850']:
self._add_autocomplete(suggestion)
class LanguageParser(QueryPartParser):
@staticmethod
def check(raw_value):
return raw_value[0] == ':'
def __call__(self, raw_value):
value = raw_value[1:].lower().replace('_', '-')
found = self._parse(value) if len(value) > 0 else False
if self.enable_autocomplete and not found:
self._autocomplete(value)
return found
def _parse(self, value):
found = False
# check if any language-code is equal with
# declared language-codes
for lc in language_codes:
lang_id, lang_name, country, english_name = map(str.lower, lc)
# if correct language-code is found
# set it as new search-language
if (value == lang_id
or value == lang_name
or value == english_name
or value.replace('-', ' ') == country)\
and value not in self.raw_text_query.languages:
found = True
lang_parts = lang_id.split('-')
if len(lang_parts) == 2:
self.raw_text_query.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())
else:
self.raw_text_query.languages.append(lang_id)
# to ensure best match (first match is not necessarily the best one)
if value == lang_id:
break
# user may set a valid, yet not selectable language
if VALID_LANGUAGE_CODE.match(value):
lang_parts = value.split('-')
if len(lang_parts) > 1:
value = lang_parts[0].lower() + '-' + lang_parts[1].upper()
if value not in self.raw_text_query.languages:
self.raw_text_query.languages.append(value)
found = True
return found
def _autocomplete(self, value):
if not value:
# show some example queries
for lang in [":en", ":en_us", ":english", ":united_kingdom"]:
self.raw_text_query.autocomplete_list.append(lang)
return
for lc in language_codes:
lang_id, lang_name, country, english_name = map(str.lower, lc)
# check if query starts with language-id
if lang_id.startswith(value):
if len(value) <= 2:
self._add_autocomplete(':' + lang_id.split('-')[0])
else:
self._add_autocomplete(':' + lang_id)
# check if query starts with language name
if lang_name.startswith(value) or english_name.startswith(value):
self._add_autocomplete(':' + lang_name)
# check if query starts with country
# here "new_zealand" is "new-zealand" (see __call__)
if country.startswith(value.replace('-', ' ')):
self._add_autocomplete(':' + country.replace(' ', '_'))
class ExternalBangParser(QueryPartParser):
@staticmethod
def check(raw_value):
return raw_value.startswith('!!')
def __call__(self, raw_value):
value = raw_value[2:]
found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])
if self.enable_autocomplete:
self._autocomplete(bang_ac_list)
return found
def _parse(self, value):
found = False
bang_definition, bang_ac_list = get_bang_definition_and_autocomplete(value)
if bang_definition is not None:
self.raw_text_query.external_bang = value
found = True
return found, bang_ac_list
def _autocomplete(self, bang_ac_list):
if not bang_ac_list:
bang_ac_list = ['g', 'ddg', 'bing']
for external_bang in bang_ac_list:
self._add_autocomplete('!!' + external_bang)
class BangParser(QueryPartParser):
@staticmethod
def check(raw_value):
return raw_value[0] == '!' or raw_value[0] == '?'
def __call__(self, raw_value):
value = raw_value[1:].replace('-', ' ').replace('_', ' ')
found = self._parse(value) if len(value) > 0 else False
if found and raw_value[0] == '!':
self.raw_text_query.specific = True
if self.enable_autocomplete:
self._autocomplete(raw_value[0], value)
return found
def _parse(self, value):
# check if prefix is equal with engine shortcut
if value in engine_shortcuts:
value = engine_shortcuts[value]
# check if prefix is equal with engine name
if value in engines:
self.raw_text_query.enginerefs.append(EngineRef(value, 'none'))
return True
# check if prefix is equal with categorie name
if value in categories:
# using all engines for that search, which
# are declared under that categorie name
self.raw_text_query.enginerefs.extend(EngineRef(engine.name, value)
for engine in categories[value]
if (engine.name, value) not in self.raw_text_query.disabled_engines)
return True
return False
def _autocomplete(self, first_char, value):
if not value:
# show some example queries
for suggestion in ['images', 'wikipedia', 'osm']:
if suggestion not in self.raw_text_query.disabled_engines or suggestion in categories:
self._add_autocomplete(first_char + suggestion)
return
# check if query starts with categorie name
for category in categories:
if category.startswith(value):
self._add_autocomplete(first_char + category)
# check if query starts with engine name
for engine in engines:
if engine.startswith(value):
self._add_autocomplete(first_char + engine.replace(' ', '_'))
# check if query starts with engine shortcut
for engine_shortcut in engine_shortcuts:
if engine_shortcut.startswith(value):
self._add_autocomplete(first_char + engine_shortcut)
class RawTextQuery: class RawTextQuery:
"""parse raw text query (the value from the html input)""" """parse raw text query (the value from the html input)"""
PARSER_CLASSES = [
TimeoutParser, # this force the timeout
LanguageParser, # this force a language
ExternalBangParser, # external bang (must be before BangParser)
BangParser # this force a engine or category
]
def __init__(self, query, disabled_engines): def __init__(self, query, disabled_engines):
assert isinstance(query, str) assert isinstance(query, str)
# input parameters
self.query = query self.query = query
self.disabled_engines = [] self.disabled_engines = disabled_engines if disabled_engines else []
# parsed values
if disabled_engines:
self.disabled_engines = disabled_engines
self.query_parts = []
self.user_query_parts = []
self.enginerefs = [] self.enginerefs = []
self.languages = [] self.languages = []
self.timeout_limit = None self.timeout_limit = None
self.external_bang = None self.external_bang = None
self.specific = False self.specific = False
self.autocomplete_list = []
# internal properties
self.query_parts = [] # use self.getFullQuery()
self.user_query_parts = [] # use self.getQuery()
self.autocomplete_location = None
self._parse_query() self._parse_query()
# parse query, if tags are set, which
# change the search engine or search-language
def _parse_query(self): def _parse_query(self):
self.query_parts = [] """
parse self.query, if tags are set, which
change the search engine or search-language
"""
# split query, including whitespaces # split query, including whitespaces
raw_query_parts = re.split(r'(\s+)', self.query) raw_query_parts = re.split(r'(\s+)', self.query)
for query_part in raw_query_parts: last_index_location = None
searx_query_part = False autocomplete_index = len(raw_query_parts) - 1
for i, query_part in enumerate(raw_query_parts):
# part does only contain spaces, skip # part does only contain spaces, skip
if query_part.isspace()\ if query_part.isspace()\
or query_part == '': or query_part == '':
continue continue
# this force the timeout # parse special commands
if query_part[0] == '<': special_part = False
try: for parser_class in RawTextQuery.PARSER_CLASSES:
raw_timeout_limit = int(query_part[1:]) if parser_class.check(query_part):
if raw_timeout_limit < 100: special_part = parser_class(self, i == autocomplete_index)(query_part)
# below 100, the unit is the second ( <3 = 3 seconds timeout ) break
self.timeout_limit = float(raw_timeout_limit)
else:
# 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )
self.timeout_limit = raw_timeout_limit / 1000.0
searx_query_part = True
except ValueError:
# error not reported to the user
pass
# this force a language
if query_part[0] == ':' and len(query_part) > 1:
lang = query_part[1:].lower().replace('_', '-')
# check if any language-code is equal with
# declared language-codes
for lc in language_codes:
lang_id, lang_name, country, english_name = map(str.lower, lc)
# if correct language-code is found
# set it as new search-language
if (lang == lang_id
or lang == lang_name
or lang == english_name
or lang.replace('-', ' ') == country)\
and lang not in self.languages:
searx_query_part = True
lang_parts = lang_id.split('-')
if len(lang_parts) == 2:
self.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())
else:
self.languages.append(lang_id)
# to ensure best match (first match is not necessarily the best one)
if lang == lang_id:
break
# user may set a valid, yet not selectable language
if VALID_LANGUAGE_CODE.match(lang):
lang_parts = lang.split('-')
if len(lang_parts) > 1:
lang = lang_parts[0].lower() + '-' + lang_parts[1].upper()
if lang not in self.languages:
self.languages.append(lang)
searx_query_part = True
# external bang
if query_part[0:2] == "!!":
self.external_bang = query_part[2:]
searx_query_part = True
continue
# this force a engine or category
if query_part[0] == '!' or query_part[0] == '?':
prefix = query_part[1:].replace('-', ' ').replace('_', ' ')
# check if prefix is equal with engine shortcut
if prefix in engine_shortcuts:
searx_query_part = True
engine_name = engine_shortcuts[prefix]
if engine_name in engines:
self.enginerefs.append(EngineRef(engine_name, 'none'))
# check if prefix is equal with engine name
elif prefix in engines:
searx_query_part = True
self.enginerefs.append(EngineRef(prefix, 'none'))
# check if prefix is equal with categorie name
elif prefix in categories:
# using all engines for that search, which
# are declared under that categorie name
searx_query_part = True
self.enginerefs.extend(EngineRef(engine.name, prefix)
for engine in categories[prefix]
if (engine.name, prefix) not in self.disabled_engines)
if query_part[0] == '!':
self.specific = True
# append query part to query_part list # append query part to query_part list
if searx_query_part: qlist = self.query_parts if special_part else self.user_query_parts
self.query_parts.append(query_part) qlist.append(query_part)
else: last_index_location = (qlist, len(qlist) - 1)
self.user_query_parts.append(query_part)
self.autocomplete_location = last_index_location
def get_autocomplete_full_query(self, text):
qlist, position = self.autocomplete_location
qlist[position] = text
return self.getFullQuery()
def changeQuery(self, query): def changeQuery(self, query):
self.user_query_parts = query.strip().split() self.user_query_parts = query.strip().split()
self.query = self.getFullQuery()
self.autocomplete_location = (self.user_query_parts, len(self.user_query_parts) - 1)
self.autocomplete_list = []
return self return self
def getQuery(self): def getQuery(self):
return ' '.join(self.user_query_parts) return ' '.join(self.user_query_parts)
def getFullQuery(self): def getFullQuery(self):
# get full querry including whitespaces """
return '{0} {1}'.format(''.join(self.query_parts), self.getQuery()).strip() get full querry including whitespaces
"""
return '{0} {1}'.format(' '.join(self.query_parts), self.getQuery()).strip()
def __str__(self):
return self.getFullQuery()
def __repr__(self):
return f"<{self.__class__.__name__} " \
+ f"query={self.query!r} " \
+ f"disabled_engines={self.disabled_engines!r}\n " \
+ f"languages={self.languages!r} " \
+ f"timeout_limit={self.timeout_limit!r} "\
+ f"external_bang={self.external_bang!r} " \
+ f"specific={self.specific!r} " \
+ f"enginerefs={self.enginerefs!r}\n " \
+ f"autocomplete_list={self.autocomplete_list!r}\n " \
+ f"query_parts={self.query_parts!r}\n " \
+ f"user_query_parts={self.user_query_parts!r} >"

View file

@ -74,12 +74,13 @@ from searx.languages import language_codes as languages
from searx.search import SearchWithPlugins, initialize as search_initialize from searx.search import SearchWithPlugins, initialize as search_initialize
from searx.search.checker import get_result as checker_get_result from searx.search.checker import get_result as checker_get_result
from searx.query import RawTextQuery from searx.query import RawTextQuery
from searx.autocomplete import searx_bang, backends as autocomplete_backends from searx.autocomplete import search_autocomplete, backends as autocomplete_backends
from searx.plugins import plugins from searx.plugins import plugins
from searx.plugins.oa_doi_rewrite import get_doi_resolver from searx.plugins.oa_doi_rewrite import get_doi_resolver
from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES from searx.preferences import Preferences, ValidationException, LANGUAGE_CODES
from searx.answerers import answerers from searx.answerers import answerers
from searx.poolrequests import get_global_proxies from searx.poolrequests import get_global_proxies
from searx.answerers import ask
from searx.metrology.error_recorder import errors_per_engines from searx.metrology.error_recorder import errors_per_engines
# serve pages with HTTP/1.1 # serve pages with HTTP/1.1
@ -763,27 +764,18 @@ def about():
def autocompleter(): def autocompleter():
"""Return autocompleter results""" """Return autocompleter results"""
# run autocompleter
results = []
# set blocked engines # set blocked engines
disabled_engines = request.preferences.engines.get_disabled() disabled_engines = request.preferences.engines.get_disabled()
# parse query # parse query
raw_text_query = RawTextQuery(request.form.get('q', ''), disabled_engines) raw_text_query = RawTextQuery(request.form.get('q', ''), disabled_engines)
# check if search query is set
if not raw_text_query.getQuery():
return '', 400
# run autocompleter
completer = autocomplete_backends.get(request.preferences.get_value('autocomplete'))
# parse searx specific autocompleter results like !bang
raw_results = searx_bang(raw_text_query)
# normal autocompletion results only appear if no inner results returned # normal autocompletion results only appear if no inner results returned
# and there is a query part besides the engine and language bangs # and there is a query part
if len(raw_results) == 0 and completer and (len(raw_text_query.query_parts) > 1 or if len(raw_text_query.autocomplete_list) == 0 and len(raw_text_query.getQuery()) > 0:
(len(raw_text_query.languages) == 0 and
not raw_text_query.specific)):
# get language from cookie # get language from cookie
language = request.preferences.get_value('language') language = request.preferences.get_value('language')
if not language or language == 'all': if not language or language == 'all':
@ -791,15 +783,18 @@ def autocompleter():
else: else:
language = language.split('-')[0] language = language.split('-')[0]
# run autocompletion # run autocompletion
raw_results.extend(completer(raw_text_query.getQuery(), language)) raw_results = search_autocomplete(request.preferences.get_value('autocomplete'),
raw_text_query.getQuery(), language)
for result in raw_results:
results.append(raw_text_query.changeQuery(result).getFullQuery())
# parse results (write :language and !engine back to result string) if len(raw_text_query.autocomplete_list) > 0:
results = [] for autocomplete_text in raw_text_query.autocomplete_list:
for result in raw_results: results.append(raw_text_query.get_autocomplete_full_query(autocomplete_text))
raw_text_query.changeQuery(result)
# add parsed result for answers in ask(raw_text_query):
results.append(raw_text_query.getFullQuery()) for answer in answers:
results.append(str(answer['answer']))
# return autocompleter results # return autocompleter results
if request.headers.get('X-Requested-With') == 'XMLHttpRequest': if request.headers.get('X-Requested-With') == 'XMLHttpRequest':

View file

@ -1,7 +1,20 @@
from searx.search import initialize
from searx.query import RawTextQuery from searx.query import RawTextQuery
from searx.testing import SearxTestCase from searx.testing import SearxTestCase
TEST_ENGINES = [
{
'name': 'dummy engine',
'engine': 'dummy',
'categories': 'general',
'shortcut': 'du',
'timeout': 3.0,
'tokens': [],
},
]
class TestQuery(SearxTestCase): class TestQuery(SearxTestCase):
def test_simple_query(self): def test_simple_query(self):
@ -14,6 +27,37 @@ class TestQuery(SearxTestCase):
self.assertEqual(len(query.languages), 0) self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific) self.assertFalse(query.specific)
def test_multiple_spaces_query(self):
query_text = '\tthe query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), 'the query')
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(len(query.user_query_parts), 2)
self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific)
def test_str_method(self):
query_text = '<7 the query'
query = RawTextQuery(query_text, [])
self.assertEqual(str(query), '<7 the query')
def test_repr_method(self):
query_text = '<8 the query'
query = RawTextQuery(query_text, [])
r = repr(query)
self.assertTrue(r.startswith(f"<RawTextQuery query='{query_text}' "))
def test_change_query(self):
query_text = '<8 the query'
query = RawTextQuery(query_text, [])
another_query = query.changeQuery('another text')
self.assertEqual(query, another_query)
self.assertEqual(query.getFullQuery(), '<8 another text')
class TestLanguageParser(SearxTestCase):
def test_language_code(self): def test_language_code(self):
language = 'es-ES' language = 'es-ES'
query_text = 'the query' query_text = 'the query'
@ -68,6 +112,30 @@ class TestQuery(SearxTestCase):
self.assertEqual(len(query.languages), 0) self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific) self.assertFalse(query.specific)
def test_autocomplete_empty(self):
query_text = 'the query :'
query = RawTextQuery(query_text, [])
self.assertEqual(query.autocomplete_list, [":en", ":en_us", ":english", ":united_kingdom"])
def test_autocomplete(self):
query = RawTextQuery(':englis', [])
self.assertEqual(query.autocomplete_list, [":english"])
query = RawTextQuery(':deutschla', [])
self.assertEqual(query.autocomplete_list, [":deutschland"])
query = RawTextQuery(':new_zea', [])
self.assertEqual(query.autocomplete_list, [":new_zealand"])
query = RawTextQuery(':hu-H', [])
self.assertEqual(query.autocomplete_list, [":hu-hu"])
query = RawTextQuery(':v', [])
self.assertEqual(query.autocomplete_list, [":vi", ":tiếng việt"])
class TestTimeoutParser(SearxTestCase):
def test_timeout_below100(self): def test_timeout_below100(self):
query_text = '<3 the query' query_text = '<3 the query'
query = RawTextQuery(query_text, []) query = RawTextQuery(query_text, [])
@ -105,3 +173,113 @@ class TestQuery(SearxTestCase):
self.assertEqual(query.getQuery(), query_text) self.assertEqual(query.getQuery(), query_text)
self.assertEqual(query.timeout_limit, None) self.assertEqual(query.timeout_limit, None)
self.assertFalse(query.specific) self.assertFalse(query.specific)
def test_timeout_autocomplete(self):
# invalid number: it is not bang but it is part of the query
query_text = 'the query <'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(query.getQuery(), query_text)
self.assertEqual(query.timeout_limit, None)
self.assertFalse(query.specific)
self.assertEqual(query.autocomplete_list, ['<3', '<850'])
class TestExternalBangParser(SearxTestCase):
def test_external_bang(self):
query_text = '!!ddg the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 1)
self.assertFalse(query.specific)
def test_external_bang_not_found(self):
query_text = '!!notfoundbang the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(query.external_bang, None)
self.assertFalse(query.specific)
def test_external_bang_autocomplete(self):
query_text = 'the query !!dd'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), '!!dd the query')
self.assertEqual(len(query.query_parts), 1)
self.assertFalse(query.specific)
self.assertGreater(len(query.autocomplete_list), 0)
a = query.autocomplete_list[0]
self.assertEqual(query.get_autocomplete_full_query(a), a + ' the query')
def test_external_bang_autocomplete_empty(self):
query_text = 'the query !!'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), 'the query !!')
self.assertEqual(len(query.query_parts), 0)
self.assertFalse(query.specific)
self.assertGreater(len(query.autocomplete_list), 2)
a = query.autocomplete_list[0]
self.assertEqual(query.get_autocomplete_full_query(a), 'the query ' + a)
class TestBang(SearxTestCase):
SPECIFIC_BANGS = ['!dummy_engine', '!du', '!general']
NOT_SPECIFIC_BANGS = ['?dummy_engine', '?du', '?general']
THE_QUERY = 'the query'
def test_bang(self):
initialize(TEST_ENGINES)
for bang in TestBang.SPECIFIC_BANGS + TestBang.NOT_SPECIFIC_BANGS:
with self.subTest(msg="Check bang", bang=bang):
query_text = TestBang.THE_QUERY + ' ' + bang
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), bang + ' ' + TestBang.THE_QUERY)
self.assertEqual(query.query_parts, [bang])
self.assertEqual(query.user_query_parts, TestBang.THE_QUERY.split(' '))
def test_specific(self):
for bang in TestBang.SPECIFIC_BANGS:
with self.subTest(msg="Check bang is specific", bang=bang):
query_text = TestBang.THE_QUERY + ' ' + bang
query = RawTextQuery(query_text, [])
self.assertTrue(query.specific)
def test_not_specific(self):
for bang in TestBang.NOT_SPECIFIC_BANGS:
with self.subTest(msg="Check bang is not specific", bang=bang):
query_text = TestBang.THE_QUERY + ' ' + bang
query = RawTextQuery(query_text, [])
self.assertFalse(query.specific)
def test_bang_not_found(self):
initialize(TEST_ENGINES)
query = RawTextQuery('the query !bang_not_found', [])
self.assertEqual(query.getFullQuery(), 'the query !bang_not_found')
def test_bang_autocomplete(self):
initialize(TEST_ENGINES)
query = RawTextQuery('the query !dum', [])
self.assertEqual(query.autocomplete_list, ['!dummy_engine'])
query = RawTextQuery('!dum the query', [])
self.assertEqual(query.autocomplete_list, [])
self.assertEqual(query.getQuery(), '!dum the query')
def test_bang_autocomplete_empty(self):
initialize()
query = RawTextQuery('the query !', [])
self.assertEqual(query.autocomplete_list, ['!images', '!wikipedia', '!osm'])
query = RawTextQuery('the query ?', ['osm'])
self.assertEqual(query.autocomplete_list, ['?images', '?wikipedia'])