on master help sign in
[enh] add infoboxes and answers
commit6bfd5663539052a64c984f5bdb7135d0d652c923 authorDalf <alex@al-f.net> dateSun 28 Sep 2014, 16:51 (12 years ago) parente39d9fe
10 files changed+525−130
searx/engines/currency_convert.py+5−6 searx/engines/duckduckgo_definitions.py+114−7 searx/engines/wikidata.py+193 searx/search.py+93−11 searx/settings.yml+1−1 searx/static/default/css/style.css+1−75 searx/static/default/less/style.less+96−28 searx/templates/default/result_templates/default.html+3−1 searx/templates/default/results.html+16 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+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():
3838
3939
4040# 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):
4242
4343 # creating a callback wrapper for the search engine results
4444 def process_callback(response, **kwargs):
4545 cb_res = []
4646 response.search_params = params
4747
48− # update stats with current page-load-time
49− engines[engine_name].stats['page_load_time'] += \
50− (datetime.now() - params['started']).total_seconds()
51−
48+ # callback
5249 try:
5350 search_results = callback(response)
5451 except Exception, e:
@@ -61,6 +58,7 @@ def make_callback(engine_name, results, suggestions, callback, params):
6158 engine_name, str(e))
6259 return
6360
61+ # add results
6462 for result in search_results:
6563 result['engine'] = engine_name
6664
@@ -70,21 +68,38 @@ def make_callback(engine_name, results, suggestions, callback, params):
7068 suggestions.add(result['suggestion'])
7169 continue
7270
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+
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
93+
8094# return the meaningful length of the content for a result
81−def content_result_len(result):
82− if isinstance(result.get('content'), basestring):
83− content = re.sub('[,;:!?\./\\\\ ()-_]', '', result.get('content'))
95+def content_result_len(content):
96+ if isinstance(content, basestring):
97+ content = re.sub('[,;:!?\./\\\\ ()-_]', '', content)
8498 return len(content)
8599 else:
86100 return 0
87101
102+
88103# score results and remove duplications
89104def score_results(results):
90105 # calculate scoring parameters
@@ -138,7 +153,7 @@ def score_results(results):
138153 # merge duplicates together
139154 if duplicated:
140155 # 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', '')):
142157 duplicated['content'] = res['content']
143158
144159 # increase result-score
@@ -197,6 +212,64 @@ def score_results(results):
197212 return gresults
198213
199214
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+
200273class Search(object):
201274
202275 """Search information container"""
@@ -219,6 +292,8 @@ class Search(object):
219292
220293 self.results = []
221294 self.suggestions = []
295+ self.answers = []
296+ self.infoboxes = []
222297 self.request_data = {}
223298
224299 # set specific language if set
@@ -350,6 +425,8 @@ class Search(object):
350425 requests = []
351426 results = {}
352427 suggestions = set()
428+ answers = set()
429+ infoboxes = []
353430
354431 # increase number of searches
355432 number_of_searches += 1
@@ -394,6 +471,8 @@ class Search(object):
394471 selected_engine['name'],
395472 results,
396473 suggestions,
474+ answers,
475+ infoboxes,
397476 engine.response,
398477 request_params
399478 )
@@ -431,11 +510,14 @@ class Search(object):
431510 # score results and remove duplications
432511 results = score_results(results)
433512
513+ # merge infoboxes according to their ids
514+ infoboxes = merge_infoboxes(infoboxes)
515+
434516 # update engine stats, using calculated score
435517 for result in results:
436518 for res_engine in result['engines']:
437519 engines[result['engine']]\
438520 .stats['score_count'] += result['score']
439521
440− # return results and suggestions
441− return results, suggestions
522+ # return results, suggestions, answers and infoboxes
523+ return results, suggestions, answers, infoboxes
searx/settings.yml+1−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
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/result_templates/default.html+3−1
@@ -8,6 +8,8 @@
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">
12+ {% if result.img_src %}<img src="{{ result.img_src|safe }}" class="image" />{% endif %}
13+ {% if result.content %}{{ result.content|safe }}<br class="last"/>{% endif %}</p>
1214 </div>
1315</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/webapp.py+3−1
@@ -198,7 +198,7 @@ def index():
198198 'index.html',
199199 )
200200
201− search.results, search.suggestions = search.search(request)
201+ search.results, search.suggestions, search.answers, search.infoboxes = search.search(request)
202202
203203 for result in search.results:
204204
@@ -291,6 +291,8 @@ def index():
291291 pageno=search.pageno,
292292 base_url=get_base_url(),
293293 suggestions=search.suggestions,
294+ answers=search.answers,
295+ infoboxes=search.infoboxes,
294296 theme=get_current_theme_name()
295297 )
296298