|
5 | 5 | from bson import Decimal128
|
6 | 6 | from django.core.exceptions import EmptyResultSet, FullResultSet
|
7 | 7 | from django.db import NotSupportedError
|
| 8 | +from django.db.models import Expression, FloatField |
8 | 9 | from django.db.models.expressions import (
|
9 | 10 | Case,
|
10 | 11 | Col,
|
@@ -212,6 +213,293 @@ def value(self, compiler, connection): # noqa: ARG001
|
212 | 213 | return value
|
213 | 214 |
|
214 | 215 |
|
| 216 | +class SearchExpression(Expression): |
| 217 | + def __init__(self): |
| 218 | + super().__init__(output_field=FloatField()) |
| 219 | + |
| 220 | + def get_source_expressions(self): |
| 221 | + return [] |
| 222 | + |
| 223 | + def __str__(self): |
| 224 | + args = ", ".join(map(str, self.get_source_expressions())) |
| 225 | + return f"{self.search_type}({args})" |
| 226 | + |
| 227 | + def __repr__(self): |
| 228 | + return str(self) |
| 229 | + |
| 230 | + def as_sql(self, compiler, connection): |
| 231 | + return "", [] |
| 232 | + |
| 233 | + def _get_query_index(self, fields, compiler): |
| 234 | + fields = set(fields) |
| 235 | + for search_indexes in compiler.collection.list_search_indexes(): |
| 236 | + mappings = search_indexes["latestDefinition"]["mappings"] |
| 237 | + if mappings["dynamic"] or fields.issubset(set(mappings["fields"])): |
| 238 | + return search_indexes["name"] |
| 239 | + return "default" |
| 240 | + |
| 241 | + |
| 242 | +class SearchAutocomplete(SearchExpression): |
| 243 | + def __init__(self, path, query, score=None): |
| 244 | + self.path = F(path) |
| 245 | + self.query = Value(query) |
| 246 | + self.score = score |
| 247 | + super().__init__() |
| 248 | + |
| 249 | + def as_mql(self, compiler, connection): |
| 250 | + params = { |
| 251 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 252 | + "query": self.query.as_mql(compiler, connection), |
| 253 | + } |
| 254 | + if self.score is not None: |
| 255 | + params["score"] = self.score |
| 256 | + index = self._get_query_index([self.path], compiler) |
| 257 | + return {"$search": {"autocomplete": params, "index": index}} |
| 258 | + |
| 259 | + |
| 260 | +class SearchEquals(SearchExpression): |
| 261 | + def __init__(self, path, value, score=None): |
| 262 | + self.path = F(path) |
| 263 | + self.value = Value(query) |
| 264 | + self.score = score |
| 265 | + super().__init__() |
| 266 | + |
| 267 | + def as_mql(self, compiler, connection): |
| 268 | + params = { |
| 269 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 270 | + "value": self.value.as_mql(compiler, connection), |
| 271 | + } |
| 272 | + if self.score is not None: |
| 273 | + params["score"] = self.score |
| 274 | + index = self._get_query_index([self.path], compiler) |
| 275 | + return {"$search": {"equals": params, "index": index}} |
| 276 | + |
| 277 | + |
| 278 | +class SearchExists(SearchExpression): |
| 279 | + def __init__(self, path, score=None): |
| 280 | + self.path = F(path) |
| 281 | + self.score = score |
| 282 | + super().__init__() |
| 283 | + |
| 284 | + def as_mql(self, compiler, connection): |
| 285 | + params = { |
| 286 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 287 | + } |
| 288 | + if self.score is not None: |
| 289 | + params["score"] = self.score |
| 290 | + index = self._get_query_index([self.path], compiler) |
| 291 | + return {"$search": {"exists": params, "index": index}} |
| 292 | + |
| 293 | + |
| 294 | +class SearchIn(SearchExpression): |
| 295 | + def __init__(self, path, value, score=None): |
| 296 | + self.path = F(path) |
| 297 | + self.value = Value(value) |
| 298 | + self.score = score |
| 299 | + super().__init__() |
| 300 | + |
| 301 | + def as_mql(self, compiler, connection): |
| 302 | + params = { |
| 303 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 304 | + "value": self.value.as_mql(compiler, connection), |
| 305 | + } |
| 306 | + if self.score is not None: |
| 307 | + params["score"] = self.score |
| 308 | + index = self._get_query_index([self.path], compiler) |
| 309 | + return {"$search": {"in": params, "index": index}} |
| 310 | + |
| 311 | + |
| 312 | +class SearchPhrase(SearchExpression): |
| 313 | + def __init__(self, path, value, slop=None, synonyms=None, score=None): |
| 314 | + self.path = F(path) |
| 315 | + self.value = Value(value) |
| 316 | + self.score = score |
| 317 | + self.slop = slop |
| 318 | + self.synonyms = synonyms |
| 319 | + super().__init__() |
| 320 | + |
| 321 | + def as_mql(self, compiler, connection): |
| 322 | + params = { |
| 323 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 324 | + "value": self.value.as_mql(compiler, connection), |
| 325 | + } |
| 326 | + if self.score is not None: |
| 327 | + params["score"] = self.score |
| 328 | + if self.slop is not None: |
| 329 | + params["slop"] = self.slop |
| 330 | + if self.synonyms is not None: |
| 331 | + params["synonyms"] = self.synonyms |
| 332 | + index = self._get_query_index([self.path], compiler) |
| 333 | + return {"$search": {"phrase": params, "index": index}} |
| 334 | + |
| 335 | + |
| 336 | +class SearchQueryString(SearchExpression): |
| 337 | + def __init__(self, path, query, score=None): |
| 338 | + self.path = F(path) |
| 339 | + self.query = Value(query) |
| 340 | + self.score = score |
| 341 | + super().__init__() |
| 342 | + |
| 343 | + def as_mql(self, compiler, connection): |
| 344 | + params = { |
| 345 | + "defaultPath": self.path.as_mql(compiler, connection)[1:], |
| 346 | + "query": self.query.as_mql(compiler, connection), |
| 347 | + } |
| 348 | + if self.score is not None: |
| 349 | + params["score"] = self.score |
| 350 | + index = self._get_query_index([self.path], compiler) |
| 351 | + return {"$search": {"queryString": params, "index": index}} |
| 352 | + |
| 353 | + |
| 354 | +class SearchRange(SearchExpression): |
| 355 | + def __init__(self, path, lt=None, lte=None, gt=None, gte=None, score=None): |
| 356 | + self.path = F(path) |
| 357 | + self.lt = Value(lt) |
| 358 | + self.lte = Value(lte) |
| 359 | + self.gt = Value(gt) |
| 360 | + self.gte = Value(gte) |
| 361 | + self.score = score |
| 362 | + super().__init__() |
| 363 | + |
| 364 | + def as_mql(self, compiler, connection): |
| 365 | + params = { |
| 366 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 367 | + } |
| 368 | + if self.score is not None: |
| 369 | + params["score"] = self.score |
| 370 | + if self.lt is not None: |
| 371 | + params["lt"] = self.lt.as_mql(compiler, connection) |
| 372 | + if self.lte is not None: |
| 373 | + params["lte"] = self.lte.as_mql(compiler, connection) |
| 374 | + if self.gt is not None: |
| 375 | + params["gt"] = self.gt.as_mql(compiler, connection) |
| 376 | + if self.gte is not None: |
| 377 | + params["gte"] = self.gte.as_mql(compiler, connection) |
| 378 | + index = self._get_query_index([self.path], compiler) |
| 379 | + return {"$search": {"range": params, "index": index}} |
| 380 | + |
| 381 | + |
| 382 | +class SearchRegex(SearchExpression): |
| 383 | + def __init__(self, path, query, allow_analyzed_field=None, score=None): |
| 384 | + self.path = F(path) |
| 385 | + self.allow_analyzed_field = Value(allow_analyzed_field) |
| 386 | + self.score = score |
| 387 | + super().__init__() |
| 388 | + |
| 389 | + def as_mql(self, compiler, connection): |
| 390 | + params = { |
| 391 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 392 | + } |
| 393 | + if self.score: |
| 394 | + params["score"] = self.score |
| 395 | + if self.allow_analyzed_field is not None: |
| 396 | + params["allowAnalyzedField"] = self.allow_analyzed_field.as_mql(compiler, connection) |
| 397 | + index = self._get_query_index([self.path], compiler) |
| 398 | + return {"$search": {"regex": params, "index": index}} |
| 399 | + |
| 400 | + |
| 401 | +class SearchText(SearchExpression): |
| 402 | + def __init__(self, path, query, fuzzy=None, match_criteria=None, synonyms=None, score=None): |
| 403 | + self.path = F(path) |
| 404 | + self.fuzzy = Value(fuzzy) |
| 405 | + self.match_criteria = Value(match_criteria) |
| 406 | + self.synonyms = Value(synonyms) |
| 407 | + self.score = score |
| 408 | + super().__init__() |
| 409 | + |
| 410 | + def as_mql(self, compiler, connection): |
| 411 | + params = { |
| 412 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 413 | + } |
| 414 | + if self.score: |
| 415 | + params["score"] = self.score |
| 416 | + if self.fuzzy is not None: |
| 417 | + params["fuzzy"] = self.fuzzy.as_mql(compiler, connection) |
| 418 | + if self.match_criteria is not None: |
| 419 | + params["matchCriteria"] = self.match_criteria.as_mql(compiler, connection) |
| 420 | + if self.synonyms is not None: |
| 421 | + params["synonyms"] = self.synonyms.as_mql(compiler, connection) |
| 422 | + index = self._get_query_index([self.path], compiler) |
| 423 | + return {"$search": {"text": params, "index": index}} |
| 424 | + |
| 425 | + |
| 426 | +class SearchWildcard(SearchExpression): |
| 427 | + def __init__(self, path, query, allow_analyzed_field=None, score=None): |
| 428 | + self.path = F(path) |
| 429 | + self.allow_analyzed_field = Value(allow_analyzed_field) |
| 430 | + self.score = score |
| 431 | + super().__init__() |
| 432 | + |
| 433 | + def as_mql(self, compiler, connection): |
| 434 | + params = { |
| 435 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 436 | + } |
| 437 | + if self.score: |
| 438 | + params["score"] = self.score |
| 439 | + if self.allow_analyzed_field is not None: |
| 440 | + params["allowAnalyzedField"] = self.allow_analyzed_field.as_mql(compiler, connection) |
| 441 | + index = self._get_query_index([self.path], compiler) |
| 442 | + return {"$search": {"wildcard": params, "index": index}} |
| 443 | + |
| 444 | + |
| 445 | +class SearchGeoShape(SearchExpression): |
| 446 | + def __init__(self, path, relation, geometry, score=None): |
| 447 | + self.path = F(path) |
| 448 | + self.relation = relation |
| 449 | + self.geometry = geometry |
| 450 | + self.score = score |
| 451 | + super().__init__() |
| 452 | + |
| 453 | + def as_mql(self, compiler, connection): |
| 454 | + params = { |
| 455 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 456 | + "relation": self.relation, |
| 457 | + "geometry": self.geometry, |
| 458 | + } |
| 459 | + if self.score: |
| 460 | + params["score"] = self.score |
| 461 | + index = self._get_query_index([self.path], compiler) |
| 462 | + return {"$search": {"wildcard": params, "index": index}} |
| 463 | + |
| 464 | + |
| 465 | +class SearchGeoWithin(SearchExpression): |
| 466 | + def __init__(self, path, kind, geo_object, geometry, score=None): |
| 467 | + self.path = F(path) |
| 468 | + self.kind = kind |
| 469 | + self.geo_object = geo_object |
| 470 | + self.score = score |
| 471 | + super().__init__() |
| 472 | + |
| 473 | + def as_mql(self, compiler, connection): |
| 474 | + params = { |
| 475 | + "path": self.path.as_mql(compiler, connection)[1:], |
| 476 | + self.kind: self.geo_object, |
| 477 | + } |
| 478 | + if self.score: |
| 479 | + params["score"] = self.score |
| 480 | + index = self._get_query_index([self.path], compiler) |
| 481 | + return {"$search": {"wildcard": params, "index": index}} |
| 482 | + |
| 483 | + |
| 484 | +class SearchMoreLikeThis(SearchExpression): |
| 485 | + def __init__(self, documents, score=None): |
| 486 | + self.documents = documents |
| 487 | + self.score = score |
| 488 | + super().__init__() |
| 489 | + |
| 490 | + def as_mql(self, compiler, connection): |
| 491 | + params = { |
| 492 | + "like": self.documents, |
| 493 | + } |
| 494 | + if self.score: |
| 495 | + params["score"] = self.score |
| 496 | + needed_fields = [] |
| 497 | + for doc in self.documents: |
| 498 | + needed_fields += list(doc.keys()) |
| 499 | + index = self._get_query_index(needed_fields, compiler) |
| 500 | + return {"$search": {"wildcard": params, "index": index}} |
| 501 | + |
| 502 | + |
215 | 503 | def register_expressions():
|
216 | 504 | Case.as_mql = case
|
217 | 505 | Col.as_mql = col
|
|
0 commit comments