on master help sign in
[enh] add quick answer functionality with an example answerer
commit971ed0abd159625bd01d0b3ca52c90b394711d77 authorAdam Tauber <asciimoo@gmail.com> dateSat 19 Nov 2016, 20:53 (10 years ago) parent55dc538
7 files changed+156−4
searx/answerers/__init__.py+46 searx/answerers/random/answerer.py+50 searx/results.py+5−4 searx/search.py+8 searx/templates/oscar/preferences.html+29 searx/webapp.py+2 tests/unit/test_answerers.py+16
searx/answerers/__init__.pynew file+46
@@ -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)
searx/answerers/random/answerer.pynew file+50
@@ -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]}
searx/results.py+5−4
@@ -146,16 +146,17 @@ class ResultContainer(object):
146146 self._number_of_results.append(result['number_of_results'])
147147 results.remove(result)
148148
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)
152153
153154 if not results:
154155 return
155156
156157 self.results[engine_name].extend(results)
157158
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:
159160 self.paging = True
160161
161162 for i, result in enumerate(results):
searx/search.py+8
@@ -24,6 +24,7 @@ import searx.poolrequests as requests_lib
2424from searx.engines import (
2525 categories, engines
2626)
27+from searx.answerers import ask
2728from searx.utils import gen_useragent
2829from searx.query import RawTextQuery, SearchQuery
2930from searx.results import ResultContainer
@@ -254,6 +255,13 @@ class Search(object):
254255 def search(self):
255256 global number_of_searches
256257
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+
257265 # init vars
258266 requests = []
259267
searx/templates/oscar/preferences.html+29
@@ -12,6 +12,7 @@
1212 <li class="active"><a href="#tab_general" role="tab" data-toggle="tab">{{ _('General') }}</a></li>
1313 <li><a href="#tab_engine" role="tab" data-toggle="tab">{{ _('Engines') }}</a></li>
1414 <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 %}
1516 <li><a href="#tab_cookies" role="tab" data-toggle="tab">{{ _('Cookies') }}</a></li>
1617 </ul>
1718
@@ -224,6 +225,34 @@
224225 </fieldset>
225226 </div>
226227
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+
227256 <div class="tab-pane active_if_nojs" id="tab_cookies">
228257 <noscript>
229258 <h3>{{ _('Cookies') }}</h3>
searx/webapp.py+2
@@ -67,6 +67,7 @@ from searx.query import RawTextQuery
6767from searx.autocomplete import searx_bang, backends as autocomplete_backends
6868from searx.plugins import plugins
6969from searx.preferences import Preferences, ValidationException
70+from searx.answerers import answerers
7071
7172# check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
7273# They are needed for SSL connection without trouble, see #298
@@ -612,6 +613,7 @@ def preferences():
612613 language_codes=language_codes,
613614 engines_by_category=categories,
614615 stats=stats,
616+ answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers],
615617 disabled_engines=disabled_engines,
616618 autocomplete_backends=autocomplete_backends,
617619 shortcuts={y: x for x, y in engine_shortcuts.items()},
tests/unit/test_answerers.pynew file+16
@@ -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))