.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
query.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2# pylint: disable=invalid-name, missing-module-docstring, missing-class-docstring
3
4from abc import abstractmethod, ABC
5import re
6
7from searx import settings
8from searx.sxng_locales import sxng_locales
9from searx.engines import categories, engines, engine_shortcuts
10from searx.external_bang import get_bang_definition_and_autocomplete
11from searx.search import EngineRef
12from searx.webutils import VALID_LANGUAGE_CODE
13
14
15class QueryPartParser(ABC):
16
17 __slots__ = "raw_text_query", "enable_autocomplete"
18
19 @staticmethod
20 @abstractmethod
21 def check(raw_value):
22 """Check if raw_value can be parsed"""
23
24 def __init__(self, raw_text_query, enable_autocomplete):
25 self.raw_text_query = raw_text_query
26 self.enable_autocomplete = enable_autocomplete
27
28 @abstractmethod
29 def __call__(self, raw_value):
30 """Try to parse raw_value: set the self.raw_text_query properties
31
32 return True if raw_value has been parsed
33
34 self.raw_text_query.autocomplete_list is also modified
35 if self.enable_autocomplete is True
36 """
37
38 def _add_autocomplete(self, value):
39 if value not in self.raw_text_query.autocomplete_list:
40 self.raw_text_query.autocomplete_list.append(value)
41
42
44 @staticmethod
45 def check(raw_value):
46 return raw_value[0] == '<'
47
48 def __call__(self, raw_value):
49 value = raw_value[1:]
50 found = self._parse(value) if len(value) > 0 else False
51 if self.enable_autocomplete and not value:
52 self._autocomplete()
53 return found
54
55 def _parse(self, value):
56 if not value.isdigit():
57 return False
58 raw_timeout_limit = int(value)
59 if raw_timeout_limit < 100:
60 # below 100, the unit is the second ( <3 = 3 seconds timeout )
61 self.raw_text_query.timeout_limit = float(raw_timeout_limit)
62 else:
63 # 100 or above, the unit is the millisecond ( <850 = 850 milliseconds timeout )
64 self.raw_text_query.timeout_limit = raw_timeout_limit / 1000.0
65 return True
66
67 def _autocomplete(self):
68 for suggestion in ['<3', '<850']:
69 self._add_autocomplete(suggestion)
70
71
73 @staticmethod
74 def check(raw_value):
75 return raw_value[0] == ':'
76
77 def __call__(self, raw_value):
78 value = raw_value[1:].lower().replace('_', '-')
79 found = self._parse(value) if len(value) > 0 else False
80 if self.enable_autocomplete and not found:
81 self._autocomplete(value)
82 return found
83
84 def _parse(self, value):
85 found = False
86 # check if any language-code is equal with
87 # declared language-codes
88 for lc in sxng_locales:
89 lang_id, lang_name, country, english_name, _flag = map(str.lower, lc)
90
91 # if correct language-code is found
92 # set it as new search-language
93
94 if (
95 value == lang_id or value == lang_name or value == english_name or value.replace('-', ' ') == country
96 ) and value not in self.raw_text_query.languages:
97 found = True
98 lang_parts = lang_id.split('-')
99 if len(lang_parts) == 2:
100 self.raw_text_query.languages.append(lang_parts[0] + '-' + lang_parts[1].upper())
101 else:
102 self.raw_text_query.languages.append(lang_id)
103 # to ensure best match (first match is not necessarily the best one)
104 if value == lang_id:
105 break
106
107 # user may set a valid, yet not selectable language
108 if VALID_LANGUAGE_CODE.match(value) or value == 'auto':
109 lang_parts = value.split('-')
110 if len(lang_parts) > 1:
111 value = lang_parts[0].lower() + '-' + lang_parts[1].upper()
112 if value not in self.raw_text_query.languages:
113 self.raw_text_query.languages.append(value)
114 found = True
115
116 return found
117
118 def _autocomplete(self, value):
119 if not value:
120 # show some example queries
121 if len(settings['search']['languages']) < 10:
122 for lang in settings['search']['languages']:
123 self.raw_text_query.autocomplete_list.append(':' + lang)
124 else:
125 for lang in [":en", ":en_us", ":english", ":united_kingdom"]:
126 self.raw_text_query.autocomplete_list.append(lang)
127 return
128
129 for lc in sxng_locales:
130 if lc[0] not in settings['search']['languages']:
131 continue
132 lang_id, lang_name, country, english_name, _flag = map(str.lower, lc)
133
134 # check if query starts with language-id
135 if lang_id.startswith(value):
136 if len(value) <= 2:
137 self._add_autocomplete(':' + lang_id.split('-')[0])
138 else:
139 self._add_autocomplete(':' + lang_id)
140
141 # check if query starts with language name
142 if lang_name.startswith(value) or english_name.startswith(value):
143 self._add_autocomplete(':' + lang_name)
144
145 # check if query starts with country
146 # here "new_zealand" is "new-zealand" (see __call__)
147 if country.startswith(value.replace('-', ' ')):
148 self._add_autocomplete(':' + country.replace(' ', '_'))
149
150
152 @staticmethod
153 def check(raw_value):
154 return raw_value.startswith('!!') and len(raw_value) > 2
155
156 def __call__(self, raw_value):
157 value = raw_value[2:]
158 found, bang_ac_list = self._parse(value) if len(value) > 0 else (False, [])
159 if self.enable_autocomplete:
160 self._autocomplete(bang_ac_list)
161 return found
162
163 def _parse(self, value):
164 found = False
165 bang_definition, bang_ac_list = get_bang_definition_and_autocomplete(value)
166 if bang_definition is not None:
167 self.raw_text_query.external_bang = value
168 found = True
169 return found, bang_ac_list
170
171 def _autocomplete(self, bang_ac_list):
172 if not bang_ac_list:
173 bang_ac_list = ['g', 'ddg', 'bing']
174 for external_bang in bang_ac_list:
175 self._add_autocomplete('!!' + external_bang)
176
177
179 @staticmethod
180 def check(raw_value):
181 # make sure it's not any bang with double '!!'
182 return raw_value[0] == '!' and (len(raw_value) < 2 or raw_value[1] != '!')
183
184 def __call__(self, raw_value):
185 value = raw_value[1:].replace('-', ' ').replace('_', ' ')
186 found = self._parse(value) if len(value) > 0 else False
187 if found and raw_value[0] == '!':
188 self.raw_text_query.specific = True
189 if self.enable_autocomplete:
190 self._autocomplete(raw_value[0], value)
191 return found
192
193 def _parse(self, value):
194 # check if prefix is equal with engine shortcut
195 if value in engine_shortcuts: # pylint: disable=consider-using-get
196 value = engine_shortcuts[value]
197
198 # check if prefix is equal with engine name
199 if value in engines:
200 self.raw_text_query.enginerefs.append(EngineRef(value, 'none'))
201 return True
202
203 # check if prefix is equal with category name
204 if value in categories:
205 # using all engines for that search, which
206 # are declared under that category name
207 self.raw_text_query.enginerefs.extend(
208 EngineRef(engine.name, value)
209 for engine in categories[value]
210 if (engine.name, value) not in self.raw_text_query.disabled_engines
211 )
212 return True
213
214 return False
215
216 def _autocomplete(self, first_char, value):
217 if not value:
218 # show some example queries
219 for suggestion in ['images', 'wikipedia', 'osm']:
220 if suggestion not in self.raw_text_query.disabled_engines or suggestion in categories:
221 self._add_autocomplete(first_char + suggestion)
222 return
223
224 # check if query starts with category name
225 for category in categories:
226 if category.startswith(value):
227 self._add_autocomplete(first_char + category.replace(' ', '_'))
228
229 # check if query starts with engine name
230 for engine in engines:
231 if engine.startswith(value):
232 self._add_autocomplete(first_char + engine.replace(' ', '_'))
233
234 # check if query starts with engine shortcut
235 for engine_shortcut in engine_shortcuts:
236 if engine_shortcut.startswith(value):
237 self._add_autocomplete(first_char + engine_shortcut)
238
239
241 @staticmethod
242 def check(raw_value):
243 return raw_value == '!!'
244
245 def __call__(self, raw_value):
246 self.raw_text_query.redirect_to_first_result = True
247 return True
248
249
251 """parse raw text query (the value from the html input)"""
252
253 PARSER_CLASSES = [
254 TimeoutParser, # force the timeout
255 LanguageParser, # force a language
256 ExternalBangParser, # external bang (must be before BangParser)
257 BangParser, # force an engine or category
258 FeelingLuckyParser, # redirect to the first link in the results list
259 ]
260
261 def __init__(self, query, disabled_engines):
262 assert isinstance(query, str)
263 # input parameters
264 self.query = query
265 self.disabled_engines = disabled_engines if disabled_engines else []
266 # parsed values
267 self.enginerefs = []
268 self.languages = []
269 self.timeout_limit = None
270 self.external_bang = None
271 self.specific = False
273 # internal properties
274 self.query_parts = [] # use self.getFullQuery()
275 self.user_query_parts = [] # use self.getQuery()
278 self._parse_query()
279
280 def _parse_query(self):
281 """
282 parse self.query, if tags are set, which
283 change the search engine or search-language
284 """
285
286 # split query, including whitespaces
287 raw_query_parts = re.split(r'(\s+)', self.query)
288
289 last_index_location = None
290 autocomplete_index = len(raw_query_parts) - 1
291
292 for i, query_part in enumerate(raw_query_parts):
293 # part does only contain spaces, skip
294 if query_part.isspace() or query_part == '':
295 continue
296
297 # parse special commands
298 special_part = False
299 for parser_class in RawTextQuery.PARSER_CLASSES:
300 if parser_class.check(query_part):
301 special_part = parser_class(self, i == autocomplete_index)(query_part)
302 break
303
304 # append query part to query_part list
305 qlist = self.query_parts if special_part else self.user_query_parts
306 qlist.append(query_part)
307 last_index_location = (qlist, len(qlist) - 1)
308
309 self.autocomplete_location = last_index_location
310
312 qlist, position = self.autocomplete_location
313 qlist[position] = text
314 return self.getFullQuery()
315
316 def changeQuery(self, query):
317 self.user_query_parts = query.strip().split()
318 self.query = self.getFullQuery()
319 self.autocomplete_location = (self.user_query_parts, len(self.user_query_parts) - 1)
320 self.autocomplete_list = []
321 return self
322
323 def getQuery(self):
324 return ' '.join(self.user_query_parts)
325
326 def getFullQuery(self):
327 """
328 get full query including whitespaces
329 """
330 return '{0} {1}'.format(' '.join(self.query_parts), self.getQuery()).strip()
331
332 def __str__(self):
333 return self.getFullQuery()
334
335 def __repr__(self):
336 return (
337 f"<{self.__class__.__name__} "
338 + f"query={self.query!r} "
339 + f"disabled_engines={self.disabled_engines!r}\n "
340 + f"languages={self.languages!r} "
341 + f"timeout_limit={self.timeout_limit!r} "
342 + f"external_bang={self.external_bang!r} "
343 + f"specific={self.specific!r} "
344 + f"enginerefs={self.enginerefs!r}\n "
345 + f"autocomplete_list={self.autocomplete_list!r}\n "
346 + f"query_parts={self.query_parts!r}\n "
347 + f"user_query_parts={self.user_query_parts!r} >\n"
348 + f"redirect_to_first_result={self.redirect_to_first_result!r}"
349 )
_parse(self, value)
Definition query.py:193
check(raw_value)
Definition query.py:180
_autocomplete(self, first_char, value)
Definition query.py:216
__call__(self, raw_value)
Definition query.py:184
_autocomplete(self, bang_ac_list)
Definition query.py:171
__call__(self, raw_value)
Definition query.py:156
__call__(self, raw_value)
Definition query.py:245
_autocomplete(self, value)
Definition query.py:118
_parse(self, value)
Definition query.py:84
__call__(self, raw_value)
Definition query.py:77
_add_autocomplete(self, value)
Definition query.py:38
__call__(self, raw_value)
Definition query.py:29
__init__(self, raw_text_query, enable_autocomplete)
Definition query.py:24
__init__(self, query, disabled_engines)
Definition query.py:261
changeQuery(self, query)
Definition query.py:316
get_autocomplete_full_query(self, text)
Definition query.py:311
__call__(self, raw_value)
Definition query.py:48
_parse(self, value)
Definition query.py:55
::1337x
Definition 1337x.py:1