Repository: django/django
File: django/contrib/admin/options.py (Python)

Code before:
                else:
                    prev_field = field
                    if hasattr(field, "path_infos"):
                        # Update opts to follow the relation.
                        opts = field.path_infos[-1].to_opts
            # Otherwise, use the field with icontains.
            return "%s__icontains" % field_name, None

        may_have_duplicates = False
        search_fields = self.get_search_fields(request)
        if search_fields and search_term:
            orm_lookups = []
            for field in search_fields:
                orm_lookups.append(construct_search(str(field)))

            term_queries = []
            for bit in smart_split(search_term):
                if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
                    bit = unescape_string_literal(bit)
                # Build lookups for this term, skipping invalid field/value
                # combinations to preserve index usage.
                bit_lookups = []
                for orm_lookup, validate_field in orm_lookups:
                    if validate_field is not None:
                        try:
                            bit = validate_field.to_python(bit)
                        except ValidationError:
                            # Skip this lookup for invalid values.
                            continue
                    bit_lookups.append((orm_lookup, bit))
                if bit_lookups:
                    or_queries = models.Q.create(bit_lookups, connector=models.Q.OR)
                    term_queries.append(or_queries)
                else:
                    # No valid lookups for this term - it matches nothing,
                    # therefore add a filter that returns nothing.
                    term_queries.append(models.Q(pk__in=[]))
            if term_queries:
                queryset = queryset.filter(models.Q.create(term_queries))
            may_have_duplicates |= any(
                lookup_spawns_duplicates(self.opts, search_spec)
                for search_spec, _ in orm_lookups
            )
        return queryset, may_have_duplicates

    def get_preserved_filters(self, request):
        """
        Return the preserved filters querystring.
        """
        match = request.resolver_match
        if self.preserve_filters and match:

Review comment (bug):
Did you give some thought to Simon's suggestion to use `.formfield()`? I expect some behavior differences, e.g.:

```py
In [1]: from django.db.models.fields import BooleanField

In [2]: f = BooleanField()

In [3]: f.to_python('false')
---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
Cell In[3], line 1
----> 1 f.to_python('false')

File ~/django/django/db/models/fields/__init__.py:1179, in BooleanField.to_python(self, value)
   1177 if value in ("f", "False", "0"):
   1178     return False
-> 1179 raise exceptions.ValidationError(
   1180     self.error_messages["invalid_nullable" if self.null else "invalid"],
   1181     code="invalid",
   1182     params={"value": value},
   1183 )

ValidationError: ['“false” value must be either True or False.']

In [4]: ff = f.formfield()

In [5]: ff.to_python('false')
Out[5]: False

In [6]: ff.to_python('garbage')
Out[6]: True
```

Can you look into this and add a test that will fail if the model field is used instead of the form field?

Code after:
                        return field_name, None
                else:
                    prev_field = field
                    if hasattr(field, "path_infos"):
                        # Update opts to follow the relation.
                        opts = field.path_infos[-1].to_opts
            # Otherwise, use the field with icontains.
            return "%s__icontains" % field_name, None

        may_have_duplicates = False
        search_fields = self.get_search_fields(request)
        if search_fields and search_term:
            orm_lookups = []
            for field in search_fields:
                orm_lookups.append(construct_search(str(field)))

            term_queries = []
            for bit in smart_split(search_term):
                if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
                    bit = unescape_string_literal(bit)
                # Build term lookups, skipping values invalid for their field.
                bit_lookups = []
                for orm_lookup, validate_field in orm_lookups:
                    if validate_field is not None:
                        formfield = validate_field.formfield()
                        try:
                            if formfield is not None:
                                value = formfield.to_python(bit)
                            else:
                                # Fields like AutoField lack a form field.
                                value = validate_field.to_python(bit)
                        except ValidationError:
                            # Skip this lookup for invalid values.
                            continue
                    else:
                        value = bit
                    bit_lookups.append((orm_lookup, value))
                if bit_lookups:
                    or_queries = models.Q.create(bit_lookups, connector=models.Q.OR)
                    term_queries.append(or_queries)
                else:
                    # No valid lookups: add a filter that returns nothing.
                    term_queries.append(models.Q(pk__in=[]))
            if term_queries:
                queryset = queryset.filter(models.Q.create(term_queries))
            may_have_duplicates |= any(
                lookup_spawns_duplicates(self.opts, search_spec)
                for search_spec, _ in orm_lookups
            )
        return queryset, may_have_duplicates