logo

searx

My custom branche(s) on searx, a meta-search engine
commit: 971ed0abd159625bd01d0b3ca52c90b394711d77
parent: 55dc538398090e437c5e495dddad983a7870d09b
Author: Adam Tauber <asciimoo@gmail.com>
Date:   Sat, 19 Nov 2016 20:53:51 +0100

[enh] add quick answer functionality with an example answerer

Diffstat:

Asearx/answerers/__init__.py46++++++++++++++++++++++++++++++++++++++++++++++
Asearx/answerers/random/answerer.py50++++++++++++++++++++++++++++++++++++++++++++++++++
Msearx/results.py9+++++----
Msearx/search.py8++++++++
Msearx/templates/oscar/preferences.html29+++++++++++++++++++++++++++++
Msearx/webapp.py2++
Atests/unit/test_answerers.py16++++++++++++++++
7 files changed, 156 insertions(+), 4 deletions(-)

diff --git a/searx/answerers/__init__.py 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 @@ -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 @@ -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 @@ -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 @@ -12,6 +12,7 @@ <li class="active"><a href="#tab_general" role="tab" data-toggle="tab">{{ _('General') }}</a></li> <li><a href="#tab_engine" role="tab" data-toggle="tab">{{ _('Engines') }}</a></li> <li><a href="#tab_plugins" role="tab" data-toggle="tab">{{ _('Plugins') }}</a></li> + {% if answerers %}<li><a href="#tab_answerers" role="tab" data-toggle="tab">{{ _('Answerers') }}</a></li>{% endif %} <li><a href="#tab_cookies" role="tab" data-toggle="tab">{{ _('Cookies') }}</a></li> </ul> @@ -224,6 +225,34 @@ </fieldset> </div> + {% if answerers %} + <div class="tab-pane active_if_nojs" id="tab_answerers"> + <noscript> + <h3>{{ _('Answerers') }}</h3> + </noscript> + <p class="text-muted" style="margin:20px 0;"> + {{ _('This is the list of searx\'s instant answering modules.') }} + </p> + <table class="table table-striped"> + <tr> + <th class="text-muted">{{ _('Name') }}</th> + <th class="text-muted">{{ _('Keywords') }}</th> + <th class="text-muted">{{ _('Description') }}</th> + <th class="text-muted">{{ _('Examples') }}</th> + </tr> + + {% for answerer in answerers %} + <tr> + <td class="text-muted">{{ answerer.info.name }}</td> + <td class="text-muted">{{ answerer.keywords|join(', ') }}</td> + <td class="text-muted">{{ answerer.info.description }}</td> + <td class="text-muted">{{ answerer.info.examples|join(', ') }}</td> + </tr> + {% endfor %} + </table> + </div> + {% endif %} + <div class="tab-pane active_if_nojs" id="tab_cookies"> <noscript> <h3>{{ _('Cookies') }}</h3> diff --git a/searx/webapp.py b/searx/webapp.py @@ -67,6 +67,7 @@ from searx.query import RawTextQuery from searx.autocomplete import searx_bang, backends as autocomplete_backends from searx.plugins import plugins from searx.preferences import Preferences, ValidationException +from searx.answerers import answerers # check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed. # They are needed for SSL connection without trouble, see #298 @@ -612,6 +613,7 @@ def preferences(): language_codes=language_codes, engines_by_category=categories, stats=stats, + answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers], disabled_engines=disabled_engines, autocomplete_backends=autocomplete_backends, shortcuts={y: x for x, y in engine_shortcuts.items()}, diff --git a/tests/unit/test_answerers.py b/tests/unit/test_answerers.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from mock import Mock + +from searx.answerers import answerers +from searx.testing import SearxTestCase + + +class AnswererTest(SearxTestCase): + + def test_unicode_input(self): + query = Mock() + unicode_payload = u'árvíztűrő tükörfúrógép' + for answerer in answerers: + query.query = u'{} {}'.format(answerer.keywords[0], unicode_payload) + self.assertTrue(isinstance(answerer.answer(query), list))