From 971ed0abd159625bd01d0b3ca52c90b394711d77 Mon Sep 17 00:00:00 2001 From: Adam Tauber Date: Sat, 19 Nov 2016 20:53:51 +0100 Subject: [PATCH] [enh] add quick answer functionality with an example answerer --- searx/answerers/__init__.py | 46 ++++++++++++++++++++++++ searx/answerers/random/answerer.py | 50 ++++++++++++++++++++++++++ searx/results.py | 9 ++--- searx/search.py | 8 +++++ searx/templates/oscar/preferences.html | 29 +++++++++++++++ searx/webapp.py | 2 ++ tests/unit/test_answerers.py | 16 +++++++++ 7 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 searx/answerers/__init__.py create mode 100644 searx/answerers/random/answerer.py create mode 100644 tests/unit/test_answerers.py diff --git a/searx/answerers/__init__.py b/searx/answerers/__init__.py new file mode 100644 index 00000000..8f5951c7 --- /dev/null +++ b/searx/answerers/__init__.py @@ -0,0 +1,46 @@ +from os import listdir +from os.path import realpath, dirname, join, isdir +from searx.utils import load_module +from collections import defaultdict + + +answerers_dir = dirname(realpath(__file__)) + + +def load_answerers(): + answerers = [] + for filename in listdir(answerers_dir): + if not isdir(join(answerers_dir, filename)): + continue + module = load_module('answerer.py', join(answerers_dir, filename)) + if not hasattr(module, 'keywords') or not isinstance(module.keywords, tuple) or not len(module.keywords): + exit(2) + answerers.append(module) + return answerers + + +def get_answerers_by_keywords(answerers): + by_keyword = defaultdict(list) + for answerer in answerers: + for keyword in answerer.keywords: + for keyword in answerer.keywords: + by_keyword[keyword].append(answerer.answer) + return by_keyword + + +def ask(query): + results = [] + query_parts = filter(None, query.query.split()) + + if query_parts[0] not in answerers_by_keywords: + return results + + for answerer in answerers_by_keywords[query_parts[0]]: + result = answerer(query) + if result: + results.append(result) + return results + + +answerers = load_answerers() +answerers_by_keywords = get_answerers_by_keywords(answerers) diff --git a/searx/answerers/random/answerer.py b/searx/answerers/random/answerer.py new file mode 100644 index 00000000..510d9f5b --- /dev/null +++ b/searx/answerers/random/answerer.py @@ -0,0 +1,50 @@ +import random +import string +from flask_babel import gettext + +# required answerer attribute +# specifies which search query keywords triggers this answerer +keywords = ('random',) + +random_int_max = 2**31 + +random_string_letters = string.lowercase + string.digits + string.uppercase + + +def random_string(): + return u''.join(random.choice(random_string_letters) + for _ in range(random.randint(8, 32))) + + +def random_float(): + return unicode(random.random()) + + +def random_int(): + return unicode(random.randint(-random_int_max, random_int_max)) + + +random_types = {u'string': random_string, + u'int': random_int, + u'float': random_float} + + +# required answerer function +# can return a list of results (any result type) for a given query +def answer(query): + parts = query.query.split() + if len(parts) != 2: + return [] + + if parts[1] not in random_types: + return [] + + return [{'answer': random_types[parts[1]]()}] + + +# required answerer function +# returns information about the answerer +def self_info(): + return {'name': gettext('Random value generator'), + 'description': gettext('Generate different random values'), + 'examples': [u'random {}'.format(x) for x in random_types]} diff --git a/searx/results.py b/searx/results.py index 634f71ac..73a96c08 100644 --- a/searx/results.py +++ b/searx/results.py @@ -146,16 +146,17 @@ class ResultContainer(object): self._number_of_results.append(result['number_of_results']) results.remove(result) - with RLock(): - engines[engine_name].stats['search_count'] += 1 - engines[engine_name].stats['result_count'] += len(results) + if engine_name in engines: + with RLock(): + engines[engine_name].stats['search_count'] += 1 + engines[engine_name].stats['result_count'] += len(results) if not results: return self.results[engine_name].extend(results) - if not self.paging and engines[engine_name].paging: + if not self.paging and engine_name in engines and engines[engine_name].paging: self.paging = True for i, result in enumerate(results): diff --git a/searx/search.py b/searx/search.py index c3f1566a..0095de82 100644 --- a/searx/search.py +++ b/searx/search.py @@ -24,6 +24,7 @@ import searx.poolrequests as requests_lib from searx.engines import ( categories, engines ) +from searx.answerers import ask from searx.utils import gen_useragent from searx.query import RawTextQuery, SearchQuery from searx.results import ResultContainer @@ -254,6 +255,13 @@ class Search(object): def search(self): global number_of_searches + answerers_results = ask(self.search_query) + + if answerers_results: + for results in answerers_results: + self.result_container.extend('answer', results) + return self.result_container + # init vars requests = [] diff --git a/searx/templates/oscar/preferences.html b/searx/templates/oscar/preferences.html index ed790c56..6ad79509 100644 --- a/searx/templates/oscar/preferences.html +++ b/searx/templates/oscar/preferences.html @@ -12,6 +12,7 @@
  • {{ _('General') }}
  • {{ _('Engines') }}
  • {{ _('Plugins') }}
  • + {% if answerers %}
  • {{ _('Answerers') }}
  • {% endif %}
  • {{ _('Cookies') }}
  • @@ -224,6 +225,34 @@ + {% if answerers %} +
    + +

    + {{ _('This is the list of searx\'s instant answering modules.') }} +

    + + + + + + + + + {% for answerer in answerers %} + + + + + + + {% endfor %} +
    {{ _('Name') }}{{ _('Keywords') }}{{ _('Description') }}{{ _('Examples') }}
    {{ answerer.info.name }}{{ answerer.keywords|join(', ') }}{{ answerer.info.description }}{{ answerer.info.examples|join(', ') }}
    +
    + {% endif %} +