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

Code before:
        def construct_search(field_name):
            if field_name.startswith("^"):
                return "%s__istartswith" % field_name.removeprefix("^"), None
            elif field_name.startswith("="):
                return "%s__iexact" % field_name.removeprefix("="), None
            elif field_name.startswith("@"):
                return "%s__search" % field_name.removeprefix("@"), None
            # Use field_name if it includes a lookup.
            opts = queryset.model._meta
            lookup_fields = field_name.split(LOOKUP_SEP)
            # Go through the fields, following all relations.
            prev_field = None
            for i, path_part in enumerate(lookup_fields):
                if path_part == "pk":
                    path_part = opts.pk.name
                try:
                    field = opts.get_field(path_part)
                except FieldDoesNotExist:
                    # Use valid query lookups.
                    if prev_field and prev_field.get_lookup(path_part):
                        if (
                            path_part == "exact"
                            and not isinstance(
                                prev_field, (models.CharField, models.TextField)
                            )
                            and not prev_field.primary_key
                        ):
                            field_name_without_exact = "__".join(lookup_fields[:i])
                            alias = Cast(
                                field_name_without_exact,
                                output_field=models.CharField(),
                            )
                            alias_name = "_".join(lookup_fields[:i])
                            return f"{alias_name}_str", alias
                        else:
                            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:
            str_aliases = {}
            orm_lookups = []
            for field in search_fields:
                lookup, str_alias = construct_search(str(field))

Review comment (suggestion):
I think the problem has little to do with whether or not a primary key is targeted. The cast will be harmful in all case where the field is indexed for example.

A better solution here would likely be to attempt `prev_field.to_python(...)` with the search term an in cases where the value is valid (no exception is raised) then don't perform a `Cast`.

In other words, `prev_field.to_python("123")` (assuming it's an `IntegerField` instance) passes and returns `123` so we know that `field__exact="123"` doesn't require a cast while `prev_field.to_python("foo")` raises a `ValueError` so we know we must cast.

Code after:
        # Apply keyword searches.
        def construct_search(field_name):
            """
            Return a tuple of (lookup, field_to_validate).

            field_to_validate is set for non-text exact lookups so that
            invalid search terms can be skipped (preserving index usage).
            """
            if field_name.startswith("^"):
                return "%s__istartswith" % field_name.removeprefix("^"), None
            elif field_name.startswith("="):
                return "%s__iexact" % field_name.removeprefix("="), None
            elif field_name.startswith("@"):
                return "%s__search" % field_name.removeprefix("@"), None
            # Use field_name if it includes a lookup.
            opts = queryset.model._meta
            lookup_fields = field_name.split(LOOKUP_SEP)
            # Go through the fields, following all relations.
            prev_field = None
            for path_part in lookup_fields:
                if path_part == "pk":
                    path_part = opts.pk.name
                try:
                    field = opts.get_field(path_part)
                except FieldDoesNotExist:
                    # Use valid query lookups.
                    if prev_field and prev_field.get_lookup(path_part):
                        if path_part == "exact" and not isinstance(
                            prev_field, (models.CharField, models.TextField)
                        ):
                            # Use prev_field to validate the search term.
                            return field_name, prev_field
                        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]: