Sfoglia il codice sorgente

[enh] add infoboxes and answers

Dalf 10 anni fa
parent
commit
6bfd566353

+ 5
- 6
searx/engines/currency_convert.py Vedi File

@@ -38,16 +38,14 @@ def response(resp):
38 38
     except:
39 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 42
         resp.search_params['ammount'],
43 43
         resp.search_params['from'],
44
+        resp.search_params['ammount'] * conversion_rate,
44 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 49
     now_date = datetime.now().strftime('%Y%m%d')
52 50
     url = 'http://finance.yahoo.com/currency/converter-results/{0}/{1}-{2}-to-{3}.html'  # noqa
53 51
     url = url.format(
@@ -56,6 +54,7 @@ def response(resp):
56 54
         resp.search_params['from'].lower(),
57 55
         resp.search_params['to'].lower()
58 56
     )
59
-    results.append({'title': title, 'content': content, 'url': url})
57
+
58
+    results.append({'answer' : answer, 'url': url})
60 59
 
61 60
     return results

+ 114
- 7
searx/engines/duckduckgo_definitions.py Vedi File

@@ -1,10 +1,25 @@
1 1
 import json
2 2
 from 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
 
7 21
 def request(query, params):
22
+    # TODO add kl={locale}
8 23
     params['url'] = url.format(query=urlencode({'q': query}))
9 24
     return params
10 25
 
@@ -12,12 +27,104 @@ def request(query, params):
12 27
 def response(resp):
13 28
     search_res = json.loads(resp.text)
14 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 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', ''),
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
+               })
22 129
 
23 130
     return results

+ 193
- 0
searx/engines/wikidata.py Vedi File

@@ -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

+ 93
- 11
searx/search.py Vedi File

@@ -38,17 +38,14 @@ def default_request_params():
38 38
 
39 39
 
40 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 43
     # creating a callback wrapper for the search engine results
44 44
     def process_callback(response, **kwargs):
45 45
         cb_res = []
46 46
         response.search_params = params
47 47
 
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
52 49
         try:
53 50
             search_results = callback(response)
54 51
         except Exception, e:
@@ -61,6 +58,7 @@ def make_callback(engine_name, results, suggestions, callback, params):
61 58
                 engine_name, str(e))
62 59
             return
63 60
 
61
+        # add results
64 62
         for result in search_results:
65 63
             result['engine'] = engine_name
66 64
 
@@ -70,21 +68,38 @@ def make_callback(engine_name, results, suggestions, callback, params):
70 68
                 suggestions.add(result['suggestion'])
71 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 82
             # append result
74 83
             cb_res.append(result)
75 84
 
76 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 91
     return process_callback
79 92
 
93
+
80 94
 # 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)
84 98
         return len(content) 
85 99
     else:
86 100
         return 0
87 101
 
102
+
88 103
 # score results and remove duplications
89 104
 def score_results(results):
90 105
     # calculate scoring parameters
@@ -138,7 +153,7 @@ def score_results(results):
138 153
         # merge duplicates together
139 154
         if duplicated:
140 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 157
                 duplicated['content'] = res['content']
143 158
 
144 159
             # increase result-score
@@ -197,6 +212,64 @@ def score_results(results):
197 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
+
200 273
 class Search(object):
201 274
 
202 275
     """Search information container"""
@@ -219,6 +292,8 @@ class Search(object):
219 292
 
220 293
         self.results = []
221 294
         self.suggestions = []
295
+        self.answers = []
296
+        self.infoboxes = []
222 297
         self.request_data = {}
223 298
 
224 299
         # set specific language if set
@@ -350,6 +425,8 @@ class Search(object):
350 425
         requests = []
351 426
         results = {}
352 427
         suggestions = set()
428
+        answers = set()
429
+        infoboxes = []
353 430
 
354 431
         # increase number of searches
355 432
         number_of_searches += 1
@@ -394,6 +471,8 @@ class Search(object):
394 471
                 selected_engine['name'],
395 472
                 results,
396 473
                 suggestions,
474
+                answers,
475
+                infoboxes,
397 476
                 engine.response,
398 477
                 request_params
399 478
             )
@@ -431,11 +510,14 @@ class Search(object):
431 510
         # score results and remove duplications
432 511
         results = score_results(results)
433 512
 
513
+        # merge infoboxes according to their ids
514
+        infoboxes = merge_infoboxes(infoboxes)
515
+
434 516
         # update engine stats, using calculated score
435 517
         for result in results:
436 518
             for res_engine in result['engines']:
437 519
                 engines[result['engine']]\
438 520
                     .stats['score_count'] += result['score']
439 521
 
440
-        # return results and suggestions
441
-        return results, suggestions
522
+        # return results, suggestions, answers and infoboxes
523
+        return results, suggestions, answers, infoboxes

+ 1
- 1
searx/settings.yml Vedi File

@@ -1,7 +1,7 @@
1 1
 server:
2 2
     port : 8888
3 3
     secret_key : "ultrasecretkey" # change this!
4
-    debug : False # Debug mode, only for development
4
+    debug : True # Debug mode, only for development
5 5
     request_timeout : 2.0 # seconds
6 6
     base_url : False # Set custom base_url. Possible values: False or "https://your.custom.host/location/"
7 7
     themes_path : "" # Custom ui themes path

+ 1
- 75
searx/static/default/css/style.css
File diff suppressed because it is too large
Vedi File


+ 96
- 28
searx/static/default/less/style.less Vedi File

@@ -235,6 +235,17 @@ a {
235 235
 		max-width: 54em;
236 236
 		word-wrap:break-word;
237 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 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 406
 	input {
397 407
 		padding: 0;
398 408
 		margin: 3px;
399 409
 		font-size: 0.8em;
400 410
 		display: inline-block;
401
-        background: transparent;
402
-        color: @color-result-search-url-font;
411
+        	background: transparent;
412
+        	color: @color-result-search-url-font;
403 413
 		cursor: pointer;
404 414
 	}
405
-    input[type="submit"] {
415
+
416
+    	input[type="submit"] {
406 417
 		text-decoration: underline;
407
-    }
418
+    	}
408 419
 
409 420
 	form {
410 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 472
 #search_url {
415 473
 	margin-top: 8px;
416 474
 
@@ -453,16 +511,6 @@ tr {
453 511
 
454 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 514
     #results {
467 515
         margin: auto;
468 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 561
 	.right {
488 562
 		display: none;
489 563
 		postion: fixed !important;
@@ -515,12 +589,6 @@ tr {
515 589
 	.result {
516 590
 		border-top: 1px solid @color-result-top-border;
517 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
 

+ 3
- 1
searx/templates/default/result_templates/default.html Vedi File

@@ -8,6 +8,8 @@
8 8
     <h3 class="result_title"><a href="{{ result.url }}">{{ result.title|safe }}</a></h3>
9 9
     <p class="url">{{ result.pretty_url }} <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}">cached</a></p>
10 10
 	{% 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>
12 14
   </div>
13 15
 </div>

+ 16
- 0
searx/templates/default/results.html Vedi File

@@ -30,6 +30,14 @@
30 30
         </div>
31 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 41
     {% if suggestions %}
34 42
     <div id="suggestions"><span>{{ _('Suggestions') }}</span>
35 43
         {% for suggestion in suggestions %}
@@ -41,6 +49,14 @@
41 49
     </div>
42 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 60
     {% for result in results %}
45 61
         {% if result['template'] %}
46 62
             {% include 'default/result_templates/'+result['template'] %}

+ 3
- 1
searx/webapp.py Vedi File

@@ -198,7 +198,7 @@ def index():
198 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 203
     for result in search.results:
204 204
 
@@ -291,6 +291,8 @@ def index():
291 291
         pageno=search.pageno,
292 292
         base_url=get_base_url(),
293 293
         suggestions=search.suggestions,
294
+        answers=search.answers,
295
+        infoboxes=search.infoboxes,
294 296
         theme=get_current_theme_name()
295 297
     )
296 298