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
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.
Multi-Field Search
Search across multiple fields:
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:
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 )
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
from django.db import models
class Article ( models . Model ):
title = models.CharField( max_length = 200 )
content = models.TextField()
slug = models.SlugField( unique = True )
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:
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
Case-Sensitive Search
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
Starts With Search
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
Regular Expression Search
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)
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
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
class Product ( models . Model ):
name = models.CharField( max_length = 200 )
list_filter = [
( "name" , FieldTextFilter), # Works perfectly
]
TextField
class Article ( models . Model ):
content = models.TextField()
list_filter = [
( "content" , FieldTextFilter), # Searches long text
]
Searching large TextField content can be slow. Consider adding database indexes or using full-text search.
EmailField
class User ( models . Model ):
email = models.EmailField( unique = True )
list_filter = [
( "email" , FieldTextFilter),
]
SlugField
class Article ( models . Model ):
slug = models.SlugField( unique = True )
list_filter = [
( "slug" , FieldTextFilter),
]
Add indexes for frequently searched fields: 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: 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.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
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
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
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
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
Display name for the filter in the admin
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
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
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.
def value ( self ):
return self .used_parameters.get( self .parameter_name)
Returns the current search text.
Best Practices
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
Add database indexes
Index searchable fields to improve performance: name = models.CharField( max_length = 200 , db_index = True )
Handle edge cases
Trim whitespace from user input
Handle empty strings gracefully
Consider minimum length requirements
Validate special characters
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
When to use search_fields
@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.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