@@ -311,6 +311,109 @@ class SearchListView(generics.ListAPIView):
311311 ]
312312
313313
314+ @pytest .mark .requires_postgres
315+ class SearchFilterFullTextTests (TestCase ):
316+ @classmethod
317+ def setUpTestData (cls ):
318+ SearchFilterModel .objects .create (title = 'The quick brown fox' , text = 'jumps over the lazy dog' )
319+ SearchFilterModel .objects .create (title = 'The slow brown turtle' , text = 'crawls under the fence' )
320+ SearchFilterModel .objects .create (title = 'A bright sunny day' , text = 'in the park with friends' )
321+
322+ def test_full_text_search_single_term (self ):
323+ class SearchListView (generics .ListAPIView ):
324+ queryset = SearchFilterModel .objects .all ()
325+ serializer_class = SearchFilterSerializer
326+ filter_backends = (filters .SearchFilter ,)
327+ search_fields = ('@title' ,)
328+
329+ view = SearchListView .as_view ()
330+ request = factory .get ('/' , {'search' : 'fox' })
331+ response = view (request )
332+ assert len (response .data ) == 1
333+ assert response .data [0 ]['title' ] == 'The quick brown fox'
334+
335+ def test_full_text_search_multiple_results (self ):
336+ class SearchListView (generics .ListAPIView ):
337+ queryset = SearchFilterModel .objects .all ()
338+ serializer_class = SearchFilterSerializer
339+ filter_backends = (filters .SearchFilter ,)
340+ search_fields = ('@title' ,)
341+
342+ view = SearchListView .as_view ()
343+ request = factory .get ('/' , {'search' : 'brown' })
344+ response = view (request )
345+ assert len (response .data ) == 2
346+ titles = {item ['title' ] for item in response .data }
347+ assert titles == {'The quick brown fox' , 'The slow brown turtle' }
348+
349+ def test_full_text_search_no_results (self ):
350+ class SearchListView (generics .ListAPIView ):
351+ queryset = SearchFilterModel .objects .all ()
352+ serializer_class = SearchFilterSerializer
353+ filter_backends = (filters .SearchFilter ,)
354+ search_fields = ('@title' ,)
355+
356+ view = SearchListView .as_view ()
357+ request = factory .get ('/' , {'search' : 'elephant' })
358+ response = view (request )
359+ assert len (response .data ) == 0
360+
361+ def test_full_text_search_multiple_fields (self ):
362+ class SearchListView (generics .ListAPIView ):
363+ queryset = SearchFilterModel .objects .all ()
364+ serializer_class = SearchFilterSerializer
365+ filter_backends = (filters .SearchFilter ,)
366+ search_fields = ('@title' , '@text' )
367+
368+ view = SearchListView .as_view ()
369+ request = factory .get ('/' , {'search' : 'lazy' })
370+ response = view (request )
371+ assert len (response .data ) == 1
372+ assert response .data [0 ]['title' ] == 'The quick brown fox'
373+
374+ def test_full_text_search_stemming (self ):
375+ """Full text search should match stemmed words (e.g. 'jumping' matches 'jumps')."""
376+ class SearchListView (generics .ListAPIView ):
377+ queryset = SearchFilterModel .objects .all ()
378+ serializer_class = SearchFilterSerializer
379+ filter_backends = (filters .SearchFilter ,)
380+ search_fields = ('@text' ,)
381+
382+ view = SearchListView .as_view ()
383+ request = factory .get ('/' , {'search' : 'jumping' })
384+ response = view (request )
385+ assert len (response .data ) == 1
386+ assert response .data [0 ]['text' ] == 'jumps over the lazy dog'
387+
388+ def test_full_text_search_multiple_terms (self ):
389+ """Each search term must match (AND semantics across terms)."""
390+ class SearchListView (generics .ListAPIView ):
391+ queryset = SearchFilterModel .objects .all ()
392+ serializer_class = SearchFilterSerializer
393+ filter_backends = (filters .SearchFilter ,)
394+ search_fields = ('@title' , '@text' )
395+
396+ view = SearchListView .as_view ()
397+ request = factory .get ('/' , {'search' : 'brown lazy' })
398+ response = view (request )
399+ assert len (response .data ) == 1
400+ assert response .data [0 ]['title' ] == 'The quick brown fox'
401+
402+ def test_full_text_search_mixed_with_icontains (self ):
403+ """Full text search fields can be mixed with regular icontains fields."""
404+ class SearchListView (generics .ListAPIView ):
405+ queryset = SearchFilterModel .objects .all ()
406+ serializer_class = SearchFilterSerializer
407+ filter_backends = (filters .SearchFilter ,)
408+ search_fields = ('@title' , 'text' )
409+
410+ view = SearchListView .as_view ()
411+ request = factory .get ('/' , {'search' : 'park' })
412+ response = view (request )
413+ assert len (response .data ) == 1
414+ assert response .data [0 ]['title' ] == 'A bright sunny day'
415+
416+
314417class AttributeModel (models .Model ):
315418 label = models .CharField (max_length = 32 )
316419
@@ -359,6 +462,10 @@ def test_custom_lookup_to_related_model(self):
359462 assert 'attribute__label__icontains' == filter_ .construct_search ('attribute__label' , SearchFilterModelFk ._meta )
360463 assert 'attribute__label__iendswith' == filter_ .construct_search ('attribute__label__iendswith' , SearchFilterModelFk ._meta )
361464
465+ def test_construct_search_with_at_prefix (self ):
466+ filter_ = filters .SearchFilter ()
467+ assert 'title__search' == filter_ .construct_search ('@title' , SearchFilterModelFk ._meta )
468+
362469
363470class SearchFilterModelM2M (models .Model ):
364471 title = models .CharField (max_length = 20 )
0 commit comments