on master help sign in
[enh] add infoboxes and answers
6bfd566Dalf
searx/engines/currency_convert.py+5−6
@@ -38,16 +38,14 @@ def response(resp):
38 except:
38 except:
39 return results
39 return results
40
40
41− title = '{0} {1} in {2} is {3}'.format(
41+ answer = '{0} {1} = {2} {3} (1 {1} = {4} {3})'.format(
42 resp.search_params['ammount'],
42 resp.search_params['ammount'],
43 resp.search_params['from'],
43 resp.search_params['from'],
44+ resp.search_params['ammount'] * conversion_rate,
44 resp.search_params['to'],
45 resp.search_params['to'],
45− resp.search_params['ammount'] * conversion_rate
46+ conversion_rate
46 )
47 )
47
48
48− content = '1 {0} is {1} {2}'.format(resp.search_params['from'],
49− conversion_rate,
50− resp.search_params['to'])
51 now_date = datetime.now().strftime('%Y%m%d')
49 now_date = datetime.now().strftime('%Y%m%d')
52 url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa
50 url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa
53 url = url.format(
51 url = url.format(
@@ -56,6 +54,7 @@ def response(resp):
56 resp.search_params['from'].lower(),
54 resp.search_params['from'].lower(),
57 resp.search_params['to'].lower()
55 resp.search_params['to'].lower()
58 )
56 )
57+
59− results.append({'title': title, 'content': content, 'url': url})
58+ results.append({'answer' : answer, 'url': url})
60
59
61 return results
60 return results
searx/engines/duckduckgo_definitions.py+114−7
@@ -1,10 +1,25 @@
1import json
1import json
2from urllib import urlencode
2from urllib import urlencode
3+from lxml import html
4+from searx.engines.xpath import extract_text
3
5
4−url = 'http://api.duckduckgo.com/?{query}&format=json&pretty=0&no_redirect=1'
6+url = 'https://api.duckduckgo.com/?{query}&format=json&pretty=0&no_redirect=1&d=1'
5
7
8+def result_to_text(url, text, htmlResult):
9+ # TODO : remove result ending with "Meaning" or "Category"
10+ dom = html.fromstring(htmlResult)
11+ a = dom.xpath('//a')
12+ if len(a)>=1:
13+ return extract_text(a[0])
14+ else:
15+ return text
16+
17+def html_to_text(htmlFragment):
18+ dom = html.fromstring(htmlFragment)
19+ return extract_text(dom)
6
20
7def request(query, params):
21def request(query, params):
22+ # TODO add kl={locale}
8 params['url'] = url.format(query=urlencode({'q': query}))
23 params['url'] = url.format(query=urlencode({'q': query}))
9 return params
24 return params
10
25
@@ -12,12 +27,104 @@ def request(query, params):
12def response(resp):
27def response(resp):
13 search_res = json.loads(resp.text)
28 search_res = json.loads(resp.text)
14 results = []
29 results = []
30+
31+ content = ''
32+ heading = search_res.get('Heading', '')
33+ attributes = []
34+ urls = []
35+ infobox_id = None
36+ relatedTopics = []
37+
38+ # add answer if there is one
39+ answer = search_res.get('Answer', '')
40+ if answer != '':
41+ results.append({ 'answer' : html_to_text(answer) })
42+
43+ # add infobox
15 if 'Definition' in search_res:
44 if 'Definition' in search_res:
16− if search_res.get('AbstractURL'):
17− res = {'title': search_res.get('Heading', ''),
18− 'content': search_res.get('Definition', ''),
45+ content = content + search_res.get('Definition', '')
19− 'url': search_res.get('AbstractURL', ''),
46+
20− 'class': 'definition_result'}
47+ if 'Abstract' in search_res:
48+ content = content + search_res.get('Abstract', '')
49+
50+
51+ # image
52+ image = search_res.get('Image', '')
53+ image = None if image == '' else image
54+
55+ # attributes
56+ if 'Infobox' in search_res:
57+ infobox = search_res.get('Infobox', None)
58+ if 'content' in infobox:
59+ for info in infobox.get('content'):
60+ attributes.append({'label': info.get('label'), 'value': info.get('value')})
61+
62+ # urls
63+ for ddg_result in search_res.get('Results', []):
64+ if 'FirstURL' in ddg_result:
65+ firstURL = ddg_result.get('FirstURL', '')
66+ text = ddg_result.get('Text', '')
67+ urls.append({'title':text, 'url':firstURL})
68+ results.append({'title':heading, 'url': firstURL})
69+
70+ # related topics
71+ for ddg_result in search_res.get('RelatedTopics', None):
72+ if 'FirstURL' in ddg_result:
73+ suggestion = result_to_text(ddg_result.get('FirstURL', None), ddg_result.get('Text', None), ddg_result.get('Result', None))
74+ if suggestion != heading:
75+ results.append({'suggestion': suggestion})
76+ elif 'Topics' in ddg_result:
77+ suggestions = []
78+ relatedTopics.append({ 'name' : ddg_result.get('Name', ''), 'suggestions': suggestions })
79+ for topic_result in ddg_result.get('Topics', []):
80+ suggestion = result_to_text(topic_result.get('FirstURL', None), topic_result.get('Text', None), topic_result.get('Result', None))
81+ if suggestion != heading:
82+ suggestions.append(suggestion)
83+
84+ # abstract
85+ abstractURL = search_res.get('AbstractURL', '')
86+ if abstractURL != '':
87+ # add as result ? problem always in english
88+ infobox_id = abstractURL
89+ urls.append({'title': search_res.get('AbstractSource'), 'url': abstractURL})
90+
91+ # definition
92+ definitionURL = search_res.get('DefinitionURL', '')
93+ if definitionURL != '':
94+ # add as result ? as answer ? problem always in english
95+ infobox_id = definitionURL
96+ urls.append({'title': search_res.get('DefinitionSource'), 'url': definitionURL})
97+
98+ # entity
99+ entity = search_res.get('Entity', None)
100+ # TODO continent / country / department / location / waterfall / mountain range : link to map search, get weather, near by locations
101+ # TODO musician : link to music search
102+ # TODO concert tour : ??
103+ # TODO film / actor / television / media franchise : links to IMDB / rottentomatoes (or scrap result)
104+ # TODO music : link tu musicbrainz / last.fm
105+ # TODO book : ??
106+ # TODO artist / playwright : ??
107+ # TODO compagny : ??
108+ # TODO software / os : ??
109+ # TODO software engineer : ??
110+ # TODO prepared food : ??
111+ # TODO website : ??
112+ # TODO performing art : ??
113+ # TODO prepared food : ??
114+ # TODO programming language : ??
115+ # TODO file format : ??
116+
117+ if len(heading)>0:
118+ # TODO get infobox.meta.value where .label='article_title'
21− results.append(res)
119+ results.append({
120+ 'infobox': heading,
121+ 'id': infobox_id,
122+ 'entity': entity,
123+ 'content': content,
124+ 'img_src' : image,
125+ 'attributes': attributes,
126+ 'urls': urls,
127+ 'relatedTopics': relatedTopics
128+ })
22
129
23 return results
130 return results
searx/engines/wikidata.pynew file+193
@@ -0,0 +1,193 @@
1+import json
2+from datetime import datetime
3+from requests import get
4+from urllib import urlencode
5+
6+resultCount=2
7+urlSearch = 'https://www.wikidata.org/w/api.php?action=query&list=search&format=json&srnamespace=0&srprop=sectionsnippet&{query}'
8+urlDetail = 'https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=labels%7Cinfo%7Csitelinks%7Csitelinks%2Furls%7Cdescriptions%7Cclaims&{query}'
9+# find the right URL for urlMap
10+urlMap = 'http://www.openstreetmap.org/?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M'
11+
12+def request(query, params):
13+ params['url'] = urlSearch.format(query=urlencode({'srsearch': query, 'srlimit': resultCount}))
14+ print params['url']
15+ return params
16+
17+
18+def response(resp):
19+ results = []
20+ search_res = json.loads(resp.text)
21+ # TODO parallel http queries
22+ before = datetime.now()
23+ for r in search_res.get('query', {}).get('search', {}):
24+ wikidata_id = r.get('title', '')
25+ results = results + getDetail(wikidata_id)
26+ after = datetime.now()
27+ print str(after - before) + " second(s)"
28+
29+ return results
30+
31+def getDetail(wikidata_id):
32+ language = 'fr'
33+
34+ url = urlDetail.format(query=urlencode({'ids': wikidata_id, 'languages': language + '|en'}))
35+ print url
36+ response = get(url)
37+ result = json.loads(response.content)
38+ result = result.get('entities', {}).get(wikidata_id, {})
39+
40+ title = result.get('labels', {}).get(language, {}).get('value', None)
41+ if title == None:
42+ title = result.get('labels', {}).get('en', {}).get('value', wikidata_id)
43+ results = []
44+ urls = []
45+ attributes = []
46+
47+ description = result.get('descriptions', {}).get(language, {}).get('value', '')
48+ if description == '':
49+ description = result.get('descriptions', {}).get('en', {}).get('value', '')
50+
51+ claims = result.get('claims', {})
52+ official_website = get_string(claims, 'P856', None)
53+ print official_website
54+ if official_website != None:
55+ urls.append({ 'title' : 'Official site', 'url': official_website })
56+ results.append({ 'title': title, 'url' : official_website })
57+
58+ if language != 'en':
59+ add_url(urls, 'Wikipedia (' + language + ')', get_wikilink(result, language + 'wiki'))
60+ wikipedia_en_link = get_wikilink(result, 'enwiki')
61+ add_url(urls, 'Wikipedia (en)', wikipedia_en_link)
62+
63+ if language != 'en':
64+ add_url(urls, 'Wiki voyage (' + language + ')', get_wikilink(result, language + 'wikivoyage'))
65+ add_url(urls, 'Wiki voyage (en)', get_wikilink(result, 'enwikivoyage'))
66+
67+ if language != 'en':
68+ add_url(urls, 'Wikiquote (' + language + ')', get_wikilink(result, language + 'wikiquote'))
69+ add_url(urls, 'Wikiquote (en)', get_wikilink(result, 'enwikiquote'))
70+
71+
72+ add_url(urls, 'Commons wiki', get_wikilink(result, 'commonswiki'))
73+
74+ add_url(urls, 'Location', get_geolink(claims, 'P625', None))
75+
76+ add_url(urls, 'Wikidata', 'https://www.wikidata.org/wiki/' + wikidata_id + '?uselang='+ language)
77+
78+ postal_code = get_string(claims, 'P281', None)
79+ if postal_code != None:
80+ attributes.append({'label' : 'Postal code(s)', 'value' : postal_code})
81+
82+ date_of_birth = get_time(claims, 'P569', None)
83+ if date_of_birth != None:
84+ attributes.append({'label' : 'Date of birth', 'value' : date_of_birth})
85+
86+ date_of_death = get_time(claims, 'P570', None)
87+ if date_of_death != None:
88+ attributes.append({'label' : 'Date of death', 'value' : date_of_death})
89+
90+
91+ results.append({
92+ 'infobox' : title,
93+ 'id' : wikipedia_en_link,
94+ 'content' : description,
95+ 'attributes' : attributes,
96+ 'urls' : urls
97+ })
98+
99+ return results
100+
101+def add_url(urls, title, url):
102+ if url != None:
103+ urls.append({'title' : title, 'url' : url})
104+
105+def get_mainsnak(claims, propertyName):
106+ propValue = claims.get(propertyName, {})
107+ if len(propValue) == 0:
108+ return None
109+
110+ propValue = propValue[0].get('mainsnak', None)
111+ return propValue
112+
113+def get_string(claims, propertyName, defaultValue=None):
114+ propValue = claims.get(propertyName, {})
115+ if len(propValue) == 0:
116+ return defaultValue
117+
118+ result = []
119+ for e in propValue:
120+ mainsnak = e.get('mainsnak', {})
121+
122+ datatype = mainsnak.get('datatype', '')
123+ datavalue = mainsnak.get('datavalue', {})
124+ if datavalue != None:
125+ result.append(datavalue.get('value', ''))
126+
127+ if len(result) == 0:
128+ return defaultValue
129+ else:
130+ return ', '.join(result)
131+
132+def get_time(claims, propertyName, defaultValue=None):
133+ propValue = claims.get(propertyName, {})
134+ if len(propValue) == 0:
135+ return defaultValue
136+
137+ result = []
138+ for e in propValue:
139+ mainsnak = e.get('mainsnak', {})
140+
141+ datatype = mainsnak.get('datatype', '')
142+ datavalue = mainsnak.get('datavalue', {})
143+ if datavalue != None:
144+ value = datavalue.get('value', '')
145+ result.append(value.get('time', ''))
146+
147+ if len(result) == 0:
148+ return defaultValue
149+ else:
150+ return ', '.join(result)
151+
152+def get_geolink(claims, propertyName, defaultValue=''):
153+ mainsnak = get_mainsnak(claims, propertyName)
154+
155+ if mainsnak == None:
156+ return defaultValue
157+
158+ datatype = mainsnak.get('datatype', '')
159+ datavalue = mainsnak.get('datavalue', {})
160+
161+ if datatype != 'globe-coordinate':
162+ return defaultValue
163+
164+ value = datavalue.get('value', {})
165+
166+ precision = value.get('precision', 0.0002)
167+
168+ # there is no zoom information, deduce from precision (error prone)
169+ # samples :
170+ # 13 --> 5
171+ # 1 --> 6
172+ # 0.016666666666667 --> 9
173+ # 0.00027777777777778 --> 19
174+ # wolframalpha : quadratic fit { {13, 5}, {1, 6}, {0.0166666, 9}, {0.0002777777,19}}
175+ # 14.1186-8.8322 x+0.625447 x^2
176+ if precision < 0.0003:
177+ zoom = 19
178+ else:
179+ zoom = int(15 - precision*8.8322 + precision*precision*0.625447)
180+
181+ url = urlMap.replace('{latitude}', str(value.get('latitude',0))).replace('{longitude}', str(value.get('longitude',0))).replace('{zoom}', str(zoom))
182+
183+ return url
184+
185+def get_wikilink(result, wikiid):
186+ url = result.get('sitelinks', {}).get(wikiid, {}).get('url', None)
187+ if url == None:
188+ return url
189+ elif url.startswith('http://'):
190+ url = url.replace('http://', 'https://')
191+ elif url.startswith('//'):
192+ url = 'https:' + url
193+ return url
searx/search.py+93−11
@@ -38,17 +38,14 @@ def default_request_params():
38
38
39
39
40# create a callback wrapper for the search engine results
40# create a callback wrapper for the search engine results
41−def make_callback(engine_name, results, suggestions, callback, params):
41+def make_callback(engine_name, results, suggestions, answers, infoboxes, callback, params):
42
42
43 # creating a callback wrapper for the search engine results
43 # creating a callback wrapper for the search engine results
44 def process_callback(response, **kwargs):
44 def process_callback(response, **kwargs):
45 cb_res = []
45 cb_res = []
46 response.search_params = params
46 response.search_params = params
47
47
48− # update stats with current page-load-time
48+ # callback
49− engines[engine_name].stats['page_load_time'] += \
50− (datetime.now() - params['started']).total_seconds()
51−
52 try:
49 try:
53 search_results = callback(response)
50 search_results = callback(response)
54 except Exception, e:
51 except Exception, e:
@@ -61,6 +58,7 @@ def make_callback(engine_name, results, suggestions, callback, params):
61 engine_name, str(e))
58 engine_name, str(e))
62 return
59 return
63
60
61+ # add results
64 for result in search_results:
62 for result in search_results:
65 result['engine'] = engine_name
63 result['engine'] = engine_name
66
64
@@ -70,21 +68,38 @@ def make_callback(engine_name, results, suggestions, callback, params):
70 suggestions.add(result['suggestion'])
68 suggestions.add(result['suggestion'])
71 continue
69 continue
72
70
71+ # if it is an answer, add it to list of answers
72+ if 'answer' in result:
73+ answers.add(result['answer'])
74+ continue
75+
76+ # if it is an infobox, add it to list of infoboxes
77+ if 'infobox' in result:
78+ infoboxes.append(result)
79+ print result
80+ continue
81+
73 # append result
82 # append result
74 cb_res.append(result)
83 cb_res.append(result)
75
84
76 results[engine_name] = cb_res
85 results[engine_name] = cb_res
77
86
87+ # update stats with current page-load-time
88+ engines[engine_name].stats['page_load_time'] += \
89+ (datetime.now() - params['started']).total_seconds()
90+
78 return process_callback
91 return process_callback
79
92
93+
80# return the meaningful length of the content for a result
94# return the meaningful length of the content for a result
81−def content_result_len(result):
95+def content_result_len(content):
82− if isinstance(result.get('content'), basestring):
96+ if isinstance(content, basestring):
83− content = re.sub('[,;:!?\./\\\\ ()-_]', '', result.get('content'))
97+ content = re.sub('[,;:!?\./\\\\ ()-_]', '', content)
84 return len(content)
98 return len(content)
85 else:
99 else:
86 return 0
100 return 0
87
101
102+
88# score results and remove duplications
103# score results and remove duplications
89def score_results(results):
104def score_results(results):
90 # calculate scoring parameters
105 # calculate scoring parameters
@@ -138,7 +153,7 @@ def score_results(results):
138 # merge duplicates together
153 # merge duplicates together
139 if duplicated:
154 if duplicated:
140 # using content with more text
155 # using content with more text
141− if content_result_len(res) > content_result_len(duplicated):
156+ if content_result_len(res.get('content', '')) > content_result_len(duplicated.get('content', '')):
142 duplicated['content'] = res['content']
157 duplicated['content'] = res['content']
143
158
144 # increase result-score
159 # increase result-score
@@ -197,6 +212,64 @@ def score_results(results):
197 return gresults
212 return gresults
198
213
199
214
215+def merge_two_infoboxes(infobox1, infobox2):
216+ if 'urls' in infobox2:
217+ urls1 = infobox1.get('urls', None)
218+ if urls1 == None:
219+ urls1 = []
220+ infobox1.set('urls', urls1)
221+
222+ urlSet = set()
223+ for url in infobox1.get('urls', []):
224+ urlSet.add(url.get('url', None))
225+
226+ for url in infobox2.get('urls', []):
227+ if url.get('url', None) not in urlSet:
228+ urls1.append(url)
229+
230+ if 'attributes' in infobox2:
231+ attributes1 = infobox1.get('attributes', None)
232+ if attributes1 == None:
233+ attributes1 = []
234+ infobox1.set('attributes', attributes1)
235+
236+ attributeSet = set()
237+ for attribute in infobox1.get('attributes', []):
238+ if attribute.get('label', None) not in attributeSet:
239+ attributeSet.add(attribute.get('label', None))
240+
241+ for attribute in infobox2.get('attributes', []):
242+ attributes1.append(attribute)
243+
244+ if 'content' in infobox2:
245+ content1 = infobox1.get('content', None)
246+ content2 = infobox2.get('content', '')
247+ if content1 != None:
248+ if content_result_len(content2) > content_result_len(content1):
249+ infobox1['content'] = content2
250+ else:
251+ infobox1.set('content', content2)
252+
253+
254+def merge_infoboxes(infoboxes):
255+ results = []
256+ infoboxes_id = {}
257+ for infobox in infoboxes:
258+ add_infobox = True
259+ infobox_id = infobox.get('id', None)
260+ if infobox_id != None:
261+ existingIndex = infoboxes_id.get(infobox_id, None)
262+ if existingIndex != None:
263+ merge_two_infoboxes(results[existingIndex], infobox)
264+ add_infobox=False
265+
266+ if add_infobox:
267+ results.append(infobox)
268+ infoboxes_id[infobox_id] = len(results)-1
269+
270+ return results
271+
272+
200class Search(object):
273class Search(object):
201
274
202 """Search information container"""
275 """Search information container"""
@@ -219,6 +292,8 @@ class Search(object):
219
292
220 self.results = []
293 self.results = []
221 self.suggestions = []
294 self.suggestions = []
295+ self.answers = []
296+ self.infoboxes = []
222 self.request_data = {}
297 self.request_data = {}
223
298
224 # set specific language if set
299 # set specific language if set
@@ -350,6 +425,8 @@ class Search(object):
350 requests = []
425 requests = []
351 results = {}
426 results = {}
352 suggestions = set()
427 suggestions = set()
428+ answers = set()
429+ infoboxes = []
353
430
354 # increase number of searches
431 # increase number of searches
355 number_of_searches += 1
432 number_of_searches += 1
@@ -394,6 +471,8 @@ class Search(object):
394 selected_engine['name'],
471 selected_engine['name'],
395 results,
472 results,
396 suggestions,
473 suggestions,
474+ answers,
475+ infoboxes,
397 engine.response,
476 engine.response,
398 request_params
477 request_params
399 )
478 )
@@ -431,11 +510,14 @@ class Search(object):
431 # score results and remove duplications
510 # score results and remove duplications
432 results = score_results(results)
511 results = score_results(results)
433
512
513+ # merge infoboxes according to their ids
514+ infoboxes = merge_infoboxes(infoboxes)
515+
434 # update engine stats, using calculated score
516 # update engine stats, using calculated score
435 for result in results:
517 for result in results:
436 for res_engine in result['engines']:
518 for res_engine in result['engines']:
437 engines[result['engine']]\
519 engines[result['engine']]\
438 .stats['score_count'] += result['score']
520 .stats['score_count'] += result['score']
439
521
440− # return results and suggestions
522+ # return results, suggestions, answers and infoboxes
441− return results, suggestions
523+ return results, suggestions, answers, infoboxes
searx/settings.yml+1−1
@@ -1,7 +1,7 @@
1server:
1server:
2 port : 8888
2 port : 8888
3 secret_key : "ultrasecretkey" # change this!
3 secret_key : "ultrasecretkey" # change this!
4− debug : False # Debug mode, only for development
4+ debug : True # Debug mode, only for development
5 request_timeout : 2.0 # seconds
5 request_timeout : 2.0 # seconds
6 base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/"
6 base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/"
7 themes_path : "" # Custom ui themes path
7 themes_path : "" # Custom ui themes path
searx/static/default/css/style.css+1−75
@@ -1,75 +1 @@
1−html{font-family:sans-serif;font-size:.9em;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;color:#444;padding:0;margin:0}
1+html{font-family:sans-serif;font-size:.9em;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;color:#444;padding:0;margin:0}body,#container{padding:0;margin:0}#container{width:100%;position:absolute;top:0}.search{padding:0;margin:0}.search .checkbox_container label{font-size:.9em;border-bottom:2px solid #e8e7e6}.search .checkbox_container label:hover{border-bottom:2px solid #3498db}.search .checkbox_container input[type="checkbox"]:checked+label{border-bottom:2px solid #2980b9}#search_wrapper{position:relative;width:50em;padding:10px}.center #search_wrapper{margin-left:auto;margin-right:auto}.q{background:none repeat scroll 0 0 #fff;border:1px solid #3498db;color:#222;font-size:16px;height:28px;margin:0;outline:medium none;padding:2px;padding-left:8px;padding-right:0 !important;width:100%;z-index:2}#search_submit{position:absolute;top:13px;right:1px;padding:0;border:0;background:url('../img/search-icon.png') no-repeat;background-size:24px 24px;opacity:.8;width:24px;height:30px;font-size:0}@media screen and (max-width:50em){#search_wrapper{width:90%;clear:both;overflow:hidden}}ul.autocompleter-choices{position:absolute;margin:0;padding:0;list-style:none;border:1px solid #3498db;border-left-color:#3498db;border-right-color:#3498db;border-bottom-color:#3498db;text-align:left;font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;z-index:50;background-color:#fff;color:#444}ul.autocompleter-choices li{position:relative;margin:-2px 0 0 0;padding:.2em 1.5em .2em 1em;display:block;float:none !important;cursor:pointer;font-weight:normal;white-space:nowrap;font-size:1em;line-height:1.5em}ul.autocompleter-choices li.autocompleter-selected{background-color:#444;color:#fff}ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried{color:#9fcfff}ul.autocompleter-choices span.autocompleter-queried{display:inline;float:none;font-weight:bold;margin:0;padding:0}.row{max-width:800px;margin:20px auto;text-align:justify}.row h1{font-size:3em;margin-top:50px}.row p{padding:0 10px;max-width:700px}.row h3,.row ul{margin:4px 8px}.hmarg{margin:0 20px;border:1px solid #3498db;padding:4px 10px}a:link.hmarg{color:#3498db}a:visited.hmarg{color:#3498db}a:active.hmarg{color:#3498db}a:hover.hmarg{color:#3498db}.top_margin{margin-top:60px}.center{text-align:center}h1{font-size:5em}div.title{background:url('../img/searx.png') no-repeat;width:100%;background-position:center}div.title h1{visibility:hidden}input[type="submit"]{padding:2px 6px;margin:2px 4px;display:inline-block;background:#3498db;color:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0;cursor:pointer}input[type="checkbox"]{visibility:hidden}fieldset{margin:8px;border:1px solid #3498db}#categories{margin:0 10px}.checkbox_container{display:inline-block;position:relative;margin:0 3px;padding:0}.checkbox_container input{display:none}.checkbox_container label,.engine_checkbox label{cursor:pointer;padding:4px 10px;margin:0;display:block;text-transform:capitalize;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkbox_container input[type="checkbox"]:checked+label{background:#3498db;color:#fff}.engine_checkbox{padding:4px}label.allow{background:#e74c3c;padding:4px 8px;color:#fff;display:none}label.deny{background:#2ecc71;padding:4px 8px;color:#444;display:inline}.engine_checkbox input[type="checkbox"]:checked+label:nth-child(2)+label{display:none}.engine_checkbox input[type="checkbox"]:checked+label.allow{display:inline}a{text-decoration:none;color:#1a11be}a:visited{color:#8e44ad}.result{margin:19px 0 18px 0;padding:0;clear:both}.result_title{margin-bottom:0}.result_title a{color:#2980b9;font-weight:normal;font-size:1.1em}.result_title a:hover{text-decoration:underline}.result_title a:visited{color:#8e44ad}.cache_link{font-size:10px !important}.result h3{font-size:1em;word-wrap:break-word;margin:5px 0 1px 0;padding:0}.result .content{font-size:.8em;margin:0;padding:0;max-width:54em;word-wrap:break-word;line-height:1.24}.result .content img{float:left;margin-right:5px;max-width:200px;max-height:100px}.result .content br.last{clear:both}.result .url{font-size:.8em;margin:0 0 3px 0;padding:0;max-width:54em;word-wrap:break-word;color:#c0392b}.result .published_date{font-size:.8em;color:#888;margin:5px 20px}.engines{color:#888}.small_font{font-size:.8em}.small p{margin:2px 0}.right{float:right}.invisible{display:none}.left{float:left}.highlight{color:#094089}.content .highlight{color:#000}.image_result{float:left;margin:10px 10px;position:relative;height:160px}.image_result img{border:0;height:160px}.image_result p{margin:0;padding:0}.image_result p span a{display:none;color:#fff}.image_result p:hover span a{display:block;position:absolute;bottom:0;right:0;padding:4px;background-color:rgba(0,0,0,0.6);font-size:.7em}.torrent_result{border-left:10px solid #d3d3d3;padding-left:3px}.torrent_result p{margin:3px;font-size:.8em}.definition_result{border-left:10px solid #808080;padding-left:3px}.percentage{position:relative;width:300px}.percentage div{background:#444}table{width:100%}td{padding:0 4px}tr:hover{background:#ddd}#results{margin:auto;padding:0;width:50em;margin-bottom:20px}#sidebar{position:absolute;top:100px;right:10px;margin:0 2px 5px 5px;padding:0 2px 2px 2px;width:14em}#sidebar input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer}#sidebar input[type="submit"]{text-decoration:underline}#suggestions,#answers{margin-top:20px}#suggestions input,#answers input,#infoboxes input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer}#suggestions input[type="submit"],#answers input[type="submit"],#infoboxes input[type="submit"]{text-decoration:underline}#suggestions form,#answers form,#infoboxes form{display:inline}#infoboxes{position:absolute;top:220px;right:20px;margin:0 2px 5px 5px;padding:0 2px 2px;max-width:21em}#infoboxes .infobox{margin:10px 0 10px;border:1px solid #ddd;padding:5px;font-size:.8em}#infoboxes .infobox img{max-width:20em;max-heigt:12em;display:block;margin:5px;padding:5px}#infoboxes .infobox h2{margin:0}#infoboxes .infobox table{width:auto}#infoboxes .infobox table td{vertical-align:top}#infoboxes .infobox input{font-size:1em}#infoboxes .infobox br{clear:both}#search_url{margin-top:8px}#search_url input{border:1px solid #888;padding:4px;color:#444;width:14em;display:block;margin:4px;font-size:.8em}#preferences{top:10px;padding:0;border:0;background:url('../img/preference-icon.png') no-repeat;background-size:28px 28px;opacity:.8;width:28px;height:30px;display:block}#preferences *{display:none}#pagination{clear:both;width:40em}#apis{margin-top:8px;clear:both}@media screen and (max-width:50em){#results{margin:auto;padding:0;width:90%}.github{display:none}.checkbox_container{display:block;width:90%}.checkbox_container label{border-bottom:0}}@media screen and (max-width:75em){#infoboxes{position:inherit;max-width:inherit}#infoboxes .infobox{clear:both}#infoboxes .infobox img{float:left;max-width:10em}#categories{font-size:90%;clear:both}#categories .checkbox_container{margin-top:2px;margin:auto}.right{display:none;postion:fixed !important;top:100px;right:0}#sidebar{position:static;max-width:50em;margin:0 0 2px 0;padding:0;float:none;border:none;width:auto}#sidebar input{border:0}#apis{display:none}#search_url{display:none}.result{border-top:1px solid #e8e7e6;margin:7px 0 6px 0}}.favicon{float:left;margin-right:4px;margin-top:2px}.preferences_back{background:none repeat scroll 0 0 #3498db;border:0 none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px 4px;padding:4px 6px}.preferences_back a{color:#fff}
2−body,#container{padding:0;margin:0}
3−#container{width:100%;position:absolute;top:0}
4−.search{padding:0;margin:0}.search .checkbox_container label{font-size:.9em;border-bottom:2px solid #e8e7e6}
5−.search .checkbox_container label:hover{border-bottom:2px solid #3498db}
6−.search .checkbox_container input[type="checkbox"]:checked+label{border-bottom:2px solid #2980b9}
7−#search_wrapper{position:relative;width:50em;padding:10px}
8−.center #search_wrapper{margin-left:auto;margin-right:auto}
9−.q{background:none repeat scroll 0 0 #fff;border:1px solid #3498db;color:#222;font-size:16px;height:28px;margin:0;outline:medium none;padding:2px;padding-left:8px;padding-right:0 !important;width:100%;z-index:2}
10−#search_submit{position:absolute;top:13px;right:1px;padding:0;border:0;background:url('../img/search-icon.png') no-repeat;background-size:24px 24px;opacity:.8;width:24px;height:30px;font-size:0}
11−@media screen and (max-width:50em){#search_wrapper{width:90%;clear:both;overflow:hidden}}ul.autocompleter-choices{position:absolute;margin:0;padding:0;list-style:none;border:1px solid #3498db;border-left-color:#3498db;border-right-color:#3498db;border-bottom-color:#3498db;text-align:left;font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;z-index:50;background-color:#fff;color:#444}ul.autocompleter-choices li{position:relative;margin:-2px 0 0 0;padding:.2em 1.5em .2em 1em;display:block;float:none !important;cursor:pointer;font-weight:normal;white-space:nowrap;font-size:1em;line-height:1.5em}ul.autocompleter-choices li.autocompleter-selected{background-color:#444;color:#fff}ul.autocompleter-choices li.autocompleter-selected span.autocompleter-queried{color:#9fcfff}
12−ul.autocompleter-choices span.autocompleter-queried{display:inline;float:none;font-weight:bold;margin:0;padding:0}
13−.row{max-width:800px;margin:20px auto;text-align:justify}.row h1{font-size:3em;margin-top:50px}
14−.row p{padding:0 10px;max-width:700px}
15−.row h3,.row ul{margin:4px 8px}
16−.hmarg{margin:0 20px;border:1px solid #3498db;padding:4px 10px}
17−a:link.hmarg{color:#3498db}
18−a:visited.hmarg{color:#3498db}
19−a:active.hmarg{color:#3498db}
20−a:hover.hmarg{color:#3498db}
21−.top_margin{margin-top:60px}
22−.center{text-align:center}
23−h1{font-size:5em}
24−div.title{background:url('../img/searx.png') no-repeat;width:100%;background-position:center}div.title h1{visibility:hidden}
25−input[type="submit"]{padding:2px 6px;margin:2px 4px;display:inline-block;background:#3498db;color:#fff;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;border:0;cursor:pointer}
26−input[type="checkbox"]{visibility:hidden}
27−fieldset{margin:8px;border:1px solid #3498db}
28−#categories{margin:0 10px}
29−.checkbox_container{display:inline-block;position:relative;margin:0 3px;padding:0}.checkbox_container input{display:none}
30−.checkbox_container label,.engine_checkbox label{cursor:pointer;padding:4px 10px;margin:0;display:block;text-transform:capitalize;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
31−.checkbox_container input[type="checkbox"]:checked+label{background:#3498db;color:#fff}
32−.engine_checkbox{padding:4px}
33−label.allow{background:#e74c3c;padding:4px 8px;color:#fff;display:none}
34−label.deny{background:#2ecc71;padding:4px 8px;color:#444;display:inline}
35−.engine_checkbox input[type="checkbox"]:checked+label:nth-child(2)+label{display:none}
36−.engine_checkbox input[type="checkbox"]:checked+label.allow{display:inline}
37−a{text-decoration:none;color:#1a11be}a:visited{color:#8e44ad}
38−.result{margin:19px 0 18px 0;padding:0;clear:both}
39−.result_title{margin-bottom:0}.result_title a{color:#2980b9;font-weight:normal;font-size:1.1em}.result_title a:hover{text-decoration:underline}
40−.result_title a:visited{color:#8e44ad}
41−.cache_link{font-size:10px !important}
42−.result h3{font-size:1em;word-wrap:break-word;margin:5px 0 1px 0;padding:0}
43−.result .content{font-size:.8em;margin:0;padding:0;max-width:54em;word-wrap:break-word;line-height:1.24}
44−.result .url{font-size:.8em;margin:0 0 3px 0;padding:0;max-width:54em;word-wrap:break-word;color:#c0392b}
45−.result .published_date{font-size:.8em;color:#888;margin:5px 20px}
46−.engines{color:#888}
47−.small_font{font-size:.8em}
48−.small p{margin:2px 0}
49−.right{float:right}
50−.invisible{display:none}
51−.left{float:left}
52−.highlight{color:#094089}
53−.content .highlight{color:#000}
54−.image_result{float:left;margin:10px 10px;position:relative;height:160px}.image_result img{border:0;height:160px}
55−.image_result p{margin:0;padding:0}.image_result p span a{display:none;color:#fff}
56−.image_result p:hover span a{display:block;position:absolute;bottom:0;right:0;padding:4px;background-color:rgba(0,0,0,0.6);font-size:.7em}
57−.torrent_result{border-left:10px solid #d3d3d3;padding-left:3px}.torrent_result p{margin:3px;font-size:.8em}
58−.definition_result{border-left:10px solid #808080;padding-left:3px}
59−.percentage{position:relative;width:300px}.percentage div{background:#444}
60−table{width:100%}
61−td{padding:0 4px}
62−tr:hover{background:#ddd}
63−#results{margin:auto;padding:0;width:50em;margin-bottom:20px}
64−#sidebar{position:absolute;top:100px;right:10px;margin:0 2px 5px 5px;padding:0 2px 2px 2px;width:14em}#sidebar input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer}
65−#sidebar input[type="submit"]{text-decoration:underline}
66−#suggestions{margin-top:20px}#suggestions span{display:inline;margin:0 2px 2px 2px;padding:0}
67−#suggestions input{padding:0;margin:3px;font-size:.8em;display:inline-block;background:transparent;color:#444;cursor:pointer}
68−#suggestions input[type="submit"]{text-decoration:underline}
69−#suggestions form{display:inline}
70−#search_url{margin-top:8px}#search_url input{border:1px solid #888;padding:4px;color:#444;width:14em;display:block;margin:4px;font-size:.8em}
71−#preferences{top:10px;padding:0;border:0;background:url('../img/preference-icon.png') no-repeat;background-size:28px 28px;opacity:.8;width:28px;height:30px;display:block}#preferences *{display:none}
72−#pagination{clear:both;width:40em}
73−#apis{margin-top:8px;clear:both}
74−@media screen and (max-width:50em){#categories{font-size:90%;clear:both}#categories .checkbox_container{margin-top:2px;margin:auto} #results{margin:auto;padding:0;width:90%} .github{display:none} .checkbox_container{display:block;width:90%}.checkbox_container label{border-bottom:0}}@media screen and (max-width:70em){.right{display:none;postion:fixed !important;top:100px;right:0} #sidebar{position:static;max-width:50em;margin:0 0 2px 0;padding:0;float:none;border:none;width:auto}#sidebar input{border:0} #apis{display:none} #search_url{display:none} .result{border-top:1px solid #e8e7e6;margin:7px 0 6px 0}.result img{max-width:90%;width:auto;height:auto}}.favicon{float:left;margin-right:4px;margin-top:2px}
75−.preferences_back{background:none repeat scroll 0 0 #3498db;border:0 none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;cursor:pointer;display:inline-block;margin:2px 4px;padding:4px 6px}.preferences_back a{color:#fff}
searx/static/default/less/style.less+96−28
@@ -235,6 +235,17 @@ a {
235 max-width: 54em;
235 max-width: 54em;
236 word-wrap:break-word;
236 word-wrap:break-word;
237 line-height: 1.24;
237 line-height: 1.24;
238+
239+ img {
240+ float: left;
241+ margin-right: 5px;
242+ max-width: 200px;
243+ max-height: 100px;
244+ }
245+
246+ br.last {
247+ clear: both;
248+ }
238 }
249 }
239
250
240 .url {
251 .url {
@@ -384,33 +395,80 @@ tr {
384 }
395 }
385}
396}
386
397
387−#suggestions {
398+#suggestions, #answers {
388
399
389− margin-top: 20px;
400+ margin-top: 20px;
401+
402+}
403+
404+#suggestions, #answers, #infoboxes {
390
405
391− span {
392− display: inline;
393− margin: 0 2px 2px 2px;
394− padding: 0;
395− }
396 input {
406 input {
397 padding: 0;
407 padding: 0;
398 margin: 3px;
408 margin: 3px;
399 font-size: 0.8em;
409 font-size: 0.8em;
400 display: inline-block;
410 display: inline-block;
401− background: transparent;
411+ background: transparent;
402− color: @color-result-search-url-font;
412+ color: @color-result-search-url-font;
403 cursor: pointer;
413 cursor: pointer;
404 }
414 }
415+
405− input[type="submit"] {
416+ input[type="submit"] {
406 text-decoration: underline;
417 text-decoration: underline;
407− }
418+ }
408
419
409 form {
420 form {
410 display: inline;
421 display: inline;
411 }
422 }
412}
423}
413
424
425+
426+#infoboxes {
427+ position: absolute;
428+ top: 220px;
429+ right: 20px;
430+ margin: 0px 2px 5px 5px;
431+ padding: 0px 2px 2px;
432+ max-width: 21em;
433+
434+ .infobox {
435+ margin: 10px 0 10px;
436+ border: 1px solid #ddd;
437+ padding: 5px;
438+ font-size: 0.8em;
439+
440+ img {
441+ max-width: 20em;
442+ max-heigt: 12em;
443+ display: block;
444+ margin: 5px;
445+ padding: 5px;
446+ }
447+
448+ h2 {
449+ margin: 0;
450+ }
451+
452+ table {
453+ width: auto;
454+
455+ td {
456+ vertical-align: top;
457+ }
458+
459+ }
460+
461+ input {
462+ font-size: 1em;
463+ }
464+
465+ br {
466+ clear: both;
467+ }
468+
469+ }
470+}
471+
414#search_url {
472#search_url {
415 margin-top: 8px;
473 margin-top: 8px;
416
474
@@ -453,16 +511,6 @@ tr {
453
511
454@media screen and (max-width: @results-width) {
512@media screen and (max-width: @results-width) {
455
513
456− #categories {
457− font-size: 90%;
458− clear: both;
459−
460− .checkbox_container {
461− margin-top: 2px;
462− margin: auto;
463− }
464− }
465−
466 #results {
514 #results {
467 margin: auto;
515 margin: auto;
468 padding: 0;
516 padding: 0;
@@ -483,7 +531,33 @@ tr {
483 }
531 }
484}
532}
485
533
486−@media screen and (max-width: 70em) {
534+@media screen and (max-width: 75em) {
535+
536+ #infoboxes {
537+ position: inherit;
538+ max-width: inherit;
539+
540+ .infobox {
541+ clear:both;
542+
543+ img {
544+ float: left;
545+ max-width: 10em;
546+ }
547+ }
548+
549+ }
550+
551+ #categories {
552+ font-size: 90%;
553+ clear: both;
554+
555+ .checkbox_container {
556+ margin-top: 2px;
557+ margin: auto;
558+ }
559+ }
560+
487 .right {
561 .right {
488 display: none;
562 display: none;
489 postion: fixed !important;
563 postion: fixed !important;
@@ -515,12 +589,6 @@ tr {
515 .result {
589 .result {
516 border-top: 1px solid @color-result-top-border;
590 border-top: 1px solid @color-result-top-border;
517 margin: 7px 0 6px 0;
591 margin: 7px 0 6px 0;
518−
519− img {
520− max-width: 90%;
521− width: auto;
522− height: auto
523− }
524 }
592 }
525}
593}
526
594
searx/templates/default/result_templates/default.html+3−1
@@ -8,6 +8,8 @@
8 <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
8 <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
9 <p class="url">{{ result.pretty_url }} <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}">cached</a></p>
9 <p class="url">{{ result.pretty_url }} <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}">cached</a></p>
10 {% if result.publishedDate %}<p class="published_date">{{ result.publishedDate }}</p>{% endif %}
10 {% if result.publishedDate %}<p class="published_date">{{ result.publishedDate }}</p>{% endif %}
11+ <p class="content">
12+ {% if result.img_src %}<img src="{{ result.img_src|safe }}" class="image" />{% endif %}
11− <p class="content">{% if result.content %}{{ result.content|safe }}<br />{% endif %}</p>
13+ {% if result.content %}{{ result.content|safe }}<br class="last"/>{% endif %}</p>
12 </div>
14 </div>
13</div>
15</div>
searx/templates/default/results.html+16
@@ -30,6 +30,14 @@
30 </div>
30 </div>
31 </div>
31 </div>
32
32
33+ {% if answers %}
34+ <div id="answers"><span>{{ _('Answers') }}</span>
35+ {% for answer in answers %}
36+ <span>{{ answer }}</span>
37+ {% endfor %}
38+ </div>
39+ {% endif %}
40+
33 {% if suggestions %}
41 {% if suggestions %}
34 <div id="suggestions"><span>{{ _('Suggestions') }}</span>
42 <div id="suggestions"><span>{{ _('Suggestions') }}</span>
35 {% for suggestion in suggestions %}
43 {% for suggestion in suggestions %}
@@ -41,6 +49,14 @@
41 </div>
49 </div>
42 {% endif %}
50 {% endif %}
43
51
52+ {% if infoboxes %}
53+ <div id="infoboxes">
54+ {% for infobox in infoboxes %}
55+ {% include 'default/infobox.html' %}
56+ {% endfor %}
57+ </div>
58+ {% endif %}
59+
44 {% for result in results %}
60 {% for result in results %}
45 {% if result['template'] %}
61 {% if result['template'] %}
46 {% include 'default/result_templates/'+result['template'] %}
62 {% include 'default/result_templates/'+result['template'] %}
searx/webapp.py+3−1
@@ -198,7 +198,7 @@ def index():
198 'index.html',
198 'index.html',
199 )
199 )
200
200
201− search.results, search.suggestions = search.search(request)
201+ search.results, search.suggestions, search.answers, search.infoboxes = search.search(request)
202
202
203 for result in search.results:
203 for result in search.results:
204
204
@@ -291,6 +291,8 @@ def index():
291 pageno=search.pageno,
291 pageno=search.pageno,
292 base_url=get_base_url(),
292 base_url=get_base_url(),
293 suggestions=search.suggestions,
293 suggestions=search.suggestions,
294+ answers=search.answers,
295+ infoboxes=search.infoboxes,
294 theme=get_current_theme_name()
296 theme=get_current_theme_name()
295 )
297 )
296
298