3131from .models import Article , Location , Writer
3232
3333
34- def wait_until_index_ready (collection , index_name , timeout : float = 30 , interval : float = 0.5 ):
34+ def wait_until_index_ready (collection , index_name , timeout : float = 5 , interval : float = 0.5 ):
3535 start = monotonic ()
3636 while monotonic () - start < timeout :
3737 indexes = list (collection .list_search_indexes ())
@@ -42,7 +42,7 @@ def wait_until_index_ready(collection, index_name, timeout: float = 30, interval
4242 raise TimeoutError (f"Index { index_name } not ready after { timeout } seconds" )
4343
4444
45- def _delayed_assertion (timeout : float = 120 , interval : float = 0.5 ):
45+ def _delayed_assertion (timeout : float = 4 , interval : float = 0.5 ):
4646 def decorator (assert_func ):
4747 @wraps (assert_func )
4848 def wrapper (self , fetch , * args , ** kwargs ):
@@ -72,6 +72,14 @@ def wrapper(self, fetch, *args, **kwargs):
7272class SearchUtilsMixin (TransactionTestCase ):
7373 available_apps = None
7474
75+ """
76+ These assertions include a small delay to account for MongoDB Atlas Search's
77+ eventual consistency and indexing latency. Data inserted into MongoDB is not
78+ immediately available for $search queries because Atlas Search indexes are
79+ updated asynchronously via change streams. While this is usually fast, delays
80+ can occur due to replication lag, system load, index complexity, or a high
81+ number of search indexes.
82+ """
7583 assertCountEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertCountEqual )
7684 assertListEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertListEqual )
7785 assertQuerySetEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertQuerySetEqual )
@@ -153,6 +161,11 @@ def test_function_score(self):
153161 scored = qs .first ()
154162 self .assertAlmostEqual (scored .score , 1.0 , places = 2 )
155163
164+ def test_str_returns_expected_format (self ):
165+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
166+ se = SearchEquals (path = "headline" , value = "cross" , score = score )
167+ self .assertEqual (str (se ), f"<SearchEquals(path='headline', value='cross', score={ score } )>" )
168+
156169
157170class SearchAutocompleteTests (SearchUtilsMixin ):
158171 @classmethod
@@ -232,6 +245,15 @@ def test_constant_score(self):
232245 scored = qs .first ()
233246 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
234247
248+ def test_str_returns_expected_format (self ):
249+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
250+ se = SearchAutocomplete (path = "writer__name" , query = "Joselina" , score = score )
251+ self .assertEqual (
252+ str (se ),
253+ "<SearchAutocomplete(path='writer__name', query='Joselina', fuzzy=None,"
254+ f" token_order=None, score={ score } )>" ,
255+ )
256+
235257
236258class SearchExistsTests (SearchUtilsMixin ):
237259 @classmethod
@@ -257,6 +279,11 @@ def test_constant_score(self):
257279 scored = qs .first ()
258280 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
259281
282+ def test_str_returns_expected_format (self ):
283+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
284+ se = SearchExists (path = "body" , score = score )
285+ self .assertEqual (str (se ), f"<SearchExists(path='body', score={ score } )>" )
286+
260287
261288class SearchInTests (SearchUtilsMixin ):
262289 @classmethod
@@ -285,6 +312,13 @@ def test_constant_score(self):
285312 scored = qs .first ()
286313 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
287314
315+ def test_str_returns_expected_format (self ):
316+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
317+ se = SearchIn (path = "headline" , value = ["cross" , "river" ], score = score )
318+ self .assertEqual (
319+ str (se ), f"<SearchIn(path='headline', value=('cross', 'river'), score={ score } )>"
320+ )
321+
288322
289323class SearchPhraseTests (SearchUtilsMixin ):
290324 @classmethod
@@ -315,6 +349,15 @@ def test_constant_score(self):
315349 scored = qs .first ()
316350 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
317351
352+ def test_str_returns_expected_format (self ):
353+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
354+ se = SearchPhrase (path = "body" , query = "quick brown" , score = score )
355+ self .assertEqual (
356+ str (se ),
357+ "<SearchPhrase(path='body', query='quick brown', slop=None, "
358+ f"synonyms=None, score={ score } )>" ,
359+ )
360+
318361
319362class SearchRangeTests (SearchUtilsMixin ):
320363 @classmethod
@@ -343,6 +386,14 @@ def test_constant_score(self):
343386 scored = qs .first ()
344387 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
345388
389+ def test_str_returns_expected_format (self ):
390+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
391+ se = SearchRange (path = "number" , gte = 10 , lt = 30 , score = score )
392+ self .assertEqual (
393+ str (se ),
394+ f"<SearchRange(path='number', lt=30, lte=None, gt=None, gte=10, score={ score } )>" ,
395+ )
396+
346397
347398class SearchRegexTests (SearchUtilsMixin ):
348399 @classmethod
@@ -380,6 +431,15 @@ def test_constant_score(self):
380431 scored = qs .first ()
381432 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
382433
434+ def test_str_returns_expected_format (self ):
435+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
436+ se = SearchRegex (path = "headline" , query = "hello.*" , allow_analyzed_field = True , score = score )
437+ self .assertEqual (
438+ str (se ),
439+ "<SearchRegex(path='headline', query='hello.*', "
440+ f"allow_analyzed_field=True, score={ score } )>" ,
441+ )
442+
383443
384444class SearchTextTests (SearchUtilsMixin ):
385445 @classmethod
@@ -428,6 +488,21 @@ def test_constant_score(self):
428488 scored = qs .first ()
429489 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
430490
491+ def test_str_returns_expected_format (self ):
492+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
493+ se = SearchText (
494+ path = "body" ,
495+ query = "lazzy" ,
496+ fuzzy = {"maxEdits" : 2 },
497+ match_criteria = "all" ,
498+ score = score ,
499+ )
500+ self .assertEqual (
501+ str (se ),
502+ "<SearchText(path='body', query='lazzy', fuzzy=(('maxEdits', 2),), "
503+ f"match_criteria='all', synonyms=None, score={ score } )>" ,
504+ )
505+
431506
432507class SearchWildcardTests (SearchUtilsMixin ):
433508 @classmethod
@@ -461,6 +536,15 @@ def test_constant_score(self):
461536 scored = qs .first ()
462537 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
463538
539+ def test_str_returns_expected_format (self ):
540+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
541+ se = SearchWildcard (path = "headline" , query = "dark-*" , score = score )
542+ self .assertEqual (
543+ str (se ),
544+ "<SearchWildcard(path='headline', query='dark-*', "
545+ f"allow_analyzed_field=None, score={ score } )>" ,
546+ )
547+
464548
465549class SearchGeoShapeTests (SearchUtilsMixin ):
466550 @classmethod
@@ -513,6 +597,20 @@ def test_constant_score(self):
513597 scored = qs .first ()
514598 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
515599
600+ def test_str_returns_expected_format (self ):
601+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
602+ polygon = {
603+ "type" : "Polygon" ,
604+ "coordinates" : [[[30 , 0 ], [50 , 0 ], [50 , 10 ], [30 , 10 ], [30 , 0 ]]],
605+ }
606+ se = SearchGeoShape (path = "location" , relation = "within" , geometry = polygon , score = score )
607+ self .assertEqual (
608+ str (se ),
609+ "<SearchGeoShape(path='location', relation='within', geometry=(('type', 'Polygon'), "
610+ "('coordinates', (((30, 0), (50, 0), (50, 10), (30, 10), (30, 0)),))), "
611+ f"score={ score } )>" ,
612+ )
613+
516614
517615class SearchGeoWithinTests (SearchUtilsMixin ):
518616 @classmethod
@@ -567,8 +665,29 @@ def test_constant_score(self):
567665 scored = qs .first ()
568666 self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
569667
668+ def test_str_returns_expected_format (self ):
669+ score = SearchScoreOption ({"constant" : {"value" : 10 }})
670+ polygon = {
671+ "type" : "Polygon" ,
672+ "coordinates" : [[[30 , 0 ], [50 , 0 ], [50 , 10 ], [30 , 10 ], [30 , 0 ]]],
673+ }
674+ se = SearchGeoWithin (
675+ path = "location" ,
676+ kind = "geometry" ,
677+ geometry = polygon ,
678+ score = score ,
679+ )
680+ self .assertEqual (
681+ str (se ),
682+ "<SearchGeoWithin(path='location', kind='geometry', geometry=(('type', 'Polygon'), "
683+ "('coordinates', (((30, 0), (50, 0), (50, 10), (30, 10), (30, 0)),))), "
684+ f"score={ score } )>" ,
685+ )
686+
570687
571- @unittest .expectedFailure
688+ @unittest .expectedFailure (
689+ "Cannot find a match for the provided reference documents in Atlas Search"
690+ )
572691class SearchMoreLikeThisTests (SearchUtilsMixin ):
573692 @classmethod
574693 def setUpClass (cls ):
@@ -770,6 +889,26 @@ def test_search_and_filter(self):
770889 qs = Article .objects .filter (headline__search = "space exploration" , number__gt = 2 )
771890 self .assertCountEqual (qs .all , [self .icy_moons ])
772891
892+ def test_str_returns_expected_format (self ):
893+ must_expr = SearchEquals (path = "headline" , value = "space exploration" )
894+ must_not_expr = SearchPhrase (path = "body" , query = "icy moons" )
895+ should_expr = SearchPhrase (path = "body" , query = "exoplanets" )
896+
897+ se = CompoundExpression (
898+ must = [must_expr or should_expr ],
899+ must_not = [must_not_expr ],
900+ should = [should_expr ],
901+ minimum_should_match = 1 ,
902+ )
903+ self .assertEqual (
904+ str (se ),
905+ "<CompoundExpression(must=(<SearchEquals(path='headline', value='space exploration', "
906+ "score=None)>,), must_not=(<SearchPhrase(path='body', query='icy moons', slop=None, "
907+ "synonyms=None, score=None)>,), should=(<SearchPhrase(path='body', "
908+ "query='exoplanets', slop=None, synonyms=None, score=None)>,), "
909+ "filter=None, score=None, minimum_should_match=1)>" ,
910+ )
911+
773912
774913class SearchVectorTests (SearchUtilsMixin ):
775914 @classmethod
@@ -816,3 +955,17 @@ def test_vector_search(self):
816955 )
817956 qs = Article .objects .annotate (score = expr ).order_by ("-score" )
818957 self .assertCountEqual (qs .all , [self .mars , self .cooking ])
958+
959+ def test_str_returns_expected_format (self ):
960+ vector_query = [0.1 , 0.2 , 0.3 ]
961+ se = SearchVector (
962+ path = "plot_embedding" ,
963+ query_vector = vector_query ,
964+ num_candidates = 5 ,
965+ limit = 2 ,
966+ )
967+ self .assertEqual (
968+ str (se ),
969+ "<SearchVector(path='plot_embedding', query_vector=(0.1, 0.2, 0.3), limit=2, "
970+ "num_candidates=5, exact=None, filter=None)>" ,
971+ )
0 commit comments