Browse Source

[enh] add quick answer functionality with an example answerer

Adam Tauber 8 years ago
parent
commit
971ed0abd1

+ 46 - 0
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)

+ 50 - 0
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]}

+ 5 - 4
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):

+ 8 - 0
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 = []
 

+ 29 - 0
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>

+ 2 - 0
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()},

+ 16 - 0
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))