on master help sign in
Merge pull request #104 from dalf/master

[enh] add infoboxes and answers, [fix] when two results are merged, really use the content with more text

commit67b69619ba8fbf17820f303093e883055531efff authorAdam Tauber <asciimoo@gmail.com> dateSat 4 Oct 2014, 22:46 (12 years ago) parentfce6fb263a0328
12 files changed+618−130
searx/engines/currency_convert.py+5−6 searx/engines/duckduckgo_definitions.py+114−7 searx/engines/wikidata.py+221 searx/search.py+103−10 searx/settings.yml+5−1 searx/static/default/css/style.css+1−75 searx/static/default/less/style.less+96−28 searx/templates/default/infobox.html+44 searx/templates/default/result_templates/default.html+1−1 searx/templates/default/results.html+16 searx/tests/test_webapp.py+9−1 searx/webapp.py+3−1
searx/engines/currency_convert.py+5−6
@@ -38,16 +38,14 @@ def response(resp):
3838 except:
3939 return results
4040
41− title = '{0} {1} in {2} is {3}'.format(
41+ answer = '{0} {1} = {2} {3} (1 {1} = {4} {3})'.format(
4242 resp.search_params['ammount'],
4343 resp.search_params['from'],
44+ resp.search_params['ammount'] * conversion_rate,
4445 resp.search_params['to'],
45− resp.search_params['ammount'] * conversion_rate
46+ conversion_rate
4647 )
4748
48− content = '1 {0} is {1} {2}'.format(resp.search_params['from'],
49− conversion_rate,
50− resp.search_params['to'])
5149 now_date = datetime.now().strftime('%Y%m%d')
5250 url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html' # noqa
5351 url = url.format(
@@ -56,6 +54,7 @@ def response(resp):
5654 resp.search_params['from'].lower(),
5755 resp.search_params['to'].lower()
5856 )
59− results.append({'title': title, 'content': content, 'url': url})
57+
58+ results.append({'answer' : answer, 'url': url})
6059
6160 return results
searx/engines/duckduckgo_definitions.py+114−7
@@ -1,10 +1,25 @@
11import json
22from urllib import urlencode
3+from lxml import html
4+from searx.engines.xpath import extract_text
35
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'
57
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)
620
721def request(query, params):
22+ # TODO add kl={locale}
823 params['url'] = url.format(query=urlencode({'q': query}))
924 return params
1025
@@ -12,12 +27,104 @@ def request(query, params):
1227def response(resp):
1328 search_res = json.loads(resp.text)
1429 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
1544 if 'Definition' in search_res:
16− if search_res.get('AbstractURL'):
17− res = {'title': search_res.get('Heading', ''),
18− 'content': search_res.get('Definition', ''),
19− 'url': search_res.get('AbstractURL', ''),
20− 'class': 'definition_result'}
21− results.append(res)
45+ content = content + search_res.get('Definition', '')
46+
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'
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+ })
22129
23130 return results
searx/engines/wikidata.pynew file+221
@@ -0,0 +1,221 @@
1+import json
2+from requests import get
3+from urllib import urlencode
4+from datetime import datetime
5+
6+resultCount=2
7+urlSearch = 'https://www.wikidata.org/w/api.php?action=query&list=search&format=json&srnamespace=0&srprop=sectiontitle&{query}'
8+urlDetail = 'https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=labels%7Cinfo%7Csitelinks%7Csitelinks%2Furls%7Cdescriptions%7Cclaims&{query}'
9+urlMap = 'https://www.openstreetmap.org/?lat={latitude}&lon={longitude}&zoom={zoom}&layers=M'
10+
11+def request(query, params):
12+ params['url'] = urlSearch.format(query=urlencode({'srsearch': query, 'srlimit': resultCount}))
13+ print params['url']
14+ return params
15+
16+
17+def response(resp):
18+ results = []
19+ search_res = json.loads(resp.text)
20+
21+ wikidata_ids = set()
22+ for r in search_res.get('query', {}).get('search', {}):
23+ wikidata_ids.add(r.get('title', ''))
24+
25+ language = resp.search_params['language'].split('_')[0]
26+ if language == 'all':
27+ language = 'en'
28+ url = urlDetail.format(query=urlencode({'ids': '|'.join(wikidata_ids), 'languages': language + '|en'}))
29+
30+ before = datetime.now()
31+ htmlresponse = get(url)
32+ print datetime.now() - before
33+ jsonresponse = json.loads(htmlresponse.content)
34+ for wikidata_id in wikidata_ids:
35+ results = results + getDetail(jsonresponse, wikidata_id, language)
36+
37+ return results
38+
39+def getDetail(jsonresponse, wikidata_id, language):
40+ result = jsonresponse.get('entities', {}).get(wikidata_id, {})
41+
42+ title = result.get('labels', {}).get(language, {}).get('value', None)
43+ if title == None:
44+ title = result.get('labels', {}).get('en', {}).get('value', wikidata_id)
45+ results = []
46+ urls = []
47+ attributes = []
48+
49+ description = result.get('descriptions', {}).get(language, {}).get('value', '')
50+ if description == '':
51+ description = result.get('descriptions', {}).get('en', {}).get('value', '')
52+
53+ claims = result.get('claims', {})
54+ official_website = get_string(claims, 'P856', None)
55+ if official_website != None:
56+ urls.append({ 'title' : 'Official site', 'url': official_website })
57+ results.append({ 'title': title, 'url' : official_website })
58+
59+ if language != 'en':
60+ add_url(urls, 'Wikipedia (' + language + ')', get_wikilink(result, language + 'wiki'))
61+ wikipedia_en_link = get_wikilink(result, 'enwiki')
62+ add_url(urls, 'Wikipedia (en)', wikipedia_en_link)
63+
64+ if language != 'en':
65+ add_url(urls, 'Wiki voyage (' + language + ')', get_wikilink(result, language + 'wikivoyage'))
66+ add_url(urls, 'Wiki voyage (en)', get_wikilink(result, 'enwikivoyage'))
67+
68+ if language != 'en':
69+ add_url(urls, 'Wikiquote (' + language + ')', get_wikilink(result, language + 'wikiquote'))
70+ add_url(urls, 'Wikiquote (en)', get_wikilink(result, 'enwikiquote'))
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+ musicbrainz_work_id = get_string(claims, 'P435')
79+ if musicbrainz_work_id != None:
80+ add_url(urls, 'MusicBrainz', 'http://musicbrainz.org/work/' + musicbrainz_work_id)
81+
82+ musicbrainz_artist_id = get_string(claims, 'P434')
83+ if musicbrainz_artist_id != None:
84+ add_url(urls, 'MusicBrainz', 'http://musicbrainz.org/artist/' + musicbrainz_artist_id)
85+
86+ musicbrainz_release_group_id = get_string(claims, 'P436')
87+ if musicbrainz_release_group_id != None:
88+ add_url(urls, 'MusicBrainz', 'http://musicbrainz.org/release-group/' + musicbrainz_release_group_id)
89+
90+ musicbrainz_label_id = get_string(claims, 'P966')
91+ if musicbrainz_label_id != None:
92+ add_url(urls, 'MusicBrainz', 'http://musicbrainz.org/label/' + musicbrainz_label_id)
93+
94+ # musicbrainz_area_id = get_string(claims, 'P982')
95+ # P1407 MusicBrainz series ID
96+ # P1004 MusicBrainz place ID
97+ # P1330 MusicBrainz instrument ID
98+ # P1407 MusicBrainz series ID
99+
100+ postal_code = get_string(claims, 'P281', None)
101+ if postal_code != None:
102+ attributes.append({'label' : 'Postal code(s)', 'value' : postal_code})
103+
104+ date_of_birth = get_time(claims, 'P569', None)
105+ if date_of_birth != None:
106+ attributes.append({'label' : 'Date of birth', 'value' : date_of_birth})
107+
108+ date_of_death = get_time(claims, 'P570', None)
109+ if date_of_death != None:
110+ attributes.append({'label' : 'Date of death', 'value' : date_of_death})
111+
112+
113+ results.append({
114+ 'infobox' : title,
115+ 'id' : wikipedia_en_link,
116+ 'content' : description,
117+ 'attributes' : attributes,
118+ 'urls' : urls
119+ })
120+
121+ return results
122+
123+
124+def add_url(urls, title, url):
125+ if url != None:
126+ urls.append({'title' : title, 'url' : url})
127+
128+
129+def get_mainsnak(claims, propertyName):
130+ propValue = claims.get(propertyName, {})
131+ if len(propValue) == 0:
132+ return None
133+
134+ propValue = propValue[0].get('mainsnak', None)
135+ return propValue
136+
137+
138+def get_string(claims, propertyName, defaultValue=None):
139+ propValue = claims.get(propertyName, {})
140+ if len(propValue) == 0:
141+ return defaultValue
142+
143+ result = []
144+ for e in propValue:
145+ mainsnak = e.get('mainsnak', {})
146+
147+ datatype = mainsnak.get('datatype', '')
148+ datavalue = mainsnak.get('datavalue', {})
149+ if datavalue != None:
150+ result.append(datavalue.get('value', ''))
151+
152+ if len(result) == 0:
153+ return defaultValue
154+ else:
155+ return ', '.join(result)
156+
157+
158+def get_time(claims, propertyName, defaultValue=None):
159+ propValue = claims.get(propertyName, {})
160+ if len(propValue) == 0:
161+ return defaultValue
162+
163+ result = []
164+ for e in propValue:
165+ mainsnak = e.get('mainsnak', {})
166+
167+ datatype = mainsnak.get('datatype', '')
168+ datavalue = mainsnak.get('datavalue', {})
169+ if datavalue != None:
170+ value = datavalue.get('value', '')
171+ result.append(value.get('time', ''))
172+
173+ if len(result) == 0:
174+ return defaultValue
175+ else:
176+ return ', '.join(result)
177+
178+
179+def get_geolink(claims, propertyName, defaultValue=''):
180+ mainsnak = get_mainsnak(claims, propertyName)
181+
182+ if mainsnak == None:
183+ return defaultValue
184+
185+ datatype = mainsnak.get('datatype', '')
186+ datavalue = mainsnak.get('datavalue', {})
187+
188+ if datatype != 'globe-coordinate':
189+ return defaultValue
190+
191+ value = datavalue.get('value', {})
192+
193+ precision = value.get('precision', 0.0002)
194+
195+ # there is no zoom information, deduce from precision (error prone)
196+ # samples :
197+ # 13 --> 5
198+ # 1 --> 6
199+ # 0.016666666666667 --> 9
200+ # 0.00027777777777778 --> 19
201+ # wolframalpha : quadratic fit { {13, 5}, {1, 6}, {0.0166666, 9}, {0.0002777777,19}}
202+ # 14.1186-8.8322 x+0.625447 x^2
203+ if precision < 0.0003:
204+ zoom = 19
205+ else:
206+ zoom = int(15 - precision*8.8322 + precision*precision*0.625447)
207+
208+ url = urlMap.replace('{latitude}', str(value.get('latitude',0))).replace('{longitude}', str(value.get('longitude',0))).replace('{zoom}', str(zoom))
209+
210+ return url
211+
212+
213+def get_wikilink(result, wikiid):
214+ url = result.get('sitelinks', {}).get(wikiid, {}).get('url', None)
215+ if url == None:
216+ return url
217+ elif url.startswith('http://'):
218+ url = url.replace('http://', 'https://')
219+ elif url.startswith('//'):
220+ url = 'https:' + url
221+ return url
searx/search.py+103−10
@@ -16,6 +16,7 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
1616'''
1717
1818import grequests
19+import re
1920from itertools import izip_longest, chain
2021from datetime import datetime
2122from operator import itemgetter
@@ -38,17 +39,14 @@ def default_request_params():
3839
3940
4041# create a callback wrapper for the search engine results
41−def make_callback(engine_name, results, suggestions, callback, params):
42+def make_callback(engine_name, results, suggestions, answers, infoboxes, callback, params):
4243
4344 # creating a callback wrapper for the search engine results
4445 def process_callback(response, **kwargs):
4546 cb_res = []
4647 response.search_params = params
4748
48− # update stats with current page-load-time
49− engines[engine_name].stats['page_load_time'] += \
50− (datetime.now() - params['started']).total_seconds()
51−
49+ # callback
5250 try:
5351 search_results = callback(response)
5452 except Exception, e:
@@ -61,6 +59,7 @@ def make_callback(engine_name, results, suggestions, callback, params):
6159 engine_name, str(e))
6260 return
6361
62+ # add results
6463 for result in search_results:
6564 result['engine'] = engine_name
6665
@@ -70,14 +69,37 @@ def make_callback(engine_name, results, suggestions, callback, params):
7069 suggestions.add(result['suggestion'])
7170 continue
7271
72+ # if it is an answer, add it to list of answers
73+ if 'answer' in result:
74+ answers.add(result['answer'])
75+ continue
76+
77+ # if it is an infobox, add it to list of infoboxes
78+ if 'infobox' in result:
79+ infoboxes.append(result)
80+ continue
81+
7382 # append result
7483 cb_res.append(result)
7584
7685 results[engine_name] = cb_res
7786
87+ # update stats with current page-load-time
88+ engines[engine_name].stats['page_load_time'] += \
89+ (datetime.now() - params['started']).total_seconds()
90+
7891 return process_callback
7992
8093
94+# return the meaningful length of the content for a result
95+def content_result_len(content):
96+ if isinstance(content, basestring):
97+ content = re.sub('[,;:!?\./\\\\ ()-_]', '', content)
98+ return len(content)
99+ else:
100+ return 0
101+
102+
81103# score results and remove duplications
82104def score_results(results):
83105 # calculate scoring parameters
@@ -99,8 +121,13 @@ def score_results(results):
99121 res['host'] = res['host'].replace('www.', '', 1)
100122
101123 res['engines'] = [res['engine']]
124+
102125 weight = 1.0
103126
127+ # strip multiple spaces and cariage returns from content
128+ if 'content' in res:
129+ res['content'] = re.sub(' +', ' ', res['content'].strip().replace('\n', ''))
130+
104131 # get weight of this engine if possible
105132 if hasattr(engines[res['engine']], 'weight'):
106133 weight = float(engines[res['engine']].weight)
@@ -108,9 +135,8 @@ def score_results(results):
108135 # calculate score for that engine
109136 score = int((flat_len - i) / engines_len) * weight + 1
110137
111− duplicated = False
112−
113138 # check for duplicates
139+ duplicated = False
114140 for new_res in results:
115141 # remove / from the end of the url if required
116142 p1 = res['parsed_url'].path[:-1] if res['parsed_url'].path.endswith('/') else res['parsed_url'].path # noqa
@@ -127,7 +153,7 @@ def score_results(results):
127153 # merge duplicates together
128154 if duplicated:
129155 # using content with more text
130− if res.get('content') > duplicated.get('content'):
156+ if content_result_len(res.get('content', '')) > content_result_len(duplicated.get('content', '')):
131157 duplicated['content'] = res['content']
132158
133159 # increase result-score
@@ -186,6 +212,64 @@ def score_results(results):
186212 return gresults
187213
188214
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+
189273class Search(object):
190274
191275 """Search information container"""
@@ -208,6 +292,8 @@ class Search(object):
208292
209293 self.results = []
210294 self.suggestions = []
295+ self.answers = []
296+ self.infoboxes = []
211297 self.request_data = {}
212298
213299 # set specific language if set
@@ -293,6 +379,8 @@ class Search(object):
293379 requests = []
294380 results = {}
295381 suggestions = set()
382+ answers = set()
383+ infoboxes = []
296384
297385 # increase number of searches
298386 number_of_searches += 1
@@ -337,6 +425,8 @@ class Search(object):
337425 selected_engine['name'],
338426 results,
339427 suggestions,
428+ answers,
429+ infoboxes,
340430 engine.response,
341431 request_params
342432 )
@@ -374,11 +464,14 @@ class Search(object):
374464 # score results and remove duplications
375465 results = score_results(results)
376466
467+ # merge infoboxes according to their ids
468+ infoboxes = merge_infoboxes(infoboxes)
469+
377470 # update engine stats, using calculated score
378471 for result in results:
379472 for res_engine in result['engines']:
380473 engines[result['engine']]\
381474 .stats['score_count'] += result['score']
382475
383− # return results and suggestions
384− return results, suggestions
476+ # return results, suggestions, answers and infoboxes
477+ return results, suggestions, answers, infoboxes
searx/settings.yml+5−1
@@ -1,7 +1,7 @@
11server:
22 port : 8888
33 secret_key : "ultrasecretkey" # change this!
4− debug : False # Debug mode, only for development
4+ debug : True # Debug mode, only for development
55 request_timeout : 2.0 # seconds
66 base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/"
77 themes_path : "" # Custom ui themes path
@@ -44,6 +44,10 @@ engines:
4444 engine : duckduckgo_definitions
4545 shortcut : ddd
4646
47+ - name : wikidata
48+ engine : wikidata
49+ shortcut : wd
50+
4751 - name : duckduckgo
4852 engine : duckduckgo
4953 shortcut : ddg
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}
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}
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}
searx/static/default/less/style.less+96−28
@@ -235,6 +235,17 @@ a {
235235 max-width: 54em;
236236 word-wrap:break-word;
237237 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+ }
238249 }
239250
240251 .url {
@@ -384,33 +395,80 @@ tr {
384395 }
385396}
386397
387−#suggestions {
398+#suggestions, #answers {
388399
389− margin-top: 20px;
400+ margin-top: 20px;
401+
402+}
403+
404+#suggestions, #answers, #infoboxes {
390405
391− span {
392− display: inline;
393− margin: 0 2px 2px 2px;
394− padding: 0;
395− }
396406 input {
397407 padding: 0;
398408 margin: 3px;
399409 font-size: 0.8em;
400410 display: inline-block;
401− background: transparent;
402− color: @color-result-search-url-font;
411+ background: transparent;
412+ color: @color-result-search-url-font;
403413 cursor: pointer;
404414 }
405− input[type="submit"] {
415+
416+ input[type="submit"] {
406417 text-decoration: underline;
407− }
418+ }
408419
409420 form {
410421 display: inline;
411422 }
412423}
413424
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+
414472#search_url {
415473 margin-top: 8px;
416474
@@ -453,16 +511,6 @@ tr {
453511
454512@media screen and (max-width: @results-width) {
455513
456− #categories {
457− font-size: 90%;
458− clear: both;
459−
460− .checkbox_container {
461− margin-top: 2px;
462− margin: auto;
463− }
464− }
465−
466514 #results {
467515 margin: auto;
468516 padding: 0;
@@ -483,7 +531,33 @@ tr {
483531 }
484532}
485533
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+
487561 .right {
488562 display: none;
489563 postion: fixed !important;
@@ -515,12 +589,6 @@ tr {
515589 .result {
516590 border-top: 1px solid @color-result-top-border;
517591 margin: 7px 0 6px 0;
518−
519− img {
520− max-width: 90%;
521− width: auto;
522− height: auto
523− }
524592 }
525593}
526594
searx/templates/default/infobox.htmlnew file+44
@@ -0,0 +1,44 @@
1+<div class="infobox">
2+ <h2>{{ infobox.infobox }}</h2>
3+ {% if infobox.img_src %}<img src="{{ infobox.img_src }}" />{% endif %}
4+ <p>{{ infobox.entity }}</p>
5+ <p>{{ infobox.content }}</p>
6+ {% if infobox.attributes %}
7+ <div class="attributes">
8+ <table>
9+ {% for attribute in infobox.attributes %}
10+ <tr><td>{{ attribute.label }}</td><td>{{ attribute.value }}</td></tr>
11+ {% endfor %}
12+ </table>
13+ </div>
14+ {% endif %}
15+
16+ {% if infobox.urls %}
17+ <div class="urls">
18+ <ul>
19+ {% for url in infobox.urls %}
20+ <li class="url"><a href="{{ url.url }}">{{ url.title }}</a></li>
21+ {% endfor %}
22+ </ul>
23+ </div>
24+ {% endif %}
25+
26+ {% if infobox.relatedTopics %}
27+ <div class="relatedTopics">
28+ {% for topic in infobox.relatedTopics %}
29+ <div>
30+ <h3>{{ topic.name }}</h3>
31+ {% for suggestion in topic.suggestions %}
32+ <form method="{{ method or 'POST' }}" action="{{ url_for('index') }}">
33+ <input type="hidden" name="q" value="{{ suggestion }}">
34+ <input type="submit" value="{{ suggestion }}" />
35+ </form>
36+ {% endfor %}
37+ </div>
38+ {% endfor %}
39+ </div>
40+ {% endif %}
41+
42+ <br />
43+
44+</div>
searx/templates/default/result_templates/default.html+1−1
@@ -8,6 +8,6 @@
88 <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
99 <p class="url">{{ result.pretty_url }} <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}">cached</a></p>
1010 {% if result.publishedDate %}<p class="published_date">{{ result.publishedDate }}</p>{% endif %}
11− <p class="content">{% if result.content %}{{ result.content|safe }}<br />{% endif %}</p>
11+ <p class="content">{% if result.img_src %}<img src="{{ result.img_src|safe }}" class="image" />{% endif %}{% if result.content %}{{ result.content|safe }}<br class="last"/>{% endif %}</p>
1212 </div>
1313</div>
searx/templates/default/results.html+16
@@ -30,6 +30,14 @@
3030 </div>
3131 </div>
3232
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+
3341 {% if suggestions %}
3442 <div id="suggestions"><span>{{ _('Suggestions') }}</span>
3543 {% for suggestion in suggestions %}
@@ -41,6 +49,14 @@
4149 </div>
4250 {% endif %}
4351
52+ {% if infoboxes %}
53+ <div id="infoboxes">
54+ {% for infobox in infoboxes %}
55+ {% include 'default/infobox.html' %}
56+ {% endfor %}
57+ </div>
58+ {% endif %}
59+
4460 {% for result in results %}
4561 {% if result['template'] %}
4662 {% include 'default/result_templates/'+result['template'] %}
searx/tests/test_webapp.py+9−1
@@ -43,6 +43,8 @@ class ViewsTestCase(SearxTestCase):
4343 def test_index_html(self, search):
4444 search.return_value = (
4545 self.test_results,
46+ set(),
47+ set(),
4648 set()
4749 )
4850 result = self.app.post('/', data={'q': 'test'})
@@ -51,7 +53,7 @@ class ViewsTestCase(SearxTestCase):
5153 result.data
5254 )
5355 self.assertIn(
54− '<p class="content">first <span class="highlight">test</span> content<br /></p>', # noqa
56+ '<p class="content">first <span class="highlight">test</span> content<br class="last"/></p>', # noqa
5557 result.data
5658 )
5759
@@ -59,6 +61,8 @@ class ViewsTestCase(SearxTestCase):
5961 def test_index_json(self, search):
6062 search.return_value = (
6163 self.test_results,
64+ set(),
65+ set(),
6266 set()
6367 )
6468 result = self.app.post('/', data={'q': 'test', 'format': 'json'})
@@ -75,6 +79,8 @@ class ViewsTestCase(SearxTestCase):
7579 def test_index_csv(self, search):
7680 search.return_value = (
7781 self.test_results,
82+ set(),
83+ set(),
7884 set()
7985 )
8086 result = self.app.post('/', data={'q': 'test', 'format': 'csv'})
@@ -90,6 +96,8 @@ class ViewsTestCase(SearxTestCase):
9096 def test_index_rss(self, search):
9197 search.return_value = (
9298 self.test_results,
99+ set(),
100+ set(),
93101 set()
94102 )
95103 result = self.app.post('/', data={'q': 'test', 'format': 'rss'})
searx/webapp.py+3−1
@@ -199,7 +199,7 @@ def index():
199199 'index.html',
200200 )
201201
202− search.results, search.suggestions = search.search(request)
202+ search.results, search.suggestions, search.answers, search.infoboxes = search.search(request)
203203
204204 for result in search.results:
205205
@@ -292,6 +292,8 @@ def index():
292292 pageno=search.pageno,
293293 base_url=get_base_url(),
294294 suggestions=search.suggestions,
295+ answers=search.answers,
296+ infoboxes=search.infoboxes,
295297 theme=get_current_theme_name()
296298 )
297299