20
20
SearchPhrase ,
21
21
SearchRange ,
22
22
SearchRegex ,
23
+ SearchScoreOption ,
23
24
SearchText ,
24
25
SearchVector ,
25
26
SearchWildcard ,
@@ -70,18 +71,16 @@ def inner_wait_loop(predicate: Callable):
70
71
class SearchUtilsMixin (TransactionTestCase ):
71
72
available_apps = []
72
73
73
- @staticmethod
74
- def _get_collection (model ):
74
+ def _get_collection (self , model ):
75
75
return connection .database .get_collection (model ._meta .db_table )
76
76
77
- @staticmethod
78
- def create_search_index (model , index_name , definition , type = "search" ):
79
- collection = SearchUtilsMixin ._get_collection (model )
77
+ def create_search_index (self , model , index_name , definition , type = "search" ):
78
+ collection = self ._get_collection (model )
80
79
idx = SearchIndexModel (definition = definition , name = index_name , type = type )
81
80
collection .create_search_index (idx )
82
81
83
82
def _tear_down (self , model ):
84
- collection = SearchUtilsMixin ._get_collection (model )
83
+ collection = self ._get_collection (model )
85
84
for search_indexes in collection .list_search_indexes ():
86
85
collection .drop_search_index (search_indexes ["name" ])
87
86
collection .delete_many ({})
@@ -95,7 +94,12 @@ def setUp(self):
95
94
self .create_search_index (
96
95
Article ,
97
96
"equals_headline_index" ,
98
- {"mappings" : {"dynamic" : False , "fields" : {"headline" : {"type" : "token" }}}},
97
+ {
98
+ "mappings" : {
99
+ "dynamic" : False ,
100
+ "fields" : {"headline" : {"type" : "token" }, "number" : {"type" : "number" }},
101
+ }
102
+ },
99
103
)
100
104
self .article = Article .objects .create (headline = "cross" , number = 1 , body = "body" )
101
105
Article .objects .create (headline = "other thing" , number = 2 , body = "body" )
@@ -108,6 +112,44 @@ def test_search_equals(self):
108
112
qs = Article .objects .annotate (score = SearchEquals (path = "headline" , value = "cross" ))
109
113
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
110
114
115
+ def test_boost_score (self ):
116
+ boost_score = SearchScoreOption ({"boost" : {"value" : 3 }})
117
+
118
+ qs = Article .objects .annotate (
119
+ score = SearchEquals (path = "headline" , value = "cross" , score = boost_score )
120
+ )
121
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
122
+ scored = qs .first ()
123
+ self .assertGreaterEqual (scored .score , 3.0 )
124
+
125
+ def test_constant_score (self ):
126
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
127
+ qs = Article .objects .annotate (
128
+ score = SearchEquals (path = "headline" , value = "cross" , score = constant_score )
129
+ )
130
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
131
+ scored = qs .first ()
132
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
133
+
134
+ def test_function_score (self ):
135
+ function_score = SearchScoreOption (
136
+ {
137
+ "function" : {
138
+ "path" : {
139
+ "value" : "number" ,
140
+ "undefined" : 0 ,
141
+ },
142
+ }
143
+ }
144
+ )
145
+
146
+ qs = Article .objects .annotate (
147
+ score = SearchEquals (path = "headline" , value = "cross" , score = function_score )
148
+ )
149
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
150
+ scored = qs .first ()
151
+ self .assertAlmostEqual (scored .score , 1.0 , places = 2 )
152
+
111
153
112
154
@skipUnlessDBFeature ("supports_atlas_search" )
113
155
class SearchAutocompleteTest (SearchUtilsMixin ):
@@ -173,6 +215,21 @@ def test_search_autocomplete_embedded_model(self):
173
215
)
174
216
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
175
217
218
+ def test_constant_score (self ):
219
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
220
+ qs = Article .objects .annotate (
221
+ score = SearchAutocomplete (
222
+ path = "headline" ,
223
+ query = "crossing" ,
224
+ token_order = "sequential" , # noqa: S106
225
+ fuzzy = {"maxEdits" : 2 },
226
+ score = constant_score ,
227
+ )
228
+ )
229
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
230
+ scored = qs .first ()
231
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
232
+
176
233
177
234
@skipUnlessDBFeature ("supports_atlas_search" )
178
235
class SearchExistsTest (SearchUtilsMixin ):
@@ -184,10 +241,21 @@ def setUp(self):
184
241
)
185
242
self .article = Article .objects .create (headline = "ignored" , number = 3 , body = "something" )
186
243
244
+ def tearDown (self ):
245
+ self ._tear_down (Article )
246
+ super ().tearDown ()
247
+
187
248
def test_search_exists (self ):
188
249
qs = Article .objects .annotate (score = SearchExists (path = "body" ))
189
250
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
190
251
252
+ def test_constant_score (self ):
253
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
254
+ qs = Article .objects .annotate (score = SearchExists (path = "body" , score = constant_score ))
255
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
256
+ scored = qs .first ()
257
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
258
+
191
259
192
260
@skipUnlessDBFeature ("supports_atlas_search" )
193
261
class SearchInTest (SearchUtilsMixin ):
@@ -208,6 +276,15 @@ def test_search_in(self):
208
276
qs = Article .objects .annotate (score = SearchIn (path = "headline" , value = ["cross" , "river" ]))
209
277
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
210
278
279
+ def test_constant_score (self ):
280
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
281
+ qs = Article .objects .annotate (
282
+ score = SearchIn (path = "headline" , value = ["cross" , "river" ], score = constant_score )
283
+ )
284
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
285
+ scored = qs .first ()
286
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
287
+
211
288
212
289
@skipUnlessDBFeature ("supports_atlas_search" )
213
290
class SearchPhraseTest (SearchUtilsMixin ):
@@ -230,6 +307,15 @@ def test_search_phrase(self):
230
307
qs = Article .objects .annotate (score = SearchPhrase (path = "body" , query = "quick brown" ))
231
308
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
232
309
310
+ def test_constant_score (self ):
311
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
312
+ qs = Article .objects .annotate (
313
+ score = SearchPhrase (path = "body" , query = "quick brown" , score = constant_score )
314
+ )
315
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
316
+ scored = qs .first ()
317
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
318
+
233
319
234
320
@skipUnlessDBFeature ("supports_atlas_search" )
235
321
class SearchRangeTest (SearchUtilsMixin ):
@@ -250,6 +336,15 @@ def test_search_range(self):
250
336
qs = Article .objects .annotate (score = SearchRange (path = "number" , gte = 10 , lt = 30 ))
251
337
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .number20 ]))
252
338
339
+ def test_constant_score (self ):
340
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
341
+ qs = Article .objects .annotate (
342
+ score = SearchRange (path = "number" , gte = 10 , lt = 30 , score = constant_score )
343
+ )
344
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .number20 ]))
345
+ scored = qs .first ()
346
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
347
+
253
348
254
349
@skipUnlessDBFeature ("supports_atlas_search" )
255
350
class SearchRegexTest (SearchUtilsMixin ):
@@ -277,6 +372,17 @@ def test_search_regex(self):
277
372
)
278
373
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
279
374
375
+ def test_constant_score (self ):
376
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
377
+ qs = Article .objects .annotate (
378
+ score = SearchRegex (
379
+ path = "headline" , query = "hello.*" , allow_analyzed_field = True , score = constant_score
380
+ )
381
+ )
382
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
383
+ scored = qs .first ()
384
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
385
+
280
386
281
387
@skipUnlessDBFeature ("supports_atlas_search" )
282
388
class SearchTextTest (SearchUtilsMixin ):
@@ -311,6 +417,21 @@ def test_search_text_with_fuzzy_and_criteria(self):
311
417
)
312
418
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
313
419
420
+ def test_constant_score (self ):
421
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
422
+ qs = Article .objects .annotate (
423
+ score = SearchText (
424
+ path = "body" ,
425
+ query = "lazzy" ,
426
+ fuzzy = {"maxEdits" : 2 },
427
+ match_criteria = "all" ,
428
+ score = constant_score ,
429
+ )
430
+ )
431
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
432
+ scored = qs .first ()
433
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
434
+
314
435
315
436
@skipUnlessDBFeature ("supports_atlas_search" )
316
437
class SearchWildcardTest (SearchUtilsMixin ):
@@ -336,6 +457,15 @@ def test_search_wildcard(self):
336
457
qs = Article .objects .annotate (score = SearchWildcard (path = "headline" , query = "dark-*" ))
337
458
self .wait_for_assertion (lambda : self .assertCountEqual ([self .article ], qs ))
338
459
460
+ def test_constant_score (self ):
461
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
462
+ qs = Article .objects .annotate (
463
+ score = SearchWildcard (path = "headline" , query = "dark-*" , score = constant_score )
464
+ )
465
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
466
+ scored = qs .first ()
467
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
468
+
339
469
340
470
@skipUnlessDBFeature ("supports_atlas_search" )
341
471
class SearchGeoShapeTest (SearchUtilsMixin ):
@@ -371,6 +501,21 @@ def test_search_geo_shape(self):
371
501
)
372
502
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
373
503
504
+ def test_constant_score (self ):
505
+ polygon = {
506
+ "type" : "Polygon" ,
507
+ "coordinates" : [[[30 , 0 ], [50 , 0 ], [50 , 10 ], [30 , 10 ], [30 , 0 ]]],
508
+ }
509
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
510
+ qs = Article .objects .annotate (
511
+ score = SearchGeoShape (
512
+ path = "location" , relation = "within" , geometry = polygon , score = constant_score
513
+ )
514
+ )
515
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
516
+ scored = qs .first ()
517
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
518
+
374
519
375
520
@skipUnlessDBFeature ("supports_atlas_search" )
376
521
class SearchGeoWithinTest (SearchUtilsMixin ):
@@ -405,6 +550,24 @@ def test_search_geo_within(self):
405
550
)
406
551
self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
407
552
553
+ def test_constant_score (self ):
554
+ polygon = {
555
+ "type" : "Polygon" ,
556
+ "coordinates" : [[[30 , 0 ], [50 , 0 ], [50 , 10 ], [30 , 10 ], [30 , 0 ]]],
557
+ }
558
+ constant_score = SearchScoreOption ({"constant" : {"value" : 10 }})
559
+ qs = Article .objects .annotate (
560
+ score = SearchGeoWithin (
561
+ path = "location" ,
562
+ kind = "geometry" ,
563
+ geo_object = polygon ,
564
+ score = constant_score ,
565
+ )
566
+ )
567
+ self .wait_for_assertion (lambda : self .assertCountEqual (qs .all (), [self .article ]))
568
+ scored = qs .first ()
569
+ self .assertAlmostEqual (scored .score , 10.0 , places = 2 )
570
+
408
571
409
572
@skipUnlessDBFeature ("supports_atlas_search" )
410
573
@unittest .expectedFailure
@@ -523,6 +686,48 @@ def test_operations(self):
523
686
lambda : self .assertCountEqual (qs .all (), [self .mars_mission , self .exoplanet ])
524
687
)
525
688
689
+ def test_mixed_scores (self ):
690
+ boost_score = SearchScoreOption ({"boost" : {"value" : 5 }})
691
+ constant_score = SearchScoreOption ({"constant" : {"value" : 20 }})
692
+ function_score = SearchScoreOption (
693
+ {"function" : {"path" : {"value" : "number" , "undefined" : 0 }}}
694
+ )
695
+
696
+ must_expr = SearchEquals (path = "headline" , value = "space exploration" , score = boost_score )
697
+ should_expr = SearchPhrase (path = "body" , query = "exoplanets" , score = constant_score )
698
+ must_not_expr = SearchPhrase (path = "body" , query = "icy moons" , score = function_score )
699
+
700
+ compound = CompoundExpression (
701
+ must = [must_expr ],
702
+ must_not = [must_not_expr ],
703
+ should = [should_expr ],
704
+ )
705
+ qs = Article .objects .annotate (score = compound ).order_by ("-score" )
706
+ self .wait_for_assertion (
707
+ lambda : self .assertListEqual (list (qs .all ()), [self .exoplanet , self .mars_mission ])
708
+ )
709
+ # Exoplanet should rank first because of the constant 20 bump.
710
+ self .assertEqual (qs .first (), self .exoplanet )
711
+
712
+ def test_operationss_with_function_score (self ):
713
+ function_score = SearchScoreOption (
714
+ {"function" : {"path" : {"value" : "number" , "undefined" : 0 }}}
715
+ )
716
+
717
+ expr = SearchEquals (
718
+ path = "headline" ,
719
+ value = "space exploration" ,
720
+ score = function_score ,
721
+ ) & ~ SearchEquals (path = "number" , value = 3 )
722
+
723
+ qs = Article .objects .annotate (score = expr ).order_by ("-score" )
724
+
725
+ self .wait_for_assertion (
726
+ lambda : self .assertListEqual (list (qs ), [self .exoplanet , self .mars_mission ])
727
+ )
728
+ # Returns mars_mission (score≈1) and exoplanet (score≈2) then; exoplanet first.
729
+ self .assertEqual (qs .first (), self .exoplanet )
730
+
526
731
def test_multiple_search (self ):
527
732
msg = (
528
733
"Only one $search operation is allowed per query. Received 2 search expressions. "
0 commit comments