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 View File

@@ -0,0 +1,46 @@
1
+from os import listdir
2
+from os.path import realpath, dirname, join, isdir
3
+from searx.utils import load_module
4
+from collections import defaultdict
5
+
6
+
7
+answerers_dir = dirname(realpath(__file__))
8
+
9
+
10
+def load_answerers():
11
+    answerers = []
12
+    for filename in listdir(answerers_dir):
13
+        if not isdir(join(answerers_dir, filename)):
14
+            continue
15
+        module = load_module('answerer.py', join(answerers_dir, filename))
16
+        if not hasattr(module, 'keywords') or not isinstance(module.keywords, tuple) or not len(module.keywords):
17
+            exit(2)
18
+        answerers.append(module)
19
+    return answerers
20
+
21
+
22
+def get_answerers_by_keywords(answerers):
23
+    by_keyword = defaultdict(list)
24
+    for answerer in answerers:
25
+        for keyword in answerer.keywords:
26
+            for keyword in answerer.keywords:
27
+                by_keyword[keyword].append(answerer.answer)
28
+    return by_keyword
29
+
30
+
31
+def ask(query):
32
+    results = []
33
+    query_parts = filter(None, query.query.split())
34
+
35
+    if query_parts[0] not in answerers_by_keywords:
36
+        return results
37
+
38
+    for answerer in answerers_by_keywords[query_parts[0]]:
39
+        result = answerer(query)
40
+        if result:
41
+            results.append(result)
42
+    return results
43
+
44
+
45
+answerers = load_answerers()
46
+answerers_by_keywords = get_answerers_by_keywords(answerers)

+ 50
- 0
searx/answerers/random/answerer.py View File

@@ -0,0 +1,50 @@
1
+import random
2
+import string
3
+from flask_babel import gettext
4
+
5
+# required answerer attribute
6
+# specifies which search query keywords triggers this answerer
7
+keywords = ('random',)
8
+
9
+random_int_max = 2**31
10
+
11
+random_string_letters = string.lowercase + string.digits + string.uppercase
12
+
13
+
14
+def random_string():
15
+    return u''.join(random.choice(random_string_letters)
16
+                    for _ in range(random.randint(8, 32)))
17
+
18
+
19
+def random_float():
20
+    return unicode(random.random())
21
+
22
+
23
+def random_int():
24
+    return unicode(random.randint(-random_int_max, random_int_max))
25
+
26
+
27
+random_types = {u'string': random_string,
28
+                u'int': random_int,
29
+                u'float': random_float}
30
+
31
+
32
+# required answerer function
33
+# can return a list of results (any result type) for a given query
34
+def answer(query):
35
+    parts = query.query.split()
36
+    if len(parts) != 2:
37
+        return []
38
+
39
+    if parts[1] not in random_types:
40
+        return []
41
+
42
+    return [{'answer': random_types[parts[1]]()}]
43
+
44
+
45
+# required answerer function
46
+# returns information about the answerer
47
+def self_info():
48
+    return {'name': gettext('Random value generator'),
49
+            'description': gettext('Generate different random values'),
50
+            'examples': [u'random {}'.format(x) for x in random_types]}

+ 5
- 4
searx/results.py View File

@@ -146,16 +146,17 @@ class ResultContainer(object):
146 146
                 self._number_of_results.append(result['number_of_results'])
147 147
                 results.remove(result)
148 148
 
149
-        with RLock():
150
-            engines[engine_name].stats['search_count'] += 1
151
-            engines[engine_name].stats['result_count'] += len(results)
149
+        if engine_name in engines:
150
+            with RLock():
151
+                engines[engine_name].stats['search_count'] += 1
152
+                engines[engine_name].stats['result_count'] += len(results)
152 153
 
153 154
         if not results:
154 155
             return
155 156
 
156 157
         self.results[engine_name].extend(results)
157 158
 
158
-        if not self.paging and engines[engine_name].paging:
159
+        if not self.paging and engine_name in engines and engines[engine_name].paging:
159 160
             self.paging = True
160 161
 
161 162
         for i, result in enumerate(results):

+ 8
- 0
searx/search.py View File

@@ -24,6 +24,7 @@ import searx.poolrequests as requests_lib
24 24
 from searx.engines import (
25 25
     categories, engines
26 26
 )
27
+from searx.answerers import ask
27 28
 from searx.utils import gen_useragent
28 29
 from searx.query import RawTextQuery, SearchQuery
29 30
 from searx.results import ResultContainer
@@ -254,6 +255,13 @@ class Search(object):
254 255
     def search(self):
255 256
         global number_of_searches
256 257
 
258
+        answerers_results = ask(self.search_query)
259
+
260
+        if answerers_results:
261
+            for results in answerers_results:
262
+                self.result_container.extend('answer', results)
263
+            return self.result_container
264
+
257 265
         # init vars
258 266
         requests = []
259 267
 

+ 29
- 0
searx/templates/oscar/preferences.html View File

@@ -12,6 +12,7 @@
12 12
           <li class="active"><a href="#tab_general" role="tab" data-toggle="tab">{{ _('General') }}</a></li>
13 13
           <li><a href="#tab_engine" role="tab" data-toggle="tab">{{ _('Engines') }}</a></li>
14 14
           <li><a href="#tab_plugins" role="tab" data-toggle="tab">{{ _('Plugins') }}</a></li>
15
+          {% if answerers %}<li><a href="#tab_answerers" role="tab" data-toggle="tab">{{ _('Answerers') }}</a></li>{% endif %}
15 16
           <li><a href="#tab_cookies" role="tab" data-toggle="tab">{{ _('Cookies') }}</a></li>
16 17
         </ul>
17 18
 
@@ -224,6 +225,34 @@
224 225
                 </fieldset>
225 226
             </div>
226 227
 
228
+            {% if answerers %}
229
+            <div class="tab-pane active_if_nojs" id="tab_answerers">
230
+                <noscript>
231
+                    <h3>{{ _('Answerers') }}</h3>
232
+                </noscript>
233
+                <p class="text-muted" style="margin:20px 0;">
234
+                    {{ _('This is the list of searx\'s instant answering modules.') }}
235
+                </p>
236
+                <table class="table table-striped">
237
+                    <tr>
238
+                        <th class="text-muted">{{ _('Name') }}</th>
239
+                        <th class="text-muted">{{ _('Keywords') }}</th>
240
+                        <th class="text-muted">{{ _('Description') }}</th>
241
+                        <th class="text-muted">{{ _('Examples') }}</th>
242
+                    </tr>
243
+
244
+                    {% for answerer in answerers %}
245
+                    <tr>
246
+                        <td class="text-muted">{{ answerer.info.name }}</td>
247
+                        <td class="text-muted">{{ answerer.keywords|join(', ') }}</td>
248
+                        <td class="text-muted">{{ answerer.info.description }}</td>
249
+                        <td class="text-muted">{{ answerer.info.examples|join(', ') }}</td>
250
+                    </tr>
251
+                    {% endfor %}
252
+                </table>
253
+            </div>
254
+            {% endif %}
255
+
227 256
             <div class="tab-pane active_if_nojs" id="tab_cookies">
228 257
                 <noscript>
229 258
                     <h3>{{ _('Cookies') }}</h3>

+ 2
- 0
searx/webapp.py View File

@@ -67,6 +67,7 @@ from searx.query import RawTextQuery
67 67
 from searx.autocomplete import searx_bang, backends as autocomplete_backends
68 68
 from searx.plugins import plugins
69 69
 from searx.preferences import Preferences, ValidationException
70
+from searx.answerers import answerers
70 71
 
71 72
 # check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
72 73
 # They are needed for SSL connection without trouble, see #298
@@ -612,6 +613,7 @@ def preferences():
612 613
                   language_codes=language_codes,
613 614
                   engines_by_category=categories,
614 615
                   stats=stats,
616
+                  answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers],
615 617
                   disabled_engines=disabled_engines,
616 618
                   autocomplete_backends=autocomplete_backends,
617 619
                   shortcuts={y: x for x, y in engine_shortcuts.items()},

+ 16
- 0
tests/unit/test_answerers.py View File

@@ -0,0 +1,16 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from mock import Mock
4
+
5
+from searx.answerers import answerers
6
+from searx.testing import SearxTestCase
7
+
8
+
9
+class AnswererTest(SearxTestCase):
10
+
11
+    def test_unicode_input(self):
12
+        query = Mock()
13
+        unicode_payload = u'árvíztűrő tükörfúrógép'
14
+        for answerer in answerers:
15
+            query.query = u'{} {}'.format(answerer.keywords[0], unicode_payload)
16
+            self.assertTrue(isinstance(answerer.answer(query), list))