11from django .db import NotSupportedError
2- from django .db .models import Expression , FloatField
2+ from django .db .models import CharField , Expression , FloatField , TextField
33from 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
47
58
69def cast_as_value (value ):
@@ -68,7 +71,7 @@ def __or__(self, other):
6871 return self ._combine (other , Operator (Operator .OR ))
6972
7073 def __ror__ (self , other ):
71- return self ._combine (self , Operator (Operator .OR ), other )
74+ return self ._combine (other , Operator (Operator .OR ))
7275
7376
7477class SearchExpression (SearchCombinable , Expression ):
@@ -86,22 +89,23 @@ def __str__(self):
8689 cls = self .identity [0 ]
8790 kwargs = dict (self .identity [1 :])
8891 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 } )> "
9093
9194 def __repr__ (self ):
9295 return str (self )
9396
9497 def as_sql (self , compiler , connection ):
9598 return "" , []
9699
97- def get_source_expressions (self ):
98- return []
99-
100100 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 } "
105109
106110 def _get_query_index (self , fields , compiler ):
107111 fields = set (fields )
@@ -139,9 +143,7 @@ class SearchAutocomplete(SearchExpression):
139143 any-order token matching.
140144 score: Optional expression to adjust score relevance (e.g., `{"boost": {"value": 5}}`).
141145
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/
145147 """
146148
147149 def __init__ (self , path , query , fuzzy = None , token_order = None , score = None ):
@@ -190,10 +192,7 @@ class SearchEquals(SearchExpression):
190192 value: The exact value to match against.
191193 score: Optional expression to modify the relevance score.
192194
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/
197196 """
198197
199198 def __init__ (self , path , value , score = None ):
@@ -236,9 +235,7 @@ class SearchExists(SearchExpression):
236235 path: The document path to check (as string or expression).
237236 score: Optional expression to modify the relevance score.
238237
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/
242239 """
243240
244241 def __init__ (self , path , score = None ):
@@ -260,11 +257,28 @@ def search_operator(self, compiler, connection):
260257 "path" : self .path .as_mql (compiler , connection , as_path = True ),
261258 }
262259 if self .score is not None :
263- params ["score" ] = self .score .definitions
260+ params ["score" ] = self .score .as_mql ( compiler , connection )
264261 return {"exists" : params }
265262
266263
267264class 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+
268282 def __init__ (self , path , value , score = None ):
269283 self .path = cast_as_field (path )
270284 self .value = cast_as_value (value )
@@ -294,7 +308,7 @@ class SearchPhrase(SearchExpression):
294308 """
295309 Atlas Search expression that matches a phrase in the specified field.
296310
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
298312 sequences of terms. It supports optional slop (word distance) and synonym sets.
299313
300314 Example:
@@ -307,10 +321,7 @@ class SearchPhrase(SearchExpression):
307321 synonyms: Optional name of a synonym mapping defined in the Atlas index.
308322 score: Optional expression to modify the relevance score.
309323
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/
314325 """
315326
316327 def __init__ (self , path , query , slop = None , synonyms = None , score = None ):
@@ -360,9 +371,7 @@ class SearchQueryString(SearchExpression):
360371 query: The Lucene-style query string.
361372 score: Optional expression to modify the relevance score.
362373
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/
366375 """
367376
368377 def __init__ (self , path , query , score = None ):
@@ -408,9 +417,7 @@ class SearchRange(SearchExpression):
408417 gte: Optional inclusive lower bound (`>=`).
409418 score: Optional expression to modify the relevance score.
410419
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/
414421 """
415422
416423 def __init__ (self , path , lt = None , lte = None , gt = None , gte = None , score = None ):
@@ -464,10 +471,7 @@ class SearchRegex(SearchExpression):
464471 allow_analyzed_field: Whether to allow matching against analyzed fields (default is False).
465472 score: Optional expression to modify the relevance score.
466473
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/
471475 """
472476
473477 def __init__ (self , path , query , allow_analyzed_field = None , score = None ):
@@ -516,9 +520,7 @@ class SearchText(SearchExpression):
516520 synonyms: Optional name of a synonym mapping defined in the Atlas index.
517521 score: Optional expression to adjust relevance scoring.
518522
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/
522524 """
523525
524526 def __init__ (self , path , query , fuzzy = None , match_criteria = None , synonyms = None , score = None ):
@@ -571,11 +573,7 @@ class SearchWildcard(SearchExpression):
571573 allow_analyzed_field: Whether to allow matching against analyzed fields (default is False).
572574 score: Optional expression to modify the relevance score.
573575
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/
579577 """
580578
581579 def __init__ (self , path , query , allow_analyzed_field = None , score = None ):
@@ -600,7 +598,7 @@ def search_operator(self, compiler, connection):
600598 "query" : self .query .value ,
601599 }
602600 if self .score :
603- params ["score" ] = self .score .query . as_mql (compiler , connection )
601+ params ["score" ] = self .score .as_mql (compiler , connection )
604602 if self .allow_analyzed_field is not None :
605603 params ["allowAnalyzedField" ] = self .allow_analyzed_field .value
606604 return {"wildcard" : params }
@@ -622,9 +620,7 @@ class SearchGeoShape(SearchExpression):
622620 geometry: The GeoJSON geometry to compare against.
623621 score: Optional expression to modify the relevance score.
624622
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/
628624 """
629625
630626 def __init__ (self , path , relation , geometry , score = None ):
@@ -671,9 +667,7 @@ class SearchGeoWithin(SearchExpression):
671667 geo_object: The GeoJSON geometry defining the boundary.
672668 score: Optional expression to adjust the relevance score.
673669
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/
677671 """
678672
679673 def __init__ (self , path , kind , geo_object , score = None ):
@@ -716,9 +710,7 @@ class SearchMoreLikeThis(SearchExpression):
716710 documents: A list of example documents or expressions to find similar documents.
717711 score: Optional expression to modify the relevance scoring.
718712
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/
722714 """
723715
724716 def __init__ (self , documents , score = None ):
@@ -771,9 +763,7 @@ class CompoundExpression(SearchExpression):
771763 score: Optional expression to adjust scoring.
772764 minimum_should_match: Minimum number of `should` clauses that must match.
773765
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/
777767 """
778768
779769 def __init__ (
@@ -856,10 +846,6 @@ class CombinedSearchExpression(SearchExpression):
856846 lhs: The left-hand search expression.
857847 operator: The boolean operator as a string (e.g., "and", "or", "not").
858848 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.
863849 """
864850
865851 def __init__ (self , lhs , operator , rhs ):
@@ -914,10 +900,7 @@ class SearchVector(SearchExpression):
914900 exact: Optional flag to enforce exact matching.
915901 filter: Optional filter expression to narrow candidate documents.
916902
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/
921904 """
922905
923906 def __init__ (
@@ -1005,7 +988,31 @@ class SearchScoreOption(Expression):
1005988 """Class to mutate scoring on a search operation"""
1006989
1007990 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 } )"
10091010
10101011 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