mirror of
				https://github.com/searxng/searxng
				synced 2024-01-01 19:24:07 +01:00 
			
		
		
		
	[enh] add quick answer functionality with an example answerer
This commit is contained in:
		
							parent
							
								
									55dc538398
								
							
						
					
					
						commit
						971ed0abd1
					
				
					 7 changed files with 156 additions and 4 deletions
				
			
		
							
								
								
									
										46
									
								
								searx/answerers/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								searx/answerers/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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
									
								
								searx/answerers/random/answerer.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								searx/answerers/random/answerer.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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]} | ||||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 = [] | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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
									
								
								tests/unit/test_answerers.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								tests/unit/test_answerers.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -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)) | ||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Adam Tauber
						Adam Tauber