Skip to main content

Overview

Text filters provide search input fields that allow users to filter querysets by text content. They support case-insensitive pattern matching and are ideal for searching through text fields.
Text filters use Django’s __icontains lookup by default, providing case-insensitive substring matching.

Available Text Filters

TextFilter

Custom text search filter using SimpleListFilter

FieldTextFilter

Automatic text search for CharField and TextField

TextFilter

Create custom text search filters with full control over the query logic.

Basic Usage

admin.py
from django.contrib import admin
from unfold.admin import ModelAdmin
from unfold.contrib.filters.admin import TextFilter

from .models import Product


class NameSearchFilter(TextFilter):
    title = "Name"
    parameter_name = "name_search"

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(name__icontains=self.value())
        return queryset


@admin.register(Product)
class ProductAdmin(ModelAdmin):
    list_display = ["name", "description", "sku"]
    list_filter = [NameSearchFilter]
Users can type any text to search. The filter applies automatically as the form is submitted.
Search across multiple fields:
admin.py
from django.db.models import Q


class MultiFieldSearchFilter(TextFilter):
    title = "Search"
    parameter_name = "search"

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(
                Q(name__icontains=self.value()) |
                Q(description__icontains=self.value()) |
                Q(sku__icontains=self.value())
            )
        return queryset


@admin.register(Product)
class ProductAdmin(ModelAdmin):
    list_filter = [MultiFieldSearchFilter]
Search through related models:
models.py
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Product(models.Model):
    name = models.CharField(max_length=200)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
admin.py
class CategoryNameFilter(TextFilter):
    title = "Category Name"
    parameter_name = "category_name"

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(
                category__name__icontains=self.value()
            )
        return queryset


@admin.register(Product)
class ProductAdmin(ModelAdmin):
    list_filter = [CategoryNameFilter]

FieldTextFilter

Automatically create text search filters for model fields.

Basic Usage

models.py
from django.db import models


class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    slug = models.SlugField(unique=True)
admin.py
from unfold.contrib.filters.admin import FieldTextFilter


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_display = ["title", "slug"]
    list_filter = [
        ("title", FieldTextFilter),
        ("slug", FieldTextFilter),
    ]
Use the tuple format (field_name, FilterClass) when applying filters to specific model fields.

How It Works

By default, FieldTextFilter uses __icontains lookup:
# User types: "django"
# Query becomes:
queryset.filter(title__icontains="django")

Custom Lookup

Override the lookup type:
admin.py
from unfold.contrib.filters.admin import FieldTextFilter


class ExactMatchFilter(FieldTextFilter):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Change from __icontains to __iexact
        self.lookup_kwarg = f"{self.field_path}__iexact"


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_filter = [
        ("slug", ExactMatchFilter),
    ]

Advanced Examples

admin.py
class CaseSensitiveFilter(TextFilter):
    title = "Name (exact case)"
    parameter_name = "name_exact"

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(name__contains=self.value())  # Case-sensitive
        return queryset
admin.py
class StartsWithFilter(TextFilter):
    title = "Name Starts With"
    parameter_name = "name_starts"

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(name__istartswith=self.value())
        return queryset
admin.py
class RegexFilter(TextFilter):
    title = "Regex Search"
    parameter_name = "regex"

    def queryset(self, request, queryset):
        if self.value():
            try:
                return queryset.filter(name__iregex=self.value())
            except Exception:
                # Invalid regex - return unfiltered
                return queryset
        return queryset

Full-Text Search (PostgreSQL)

admin.py
from django.contrib.postgres.search import SearchQuery, SearchVector


class FullTextSearchFilter(TextFilter):
    title = "Full-Text Search"
    parameter_name = "fulltext"

    def queryset(self, request, queryset):
        if self.value():
            vector = SearchVector('title', 'content')
            query = SearchQuery(self.value())
            return queryset.annotate(
                search=vector
            ).filter(search=query)
        return queryset


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_filter = [FullTextSearchFilter]
Full-text search requires PostgreSQL and the django.contrib.postgres app in INSTALLED_APPS.

Combining with Other Filters

admin.py
from unfold.contrib.filters.admin import (
    FieldTextFilter,
    RangeDateFilter,
    DropdownFilter,
)


class StatusFilter(DropdownFilter):
    title = "Status"
    parameter_name = "status"

    def lookups(self, request, model_admin):
        return (
            ("draft", "Draft"),
            ("published", "Published"),
        )

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(status=self.value())
        return queryset


@admin.register(Article)
class ArticleAdmin(ModelAdmin):
    list_filter = [
        ("title", FieldTextFilter),        # Text search
        StatusFilter,                       # Dropdown
        ("created_at", RangeDateFilter),   # Date range
    ]

Search Field Types

CharField

models.py
class Product(models.Model):
    name = models.CharField(max_length=200)
admin.py
list_filter = [
    ("name", FieldTextFilter),  # Works perfectly
]

TextField

models.py
class Article(models.Model):
    content = models.TextField()
admin.py
list_filter = [
    ("content", FieldTextFilter),  # Searches long text
]
Searching large TextField content can be slow. Consider adding database indexes or using full-text search.

EmailField

models.py
class User(models.Model):
    email = models.EmailField(unique=True)
admin.py
list_filter = [
    ("email", FieldTextFilter),
]

SlugField

models.py
class Article(models.Model):
    slug = models.SlugField(unique=True)
admin.py
list_filter = [
    ("slug", FieldTextFilter),
]

Performance Optimization

Add indexes for frequently searched fields:
models.py
class Product(models.Model):
    name = models.CharField(max_length=200, db_index=True)
    sku = models.CharField(max_length=50, db_index=True)
Use __istartswith instead of __icontains when possible:
admin.py
def queryset(self, request, queryset):
    if self.value():
        # Faster than __icontains
        return queryset.filter(name__istartswith=self.value())
    return queryset
For large text fields, use database-specific full-text indexes:
# PostgreSQL
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField

class Article(models.Model):
    search_vector = SearchVectorField(null=True)

    class Meta:
        indexes = [
            GinIndex(fields=['search_vector']),
        ]
Use select_related() and prefetch_related() for related field searches:
admin.py
@admin.register(Product)
class ProductAdmin(ModelAdmin):
    list_filter = [("category__name", FieldTextFilter)]

    def get_queryset(self, request):
        return super().get_queryset(request).select_related('category')

User Experience

Empty Value Handling

admin.py
class NameSearchFilter(TextFilter):
    title = "Name"
    parameter_name = "name"

    def queryset(self, request, queryset):
        value = self.value()
        
        # Handle empty strings and whitespace
        if value and value.strip():
            return queryset.filter(name__icontains=value.strip())
        
        return queryset

Minimum Length Requirement

admin.py
class MinLengthSearchFilter(TextFilter):
    title = "Search (min 3 chars)"
    parameter_name = "search"
    MIN_LENGTH = 3

    def queryset(self, request, queryset):
        value = self.value()
        
        if value and len(value.strip()) >= self.MIN_LENGTH:
            return queryset.filter(name__icontains=value.strip())
        
        return queryset

Special Character Handling

admin.py
import re

class SafeSearchFilter(TextFilter):
    title = "Safe Search"
    parameter_name = "search"

    def queryset(self, request, queryset):
        value = self.value()
        
        if value:
            # Remove special characters that might cause issues
            safe_value = re.sub(r'[^\w\s-]', '', value)
            
            if safe_value:
                return queryset.filter(name__icontains=safe_value)
        
        return queryset

Custom Form Styling

admin.py
from unfold.contrib.filters.forms import SearchForm


class CustomSearchForm(SearchForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Add placeholder text
        for field in self.fields.values():
            field.widget.attrs.update({
                'placeholder': 'Type to search...',
                'autocomplete': 'off',
            })


class StyledTextFilter(TextFilter):
    form_class = CustomSearchForm
    title = "Search"
    parameter_name = "search"

API Reference

TextFilter

title
str
required
Display name for the filter in the admin
parameter_name
str
required
URL parameter name for the search value
form_class
type[SearchForm]
default:"SearchForm"
Form class used for rendering the search input
template
str
default:"\"unfold/filters/filters_field.html\""
Template used to render the filter

FieldTextFilter

lookup_kwarg
str
Automatically generated as {field_path}__icontains
form_class
type[SearchForm]
default:"SearchForm"
Form class used for rendering the search input
template
str
default:"\"unfold/filters/filters_field.html\""
Template used to render the filter

Methods

queryset
method
def queryset(self, request, queryset):
    if self.value():
        return queryset.filter(field__icontains=self.value())
    return queryset
Filters the queryset based on the search value.
value
method
def value(self):
    return self.used_parameters.get(self.parameter_name)
Returns the current search text.

Best Practices

1

Choose appropriate lookups

  • __icontains: General substring search (slowest)
  • __istartswith: Prefix search (faster, can use indexes)
  • __iexact: Exact match (fastest)
  • Full-text search: Best for large text fields
2

Add database indexes

Index searchable fields to improve performance:
name = models.CharField(max_length=200, db_index=True)
3

Handle edge cases

  • Trim whitespace from user input
  • Handle empty strings gracefully
  • Consider minimum length requirements
  • Validate special characters
4

Consider alternatives

  • Use AutocompleteSelectFilter for searching related objects
  • Use Django’s built-in search_fields for simple cases
  • Use database full-text search for large text fields

Comparison with search_fields

  • Need filter UI in sidebar
  • Want to combine with other filters
  • Need custom search logic
  • Want to search related fields
admin.py
@admin.register(Product)
class ProductAdmin(ModelAdmin):
    search_fields = ['name', 'sku', 'description']
  • Need global search bar at top
  • Simple multi-field search
  • Standard Django behavior
  • Better for quick lookups
admin.py
@admin.register(Product)
class ProductAdmin(ModelAdmin):
    search_fields = ['name', 'sku']  # Top search bar
    list_filter = [
        ("description", FieldTextFilter),  # Sidebar filter
    ]
Combine for maximum flexibility!

Next Steps

Autocomplete Filters

Learn about AJAX-powered autocomplete for related objects

Checkbox & Radio

Explore checkbox and radio button filters

Build docs developers (and LLMs) love