1) Planteamiento del problema: “¿Por qué aparecen resultados innecesarios?”

Recientemente, mi blog empezó a crecer y la necesidad de una gestión más estructurada se volvió evidente. Como el blog se publica en varios idiomas, el mismo slug puede ser usado por distintas instancias, lo que hace que, a medida que aumentan los artículos, localizar una publicación concreta se vuelva un poco más complicado. Es una señal de que el contenido está prosperando, pero también implica desafíos.

Este problema de encontrar rápidamente la instancia deseada me llevó a concluir que la lógica de búsqueda predeterminada de Django Admin (search_fields) es demasiado vaga.

Bibliotecario con una lupa mágica buscando un libro

Cuando se configuran muchos search_fields (y suele suceder a medida que crecen las funcionalidades), al escribir una sola palabra en la barra de búsqueda ocurre que:

  • Coincide con el título
  • Coincide con la descripción
  • Coincide con el nombre de usuario
  • Coincide con campos de notas irrelevantes

El resultado deja de ser "muchos resultados" y pasa a ser "los que busco están enterrados entre ellos". En una situación de emergencia, cuando necesitas encontrar rápidamente una entrada concreta y aparecen decenas de filas, la frustración es inevitable.

Así nació la idea de "ser capaz de buscar especificando solo el campo que me interesa".


2) Idea de solución: sintaxis campo:valor + shlex para manejar comillas

La propuesta es sencilla:

  • Buscar especificando el campo: nombre:diff
  • Soportar búsquedas con espacios usando comillas: título:"system design"

Aquí es donde shlex.split() resulta muy útil. El cuadro de búsqueda de Admin normalmente divide la cadena por espacios, lo que rompe entradas como "system design". shlex respeta las comillas al estilo de la shell y tokeniza correctamente.

Ejemplos:

  • título:"system design"["título:system design"]
  • nombre:diff python["nombre:diff", "python"]

En otras palabras, podemos combinar búsquedas por campo y términos genéricos sin problemas.


3) Explicación del código

Todo el código está disponible en el repositorio. Sólo tienes que copiar‑pegar.

Enlace directo al repositorio en GitHub

A continuación, los puntos clave.

_build_lookup_for: respetar los prefijos de search_fields

search_fields permite prefijos que modifican el tipo de búsqueda:

  • =campo → coincidencia exacta (insensible a mayúsculas)
  • ^campo → coincidencia por prefijo
  • Sin prefijo → icontains

Queríamos que estos comportamientos se mantuvieran incluso con la nueva sintaxis, por lo que añadimos un pequeño helper que determina el lookup a partir del prefijo.

def _build_lookup_for(self, raw_field):
    if raw_field.startswith('='):
        return raw_field.lstrip('=^@'), '__iexact'
    if raw_field.startswith('^'):
        return raw_field.lstrip('=^@'), '__istartswith'
    return raw_field.lstrip('=^@'), '__icontains'

Ejemplo: si en admin.py defines search_fields = ['=nombre', 'título'],

  • nombre usará iexact
  • título usará icontains

Y la misma regla se aplica a las búsquedas con campo:valor.


get_search_results: los campos especificados se combinan con AND, los términos genéricos con la lógica tradicional

La lógica principal está en get_search_results():

  1. Tokenizar la entrada con shlex.
  2. Clasificar los tokens en dos grupos: * campo:valor donde campo está en search_fieldsfield_specific_terms * El resto → normal_terms
  3. Los términos específicos de campo se acumulan con AND.
  4. Los términos genéricos siguen la regla de Admin: OR entre campos, AND entre términos.

Fragmento relevante:

terms = shlex.split(search_term or "")

field_specs = [self._build_lookup_for(f) for f in getattr(self, 'search_fields', [])]
field_lookup_map = {}
for name, lookup in field_specs:
    field_lookup_map.setdefault(name, lookup)

field_specific_terms = []
normal_terms = []

for t in terms:
    if ':' in t:
        field, value = t.split(':', 1)
        if field in field_lookup_map and value != '':
            field_specific_terms.append((field, value))
        else:
            normal_terms.append(t)
    else:
        normal_terms.append(t)

Los campos no válidos o valores vacíos simplemente se tratan como términos normales.

(1) campo:valor → acumulación con AND

for field, value in field_specific_terms:
    lookup = field_lookup_map[field]
    qs = qs.filter(Q(**{f"{field}{lookup}": value}))

Una entrada como nombre:diff estado:activo se traduce a nombre = diff AND estado = activo.

(2) Términos genéricos → (campo OR) por término, luego AND entre términos

if normal_terms and field_lookup_map:
    for term in normal_terms:
        term_q = Q()
        for field, lookup in field_lookup_map.items():
            term_q |= Q(**{f"{field}{lookup}": term})
        qs = qs.filter(term_q)

Ejemplo: nombre:diff python

  • nombre = diff AND
  • (nombre OR título OR descripción contiene python)

Esta combinación conserva la "sensación" de la búsqueda estándar de Admin, mientras que los filtros explícitos son precisos.


4) Uso: basta con añadir el mixin al Admin

@admin.register(MiModelo)
class MiAdmin(FieldScopedSearchMixin, admin.ModelAdmin):
    search_fields = ['=nombre', 'título', 'descripción']

Ahora la barra de búsqueda acepta:

  • nombre:diff
  • título:"system design"
  • nombre:diff python

Si deseas personalizar la ayuda que se muestra, basta con sobrescribir get_changelist_instance().


5) Reflexión y limitaciones: "Funciona bien, pero quiero más"

Crear este mixin me dio la capacidad de ajustar el rango de búsqueda según mis necesidades operativas, lo que supuso una mejora notable en la experiencia diaria.

Sin embargo, existen limitaciones claras:

  • Búsqueda full‑text (@ prefijo) no está contemplada. Integrar el full‑text de PostgreSQL podría aportar más inteligencia, pero implica configuración de pesos, tsvector, etc.
  • Al basarse en icontains, el rendimiento puede degradarse con volúmenes grandes de datos, especialmente cuando se combinan varios campos con OR y varios términos.
  • Próximas ideas que me gustaría explorar:
  • Rango de fechas, por ejemplo creado:2026-01-01..2026-01-31
  • Condiciones de exclusión: estado:activo,-estado:borrado
  • Operadores comparativos simples: campo>10

Por ahora, la solución actual cumple con el objetivo de ser pequeña, clara y útil. Resolver un punto de fricción que yo mismo experimentaba en producción ha sido muy gratificante.

Un pequeño esfuerzo adicional en la lógica y la experiencia del usuario puede traducirse en una mejora permanente de la eficiencia laboral.


Artículos relacionados