Filtrado avanzado con django-filter y FilterSet

Avanzado
Django
Django
Actualizado: 19/04/2026

Por qué necesitas django-filter

Una API REST que solo permite GET /productos/ con todos los resultados se queda corta rápido. Los consumidores piden:

  • ?categoria_id=5 (filtro exacto).
  • ?precio_min=100&precio_max=500 (rango).
  • ?nombre=laptop (búsqueda parcial case-insensitive).
  • ?fecha_desde=2026-01-01&fecha_hasta=2026-12-31 (rango fechas).
  • ?ids=1,2,3 (filtro por lista).
  • ?ordering=-precio (ordenación).

Programar todo esto a mano en cada ViewSet es tedioso y propenso a errores. django-filter lo resuelve con clases declarativas que DRF integra automáticamente.

Instalación

pip install django-filter
# settings.py
INSTALLED_APPS = [..., 'django_filters']

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
}

Con esto cualquier ViewSet ya puede usar filtros.

Filtros simples con filterset_fields

Para casos sencillos, lista los campos:

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Producto.objects.all()
    serializer_class = ProductoSerializer
    filterset_fields = ['categoria', 'disponible', 'marca']

Queries soportadas:

GET /api/productos/?categoria=5
GET /api/productos/?categoria=5&disponible=true
GET /api/productos/?marca=Samsung&disponible=false

Los filtros son igualdad exacta sobre esos campos.

FilterSet con tipos de filtro

Para casos complejos, declara una FilterSet:

# filters.py
import django_filters
from django_filters import rest_framework as filters
from .models import Producto

class ProductoFilter(filters.FilterSet):
    # Búsqueda parcial case-insensitive
    nombre = django_filters.CharFilter(lookup_expr='icontains')

    # Rango de precio
    precio_min = django_filters.NumberFilter(field_name='precio', lookup_expr='gte')
    precio_max = django_filters.NumberFilter(field_name='precio', lookup_expr='lte')

    # Fechas de rango
    creado_desde = django_filters.DateFilter(field_name='creado_en', lookup_expr='gte')
    creado_hasta = django_filters.DateFilter(field_name='creado_en', lookup_expr='lte')

    # Booleano
    disponible = django_filters.BooleanFilter()

    # Choice (desde un enum de modelo)
    estado = django_filters.ChoiceFilter(choices=Producto.ESTADOS)

    # Lista de IDs (?ids=1,2,3)
    ids = django_filters.BaseInFilter(field_name='id', lookup_expr='in')

    # Relaciones
    categoria_nombre = django_filters.CharFilter(
        field_name='categoria__nombre',
        lookup_expr='iexact',
    )

    class Meta:
        model = Producto
        fields = ['nombre', 'precio_min', 'precio_max', 'categoria',
                  'creado_desde', 'creado_hasta', 'disponible', 'estado', 'ids']

En el ViewSet:

from .filters import ProductoFilter

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Producto.objects.all()
    serializer_class = ProductoSerializer
    filterset_class = ProductoFilter

Queries:

GET /api/productos/?nombre=lap&precio_min=500&precio_max=2000&disponible=true
GET /api/productos/?creado_desde=2026-01-01&creado_hasta=2026-12-31
GET /api/productos/?ids=1,5,8,13

Tipos de filtro más útiles

| Filter | Uso | Ejemplo query | |--------|-----|---------------| | CharFilter | Texto con lookup_expr | ?nombre=lap con icontains → LIKE '%lap%' | | NumberFilter | Números con gte/lte/exact | ?precio_min=100 | | BooleanFilter | true/false | ?activo=true | | DateFilter | Fecha exacta o rango | ?fecha=2026-04-18 | | DateFromToRangeFilter | Rango con _after/_before | ?fecha_after=2026-01-01&fecha_before=2026-12-31 | | NumericRangeFilter | Rango numérico | ?precio_min=100&precio_max=500 | | ChoiceFilter | Lista de opciones predefinidas | ?estado=activo | | MultipleChoiceFilter | Varias opciones | ?estados=activo&estados=revision | | BaseInFilter | Lista separada por coma | ?ids=1,2,3 | | BaseRangeFilter | Rango separado por coma | ?precio_range=100,500 | | ModelChoiceFilter | FK a otro modelo | ?categoria=5 (resuelve a Categoria.objects.get) | | ModelMultipleChoiceFilter | M2M | ?tags=1&tags=2 |

Filtros custom con method

Cuando la lógica es compleja, define un método:

class ProductoFilter(filters.FilterSet):
    disponible_en_stock = django_filters.BooleanFilter(method='filter_en_stock')

    def filter_en_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock__gt=0, disponible=True)
        return queryset.filter(stock=0) | queryset.filter(disponible=False)

    # Filtro custom que combina varias columnas
    busqueda = django_filters.CharFilter(method='filter_busqueda')

    def filter_busqueda(self, queryset, name, value):
        return queryset.filter(
            Q(nombre__icontains=value) |
            Q(descripcion__icontains=value) |
            Q(sku__iexact=value)
        )

Uso: ?disponible_en_stock=true o ?busqueda=laptop.

SearchFilter: búsqueda full-text simple

Independiente de django-filter, DRF ofrece SearchFilter para búsquedas sobre varias columnas:

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Producto.objects.all()
    serializer_class = ProductoSerializer
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_class = ProductoFilter
    search_fields = ['nombre', 'descripcion', 'sku']

Uso: ?search=laptop busca en todos los search_fields con icontains (o istartswith si el campo empieza con ^, iexact si con =).

Para búsquedas realmente potentes (relevancia, stemming) usa PostgreSQL full-text o Elasticsearch.

OrderingFilter: ordenación dinámica

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    ordering_fields = ['precio', 'creado_en', 'nombre']
    ordering = ['-creado_en']  # default

Uso: ?ordering=precio (ascendente) o ?ordering=-precio (descendente). Varios: ?ordering=-precio,nombre.

Limitar a un set concreto:

ordering_fields = ['precio', 'creado_en']  # solo estos

O permitir ordenación sobre campos anotados:

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Producto.objects.annotate(total_vendido=Count('ventas'))
    ordering_fields = ['precio', 'total_vendido']

Combinar filtros con permisos (multi-tenant)

Si la API es multi-tenant y el usuario solo ve sus productos, combina get_queryset con filtros:

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = ProductoSerializer
    filterset_class = ProductoFilter

    def get_queryset(self):
        return (
            Producto.objects
            .filter(tenant=self.request.user.tenant)  # filtro base obligatorio
            .select_related('categoria')
            .prefetch_related('tags')
        )

El usuario puede aplicar cualquier filtro de ProductoFilter, pero siempre limitado a su tenant.

Documentación automática con drf-spectacular

Los filtros de django-filter se documentan automáticamente en Swagger UI:

GET /api/productos/?
  nombre         string      (query, optional)
  precio_min     number      (query, optional)
  precio_max     number      (query, optional)
  disponible     boolean     (query, optional)
  ordering       string      (query, optional) [precio, -precio, nombre, -nombre]

No requiere configuración extra. El schema OpenAPI es correcto y los integradores lo ven desde Swagger UI y pueden probarlo interactivamente.

Paginación combinada con filtros

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 50,
}

Query: ?nombre=lap&precio_min=500&page=2&page_size=25 — se aplican los filtros y luego se pagina.

Para datasets grandes, cursor pagination escala mejor:

class ProductoPagination(CursorPagination):
    page_size = 50
    ordering = '-creado_en'
    cursor_query_param = 'cursor'

class ProductoViewSet(viewsets.ReadOnlyModelViewSet):
    pagination_class = ProductoPagination

Rendimiento de filtros

  • Indexa los campos filtrables más usados: CREATE INDEX idx_producto_categoria ON producto(categoria_id);.
  • Filtros con icontains (LIKE '%texto%') no usan índice B-tree estándar. Para búsquedas full-text, usar pg_trgm con GIN o full-text search.
  • Filtros con OR o Q(...) | Q(...) pueden ser lentos. A veces dos queries UNION ALL son más eficientes.
  • Limitar la cardinalidad: siempre ordering + limit para evitar "SELECT * FROM tabla_enorme".
-- Crear indices para los filtros mas frecuentes
CREATE INDEX CONCURRENTLY idx_producto_categoria ON producto(categoria_id);
CREATE INDEX CONCURRENTLY idx_producto_precio ON producto(precio) WHERE disponible = true;
CREATE INDEX CONCURRENTLY idx_producto_nombre_trgm ON producto USING GIN (nombre gin_trgm_ops);

El último necesita CREATE EXTENSION pg_trgm; para búsquedas icontains rápidas.

Checklist

  • [ ] DEFAULT_FILTER_BACKENDS configurado en settings.
  • [ ] FilterSet por modelo con los filtros documentados.
  • [ ] SearchFilter con search_fields de las columnas más buscadas.
  • [ ] OrderingFilter con ordering_fields explícito (evita ordenar por columnas no indexadas).
  • [ ] get_queryset con select_related/prefetch_related apropiados.
  • [ ] Índices en columnas filtrables más frecuentes.
  • [ ] Paginación activada para endpoints de listado.
  • [ ] Tests que cubren combinaciones de filtros.
  • [ ] Documentación Swagger muestra los filtros correctamente.
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Instalar django-filter e integrar DjangoFilterBackend. Declarar una FilterSet con los tipos de filtros mas comunes. Usar filtros por rango (DateFromToRangeFilter, NumericRangeFilter), IN (BaseInFilter), case-insensitive (icontains). Escribir filtros custom con un method=. Combinar con SearchFilter y OrderingFilter para busquedas full-text y ordenacion dinamica.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje