31
31
from .models import Article , Location , Writer
32
32
33
33
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 ):
35
35
start = monotonic ()
36
36
while monotonic () - start < timeout :
37
37
indexes = list (collection .list_search_indexes ())
@@ -42,7 +42,7 @@ def wait_until_index_ready(collection, index_name, timeout: float = 30, interval
42
42
raise TimeoutError (f"Index { index_name } not ready after { timeout } seconds" )
43
43
44
44
45
- def _delayed_assertion (timeout : float = 120 , interval : float = 0.5 ):
45
+ def _delayed_assertion (timeout : float = 4 , interval : float = 0.5 ):
46
46
def decorator (assert_func ):
47
47
@wraps (assert_func )
48
48
def wrapper (self , fetch , * args , ** kwargs ):
@@ -72,6 +72,14 @@ def wrapper(self, fetch, *args, **kwargs):
72
72
class SearchUtilsMixin (TransactionTestCase ):
73
73
available_apps = None
74
74
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
+ """
75
83
assertCountEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertCountEqual )
76
84
assertListEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertListEqual )
77
85
assertQuerySetEqual = _delayed_assertion (timeout = 2 )(TransactionTestCase .assertQuerySetEqual )
@@ -153,6 +161,11 @@ def test_function_score(self):
153
161
scored = qs .first ()
154
162
self .assertAlmostEqual (scored .score , 1.0 , places = 2 )
155
163
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
+
156
169
157
170
class SearchAutocompleteTests (SearchUtilsMixin ):
158
171
@classmethod
@@ -232,6 +245,15 @@ def test_constant_score(self):
232
245
scored = qs .first ()
233
246
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
234
247
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
+
235
257
236
258
class SearchExistsTests (SearchUtilsMixin ):
237
259
@classmethod
@@ -257,6 +279,11 @@ def test_constant_score(self):
257
279
scored = qs .first ()
258
280
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
259
281
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
+
260
287
261
288
class SearchInTests (SearchUtilsMixin ):
262
289
@classmethod
@@ -285,6 +312,13 @@ def test_constant_score(self):
285
312
scored = qs .first ()
286
313
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
287
314
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
+
288
322
289
323
class SearchPhraseTests (SearchUtilsMixin ):
290
324
@classmethod
@@ -315,6 +349,15 @@ def test_constant_score(self):
315
349
scored = qs .first ()
316
350
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
317
351
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
+
318
361
319
362
class SearchRangeTests (SearchUtilsMixin ):
320
363
@classmethod
@@ -343,6 +386,14 @@ def test_constant_score(self):
343
386
scored = qs .first ()
344
387
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
345
388
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
+
346
397
347
398
class SearchRegexTests (SearchUtilsMixin ):
348
399
@classmethod
@@ -380,6 +431,15 @@ def test_constant_score(self):
380
431
scored = qs .first ()
381
432
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
382
433
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
+
383
443
384
444
class SearchTextTests (SearchUtilsMixin ):
385
445
@classmethod
@@ -428,6 +488,21 @@ def test_constant_score(self):
428
488
scored = qs .first ()
429
489
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
430
490
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
+
431
506
432
507
class SearchWildcardTests (SearchUtilsMixin ):
433
508
@classmethod
@@ -461,6 +536,15 @@ def test_constant_score(self):
461
536
scored = qs .first ()
462
537
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
463
538
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
+
464
548
465
549
class SearchGeoShapeTests (SearchUtilsMixin ):
466
550
@classmethod
@@ -513,6 +597,20 @@ def test_constant_score(self):
513
597
scored = qs .first ()
514
598
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
515
599
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
+
516
614
517
615
class SearchGeoWithinTests (SearchUtilsMixin ):
518
616
@classmethod
@@ -567,8 +665,29 @@ def test_constant_score(self):
567
665
scored = qs .first ()
568
666
self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
569
667
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
+
570
687
571
- @unittest .expectedFailure
688
+ @unittest .expectedFailure (
689
+ "Cannot find a match for the provided reference documents in Atlas Search"
690
+ )
572
691
class SearchMoreLikeThisTests (SearchUtilsMixin ):
573
692
@classmethod
574
693
def setUpClass (cls ):
@@ -770,6 +889,26 @@ def test_search_and_filter(self):
770
889
qs = Article .objects .filter (headline__search = "space exploration" , number__gt = 2 )
771
890
self .assertCountEqual (qs .all , [self .icy_moons ])
772
891
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
+
773
912
774
913
class SearchVectorTests (SearchUtilsMixin ):
775
914
@classmethod
@@ -816,3 +955,17 @@ def test_vector_search(self):
816
955
)
817
956
qs = Article .objects .annotate (score = expr ).order_by ("-score" )
818
957
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