summaryrefslogtreecommitdiff
path: root/searx/search.py
blob: e0aa1b58c08d4058b49eefe4ec18750835b43bb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
'''
searx is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

searx is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with searx. If not, see < http://www.gnu.org/licenses/ >.

(C) 2013- by Adam Tauber, <asciimoo@gmail.com>
'''

import gc
import threading
from thread import start_new_thread
from time import time
from uuid import uuid4
import searx.poolrequests as requests_lib
from searx.engines import (
    categories, engines
)
from searx.answerers import ask
from searx.utils import gen_useragent
from searx.query import RawTextQuery, SearchQuery
from searx.results import ResultContainer
from searx import logger
from searx.plugins import plugins

logger = logger.getChild('search')

number_of_searches = 0


def send_http_request(engine, request_params, timeout_limit):
    response = None
    try:
        # create dictionary which contain all
        # informations about the request
        request_args = dict(
            headers=request_params['headers'],
            cookies=request_params['cookies'],
            timeout=timeout_limit,
            verify=request_params['verify']
        )
        # specific type of request (GET or POST)
        if request_params['method'] == 'GET':
            req = requests_lib.get
        else:
            req = requests_lib.post
            request_args['data'] = request_params['data']

        # for page_load_time stats
        time_before_request = time()

        # send the request
        response = req(request_params['url'], **request_args)

        with threading.RLock():
            # no error : reset the suspend variables
            engine.continuous_errors = 0
            engine.suspend_end_time = 0
            # update stats with current page-load-time
            # only the HTTP request
            engine.stats['page_load_time'] += time() - time_before_request
            engine.stats['page_load_count'] += 1

        # is there a timeout (no parsing in this case)
        timeout_overhead = 0.2  # seconds
        search_duration = time() - request_params['started']
        if search_duration > timeout_limit + timeout_overhead:
            logger.exception('engine timeout on HTTP request:'
                             '{0} (search duration : {1} ms, time-out: {2} )'
                             .format(engine.name, search_duration, timeout_limit))
            with threading.RLock():
                engine.stats['errors'] += 1
            return False

        # everything is ok : return the response
        return response

    except:
        # increase errors stats
        with threading.RLock():
            engine.stats['errors'] += 1
            engine.continuous_errors += 1
            engine.suspend_end_time = time() + min(60, engine.continuous_errors)

        # print engine name and specific error message
        logger.exception('engine crash: {0}'.format(engine.name))
        return False


def search_one_request(engine_name, query, request_params, result_container, timeout_limit):
    engine = engines[engine_name]

    # update request parameters dependent on
    # search-engine (contained in engines folder)
    engine.request(query, request_params)

    # TODO add support of offline engines
    if request_params['url'] is None:
        return False

    # ignoring empty urls
    if not request_params['url']:
        return False

    # send request
    response = send_http_request(engine, request_params, timeout_limit)

    # parse response
    success = None
    if response:
        # parse the response
        response.search_params = request_params
        try:
            search_results = engine.response(response)
        except:
            logger.exception('engine crash: {0}'.format(engine.name))
            search_results = []

        # add results
        for result in search_results:
            result['engine'] = engine.name

        result_container.extend(engine.name, search_results)

        success = True
    else:
        success = False

    with threading.RLock():
        # update stats : total time
        engine.stats['engine_time'] += time() - request_params['started']
        engine.stats['engine_time_count'] += 1

    return success


def search_multiple_requests(requests, result_container, timeout_limit):
    start_time = time()
    search_id = uuid4().__str__()

    for engine_name, query, request_params in requests:
        th = threading.Thread(
            target=search_one_request,
            args=(engine_name, query, request_params, result_container, timeout_limit),
            name=search_id,
        )
        th._engine_name = engine_name
        th.start()

    for th in threading.enumerate():
        if th.name == search_id:
            remaining_time = max(0.0, timeout_limit - (time() - start_time))
            th.join(remaining_time)
            if th.isAlive():
                logger.warning('engine timeout: {0}'.format(th._engine_name))


# get default reqest parameter
def default_request_params():
    return {
        'method': 'GET',
        'headers': {},
        'data': {},
        'url': '',
        'cookies': {},
        'verify': True
    }


def get_search_query_from_webapp(preferences, form):
    query = None
    query_engines = []
    query_categories = []
    query_paging = False
    query_pageno = 1
    query_lang = 'all'
    query_time_range = None

    # set blocked engines
    disabled_engines = preferences.engines.get_disabled()

    # set specific language if set
    query_lang = preferences.get_value('language')

    # safesearch
    query_safesearch = preferences.get_value('safesearch')

    # TODO better exceptions
    if not form.get('q'):
        raise Exception('noquery')

    # set pagenumber
    pageno_param = form.get('pageno', '1')
    if not pageno_param.isdigit() or int(pageno_param) < 1:
        pageno_param = 1

    query_pageno = int(pageno_param)

    # parse query, if tags are set, which change
    # the serch engine or search-language
    raw_text_query = RawTextQuery(form['q'], disabled_engines)
    raw_text_query.parse_query()

    # set query
    query = raw_text_query.getSearchQuery()

    # get last selected language in query, if possible
    # TODO support search with multible languages
    if len(raw_text_query.languages):
        query_lang = raw_text_query.languages[-1]

    query_time_range = form.get('time_range')

    query_engines = raw_text_query.engines

    # if engines are calculated from query,
    # set categories by using that informations
    if query_engines and raw_text_query.specific:
        query_categories = list(set(engine['category']
                                    for engine in query_engines))

    # otherwise, using defined categories to
    # calculate which engines should be used
    else:
        # set categories/engines
        load_default_categories = True
        for pd_name, pd in form.items():
            if pd_name == 'categories':
                query_categories.extend(categ for categ in map(unicode.strip, pd.split(',')) if categ in categories)
            elif pd_name == 'engines':
                pd_engines = [{'category': engines[engine].categories[0],
                               'name': engine}
                              for engine in map(unicode.strip, pd.split(',')) if engine in engines]
                if pd_engines:
                    query_engines.extend(pd_engines)
                    load_default_categories = False
            elif pd_name.startswith('category_'):
                category = pd_name[9:]

                # if category is not found in list, skip
                if category not in categories:
                    continue

                if pd != 'off':
                    # add category to list
                    query_categories.append(category)
                elif category in query_categories:
                    # remove category from list if property is set to 'off'
                    query_categories.remove(category)

        if not load_default_categories:
            if not query_categories:
                query_categories = list(set(engine['category']
                                            for engine in engines))
        else:
            # if no category is specified for this search,
            # using user-defined default-configuration which
            # (is stored in cookie)
            if not query_categories:
                cookie_categories = preferences.get_value('categories')
                for ccateg in cookie_categories:
                    if ccateg in categories:
                        query_categories.append(ccateg)

            # if still no category is specified, using general
            # as default-category
            if not query_categories:
                query_categories = ['general']

            # using all engines for that search, which are
            # declared under the specific categories
            for categ in query_categories:
                query_engines.extend({'category': categ,
                                      'name': engine.name}
                                     for engine in categories[categ]
                                     if (engine.name, categ) not in disabled_engines)

    return SearchQuery(query, query_engines, query_categories,
                       query_lang, query_safesearch, query_pageno, query_time_range)


class Search(object):

    """Search information container"""

    def __init__(self, search_query):
        # init vars
        super(Search, self).__init__()
        self.search_query = search_query
        self.result_container = ResultContainer()

    # do search-request
    def search(self):
        global number_of_searches

        # start time
        start_time = time()

        # answeres ?
        answerers_results = ask(self.search_query)

        if answerers_results:
            for results in answerers_results:
                self.result_container.extend('answer', results)
            return self.result_container

        # init vars
        requests = []

        # increase number of searches
        number_of_searches += 1

        # set default useragent
        # user_agent = request.headers.get('User-Agent', '')
        user_agent = gen_useragent()

        search_query = self.search_query

        # max of all selected engine timeout
        timeout_limit = 0

        # start search-reqest for all selected engines
        for selected_engine in search_query.engines:
            if selected_engine['name'] not in engines:
                continue

            engine = engines[selected_engine['name']]

            # skip suspended engines
            if engine.suspend_end_time >= time():
                logger.debug('Engine currently suspended: %s', selected_engine['name'])
                continue

            # if paging is not supported, skip
            if search_query.pageno > 1 and not engine.paging:
                continue

            # if search-language is set and engine does not
            # provide language-support, skip
            if search_query.lang != 'all' and not engine.language_support:
                continue

            # if time_range is not supported, skip
            if search_query.time_range and not engine.time_range_support:
                continue

            # set default request parameters
            request_params = default_request_params()
            request_params['headers']['User-Agent'] = user_agent
            request_params['category'] = selected_engine['category']
            request_params['started'] = start_time
            request_params['pageno'] = search_query.pageno

            if hasattr(engine, 'language') and engine.language:
                request_params['language'] = engine.language
            else:
                request_params['language'] = search_query.lang

            # 0 = None, 1 = Moderate, 2 = Strict
            request_params['safesearch'] = search_query.safesearch
            request_params['time_range'] = search_query.time_range

            # append request to list
            requests.append((selected_engine['name'], search_query.query.encode('utf-8'), request_params))

            # update timeout_limit
            timeout_limit = max(timeout_limit, engine.timeout)

        if requests:
            # send all search-request
            search_multiple_requests(requests, self.result_container, timeout_limit - (time() - start_time))
            start_new_thread(gc.collect, tuple())

        # return results, suggestions, answers and infoboxes
        return self.result_container


class SearchWithPlugins(Search):

    """Similar to the Search class but call the plugins."""

    def __init__(self, search_query, request):
        super(SearchWithPlugins, self).__init__(search_query)
        self.request = request

    def search(self):
        if plugins.call('pre_search', self.request, self):
            super(SearchWithPlugins, self).search()

        plugins.call('post_search', self.request, self)

        results = self.result_container.get_ordered_results()

        for result in results:
            plugins.call('on_result', self.request, self, result)

        return self.result_container