Repository: encode/django-rest-framework

Issue:
3.15 backward compatibility issue with 3.14 - `rest_framework.filters.SearchFilter.get_search_terms` returns `str` instead of `list`
### Discussed in https://github.com/encode/django-rest-framework/discussions/9307

<div type='discussions-op-text'>

<sup>Originally posted by **freenoth** March 19, 2024</sup>
in 3.14 the `get_search_terms` method of `rest_framework.filters.SearchFilter` class was returning a **list** of terms
![image](https://github.com/encode/django-rest-framework/assets/2629328/05f7b36c-1d06-461e-b7e6-5737b7c03e93)

so we were able to iterate the search terms directly
![image](https://github.com/encode/django-rest-framework/assets/2629328/5cc38777-058d-4be5-a237-fa04924f7b49)


in 3.15 the same method `get_search_terms` is returning a **string** representation
![image](https://github.com/encode/django-rest-framework/assets/2629328/6c27762a-64eb-42ef-80cc-a80cfcb8ea99)

and to convert the `str` to a `list` an additional function was introduced `rest_framework.filters.search_smart_split`:
![image](https://github.com/encode/django-rest-framework/assets/2629328/c6bbb17a-1463-421d-97bd-f41b3cdf7c16)

and now we have to use this function to iterate the terms explicitly
![image](https://github.com/encode/django-rest-framework/assets/2629328/f317bd30-948c-4f90-af8e-a73d6390c4f2)

So the changing of return value type (and the result itself, actually) can break some custom classes, that are using the basic functionality. Moreover it's hard to figure out what is wrong, because both `list` and `str` are iterable, so the implementation works, but with the wrong results.

Example:

I had a custom Search filter class based on `rest_framework.filters.SearchFilter` with overrided `filter_queryset` method, so I used the basic method to get the list of search terms
```python
        search_terms = self.get_search_terms(request)
```
and a simple iteration by search terms works well

```python
        for search_term in search_terms:
```

but after upgrading to 3.15, my iteration started working differently, because now `get_search_terms` is returning a string -> so it works as iteration by string, it's adding every symbol of this string as independent query, instead of one query


for input `?search=asd` it builds a query like that
```sql
WHERE ((UPPER("resource"."id"::text) LIKE UPPER(%a%) OR UPPER("resource"."name"::text) LIKE UPPER(%a%))
	AND (UPPER("resource"."id"::text) LIKE UPPER(%s%) OR UPPER("resource"."name"::text) LIKE UPPER(%s%)) 
	AND (UPPER("resource"."id"::text) LIKE UPPER(%d%) OR UPPER("resource"."name"::text) LIKE UPPER(%d%)))
```

instead of a single search query
```sql
WHERE (UPPER("resource"."id"::text) LIKE UPPER(%asd%) OR UPPER("resource"."name"::text) LIKE UPPER(%asd%))
```

so we have to add using a `rest_framework.filters.search_smart_split` function explicitly everywhere to iterate search terms in an old way
```python
        for search_term in search_smart_split(search_terms):
```

is there a chance to use `search_smart_split` inside the `rest_framework.filters.SearchFilter.get_search_terms` to not to break backward compatibility and method behavior?</div>


Discussion:
Suggest reverting #9017.
Okay, I can't automatically revert #9017 through the GH interface.
If anyone's up for creating the revert PR for that, would be appreciated.
I have to look into it

Resolving patch:
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -24,15 +24,19 @@ def search_smart_split(search_terms):
     """generator that first splits string by spaces, leaving quoted phrases together,
     then it splits non-quoted phrases by commas.
     """
+    split_terms = []
     for term in smart_split(search_terms):
         # trim commas to avoid bad matching for quoted phrases
         term = term.strip(',')
         if term.startswith(('"', "'")) and term[0] == term[-1]:
             # quoted phrases are kept together without any other split
-            yield unescape_string_literal(term)
+            split_terms.append(unescape_string_literal(term))
         else:
             # non-quoted tokens are split by comma, keeping only non-empty ones
-            yield from (sub_term.strip() for sub_term in term.split(',') if sub_term)
+            for sub_term in term.split(','):
+                if sub_term:
+                    split_terms.append(sub_term.strip())
+    return split_terms
 
 
 class BaseFilterBackend:
@@ -85,7 +89,8 @@ def get_search_terms(self, request):
         """
         value = request.query_params.get(self.search_param, '')
         field = CharField(trim_whitespace=False, allow_blank=True)
-        return field.run_validation(value)
+        cleaned_value = field.run_validation(value)
+        return search_smart_split(cleaned_value)
 
     def construct_search(self, field_name, queryset):
         lookup = self.lookup_prefixes.get(field_name[0])
@@ -163,7 +168,7 @@ def filter_queryset(self, request, queryset, view):
             reduce(
                 operator.or_,
                 (models.Q(**{orm_lookup: term}) for orm_lookup in orm_lookups)
-            ) for term in search_smart_split(search_terms)
+            ) for term in search_terms
         )
         queryset = queryset.filter(reduce(operator.and_, conditions))
 


Test patch: