1
1
from django .db import NotSupportedError
2
- from django .db .models import Expression , FloatField
2
+ from django .db .models import CharField , Expression , FloatField , TextField
3
3
from django .db .models .expressions import F , Value
4
+ from django .db .models .lookups import Lookup
5
+
6
+ from ..query_utils import process_lhs , process_rhs
4
7
5
8
6
9
def cast_as_value (value ):
@@ -68,7 +71,7 @@ def __or__(self, other):
68
71
return self ._combine (other , Operator (Operator .OR ))
69
72
70
73
def __ror__ (self , other ):
71
- return self ._combine (self , Operator (Operator .OR ), other )
74
+ return self ._combine (other , Operator (Operator .OR ))
72
75
73
76
74
77
class SearchExpression (SearchCombinable , Expression ):
@@ -86,22 +89,23 @@ def __str__(self):
86
89
cls = self .identity [0 ]
87
90
kwargs = dict (self .identity [1 :])
88
91
arg_str = ", " .join (f"{ k } ={ v !r} " for k , v in kwargs .items ())
89
- return f"{ cls .__name__ } ({ arg_str } )"
92
+ return f"< { cls .__name__ } ({ arg_str } )> "
90
93
91
94
def __repr__ (self ):
92
95
return str (self )
93
96
94
97
def as_sql (self , compiler , connection ):
95
98
return "" , []
96
99
97
- def get_source_expressions (self ):
98
- return []
99
-
100
100
def _get_indexed_fields (self , mappings ):
101
- for field , definition in mappings .get ("fields" , {}).items ():
102
- yield field
103
- for path in self ._get_indexed_fields (definition ):
104
- yield f"{ field } .{ path } "
101
+ if isinstance (mappings , list ):
102
+ for definition in mappings :
103
+ yield from self ._get_indexed_fields (definition )
104
+ else :
105
+ for field , definition in mappings .get ("fields" , {}).items ():
106
+ yield field
107
+ for path in self ._get_indexed_fields (definition ):
108
+ yield f"{ field } .{ path } "
105
109
106
110
def _get_query_index (self , fields , compiler ):
107
111
fields = set (fields )
@@ -139,9 +143,7 @@ class SearchAutocomplete(SearchExpression):
139
143
any-order token matching.
140
144
score: Optional expression to adjust score relevance (e.g., `{"boost": {"value": 5}}`).
141
145
142
- Notes:
143
- * Requires an Atlas Search index with `autocomplete` mappings.
144
- * The operator is injected under the `$search` stage in the aggregation pipeline.
146
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/autocomplete/
145
147
"""
146
148
147
149
def __init__ (self , path , query , fuzzy = None , token_order = None , score = None ):
@@ -190,10 +192,7 @@ class SearchEquals(SearchExpression):
190
192
value: The exact value to match against.
191
193
score: Optional expression to modify the relevance score.
192
194
193
- Notes:
194
- * The field must be indexed with a supported type for `equals`.
195
- * Supports numeric, string, boolean, and date values.
196
- * Score boosting can be applied using the `score` parameter.
195
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/equals/
197
196
"""
198
197
199
198
def __init__ (self , path , value , score = None ):
@@ -236,9 +235,7 @@ class SearchExists(SearchExpression):
236
235
path: The document path to check (as string or expression).
237
236
score: Optional expression to modify the relevance score.
238
237
239
- Notes:
240
- * The target field must be mapped in the Atlas Search index.
241
- * This does not test for null—only for presence.
238
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/exists/
242
239
"""
243
240
244
241
def __init__ (self , path , score = None ):
@@ -260,11 +257,28 @@ def search_operator(self, compiler, connection):
260
257
"path" : self .path .as_mql (compiler , connection , as_path = True ),
261
258
}
262
259
if self .score is not None :
263
- params ["score" ] = self .score .definitions
260
+ params ["score" ] = self .score .as_mql ( compiler , connection )
264
261
return {"exists" : params }
265
262
266
263
267
264
class SearchIn (SearchExpression ):
265
+ """
266
+ Atlas Search expression that matches documents where the field value is in a given list.
267
+
268
+ This expression uses the **in** operator to match documents whose field
269
+ contains a value from the provided array of values.
270
+
271
+ Example:
272
+ SearchIn("status", ["pending", "approved", "rejected"])
273
+
274
+ Args:
275
+ path: The document path to match against (as string or expression).
276
+ value: A list of values to check for membership.
277
+ score: Optional expression to adjust the relevance score.
278
+
279
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/in/
280
+ """
281
+
268
282
def __init__ (self , path , value , score = None ):
269
283
self .path = cast_as_field (path )
270
284
self .value = cast_as_value (value )
@@ -294,7 +308,7 @@ class SearchPhrase(SearchExpression):
294
308
"""
295
309
Atlas Search expression that matches a phrase in the specified field.
296
310
297
- This expression uses the **phrase** operator to search for exact or near- exact
311
+ This expression uses the **phrase** operator to search for exact or near exact
298
312
sequences of terms. It supports optional slop (word distance) and synonym sets.
299
313
300
314
Example:
@@ -307,10 +321,7 @@ class SearchPhrase(SearchExpression):
307
321
synonyms: Optional name of a synonym mapping defined in the Atlas index.
308
322
score: Optional expression to modify the relevance score.
309
323
310
- Notes:
311
- * The field must be mapped as `"type": "string"` with appropriate analyzers.
312
- * Slop allows flexibility in word positioning, like `"quick brown fox"`
313
- matching `"quick fox"` if `slop=1`.
324
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/phrase/
314
325
"""
315
326
316
327
def __init__ (self , path , query , slop = None , synonyms = None , score = None ):
@@ -360,9 +371,7 @@ class SearchQueryString(SearchExpression):
360
371
query: The Lucene-style query string.
361
372
score: Optional expression to modify the relevance score.
362
373
363
- Notes:
364
- * The query string syntax must conform to Atlas Search rules.
365
- * This operator is powerful but can be harder to validate or sanitize.
374
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/queryString/
366
375
"""
367
376
368
377
def __init__ (self , path , query , score = None ):
@@ -408,9 +417,7 @@ class SearchRange(SearchExpression):
408
417
gte: Optional inclusive lower bound (`>=`).
409
418
score: Optional expression to modify the relevance score.
410
419
411
- Notes:
412
- * At least one of `lt`, `lte`, `gt`, or `gte` must be provided.
413
- * The field must be mapped in the Atlas Search index as a comparable type.
420
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/range/
414
421
"""
415
422
416
423
def __init__ (self , path , lt = None , lte = None , gt = None , gte = None , score = None ):
@@ -464,10 +471,7 @@ class SearchRegex(SearchExpression):
464
471
allow_analyzed_field: Whether to allow matching against analyzed fields (default is False).
465
472
score: Optional expression to modify the relevance score.
466
473
467
- Notes:
468
- * Regular expressions must follow JavaScript regex syntax.
469
- * By default, the field must be mapped as `"analyzer": "keyword"`
470
- unless `allow_analyzed_field=True`.
474
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/regex/
471
475
"""
472
476
473
477
def __init__ (self , path , query , allow_analyzed_field = None , score = None ):
@@ -516,9 +520,7 @@ class SearchText(SearchExpression):
516
520
synonyms: Optional name of a synonym mapping defined in the Atlas index.
517
521
score: Optional expression to adjust relevance scoring.
518
522
519
- Notes:
520
- * The target field must be indexed for full-text search in Atlas.
521
- * Fuzzy matching helps match terms with minor typos or variations.
523
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/text/
522
524
"""
523
525
524
526
def __init__ (self , path , query , fuzzy = None , match_criteria = None , synonyms = None , score = None ):
@@ -571,11 +573,7 @@ class SearchWildcard(SearchExpression):
571
573
allow_analyzed_field: Whether to allow matching against analyzed fields (default is False).
572
574
score: Optional expression to modify the relevance score.
573
575
574
- Notes:
575
- * Wildcard patterns follow standard syntax, where `*` matches any sequence of characters
576
- and `?` matches a single character.
577
- * By default, the field should be keyword or unanalyzed
578
- unless `allow_analyzed_field=True`.
576
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/wildcard/
579
577
"""
580
578
581
579
def __init__ (self , path , query , allow_analyzed_field = None , score = None ):
@@ -600,7 +598,7 @@ def search_operator(self, compiler, connection):
600
598
"query" : self .query .value ,
601
599
}
602
600
if self .score :
603
- params ["score" ] = self .score .query . as_mql (compiler , connection )
601
+ params ["score" ] = self .score .as_mql (compiler , connection )
604
602
if self .allow_analyzed_field is not None :
605
603
params ["allowAnalyzedField" ] = self .allow_analyzed_field .value
606
604
return {"wildcard" : params }
@@ -622,9 +620,7 @@ class SearchGeoShape(SearchExpression):
622
620
geometry: The GeoJSON geometry to compare against.
623
621
score: Optional expression to modify the relevance score.
624
622
625
- Notes:
626
- * The field must be indexed as a geo shape type in Atlas Search.
627
- * Geometry must conform to GeoJSON specification.
623
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/geoShape/
628
624
"""
629
625
630
626
def __init__ (self , path , relation , geometry , score = None ):
@@ -671,9 +667,7 @@ class SearchGeoWithin(SearchExpression):
671
667
geo_object: The GeoJSON geometry defining the boundary.
672
668
score: Optional expression to adjust the relevance score.
673
669
674
- Notes:
675
- * The geo field must be indexed appropriately in the Atlas Search index.
676
- * The geometry must follow GeoJSON format.
670
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/geoWithin/
677
671
"""
678
672
679
673
def __init__ (self , path , kind , geo_object , score = None ):
@@ -716,9 +710,7 @@ class SearchMoreLikeThis(SearchExpression):
716
710
documents: A list of example documents or expressions to find similar documents.
717
711
score: Optional expression to modify the relevance scoring.
718
712
719
- Notes:
720
- * The documents should be representative examples to base similarity on.
721
- * Supports various field types depending on the Atlas Search configuration.
713
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/
722
714
"""
723
715
724
716
def __init__ (self , documents , score = None ):
@@ -771,9 +763,7 @@ class CompoundExpression(SearchExpression):
771
763
score: Optional expression to adjust scoring.
772
764
minimum_should_match: Minimum number of `should` clauses that must match.
773
765
774
- Notes:
775
- * This is the most flexible way to build complex Atlas Search queries.
776
- * Supports nesting of expressions to any depth.
766
+ Reference: https://www.mongodb.com/docs/atlas/atlas-search/compound/
777
767
"""
778
768
779
769
def __init__ (
@@ -856,10 +846,6 @@ class CombinedSearchExpression(SearchExpression):
856
846
lhs: The left-hand search expression.
857
847
operator: The boolean operator as a string (e.g., "and", "or", "not").
858
848
rhs: The right-hand search expression.
859
-
860
- Notes:
861
- * The operator must be supported by MongoDB Atlas Search boolean logic.
862
- * This class enables building complex nested search queries.
863
849
"""
864
850
865
851
def __init__ (self , lhs , operator , rhs ):
@@ -914,10 +900,7 @@ class SearchVector(SearchExpression):
914
900
exact: Optional flag to enforce exact matching.
915
901
filter: Optional filter expression to narrow candidate documents.
916
902
917
- Notes:
918
- * The vector field must be indexed as a vector type in Atlas Search.
919
- * Parameters like `num_candidates` and `exact` control search
920
- performance and accuracy trade-offs.
903
+ Reference: https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/
921
904
"""
922
905
923
906
def __init__ (
@@ -1005,7 +988,31 @@ class SearchScoreOption(Expression):
1005
988
"""Class to mutate scoring on a search operation"""
1006
989
1007
990
def __init__ (self , definitions = None ):
1008
- self .definitions = definitions
991
+ self ._definitions = definitions
992
+
993
+ def as_mql (self , compiler , connection ):
994
+ return self ._definitions
995
+
996
+
997
+ class SearchTextLookup (Lookup ):
998
+ lookup_name = "search"
999
+
1000
+ def __init__ (self , lhs , rhs ):
1001
+ super ().__init__ (lhs , rhs )
1002
+ self .lhs = SearchText (self .lhs , self .rhs )
1003
+ self .rhs = Value (0 )
1004
+
1005
+ def __str__ (self ):
1006
+ return f"SearchText({ self .lhs } , { self .rhs } )"
1007
+
1008
+ def __repr__ (self ):
1009
+ return f"SearchText({ self .lhs } , { self .rhs } )"
1009
1010
1010
1011
def as_mql (self , compiler , connection ):
1011
- return self .definitions
1012
+ lhs_mql = process_lhs (self , compiler , connection )
1013
+ value = process_rhs (self , compiler , connection )
1014
+ return {"$gte" : [lhs_mql , value ]}
1015
+
1016
+
1017
+ CharField .register_lookup (SearchTextLookup )
1018
+ TextField .register_lookup (SearchTextLookup )
0 commit comments