## 1) Planteamiento del problema: “¿Por qué aparecen resultados innecesarios?” {#sec-b345eb02ca5e} 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](/media/editor_temp/6/12d4b1c2-0844-485d-b9ef-0e1f766b18ca.png) 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 {#sec-5381e00c4696} 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 {#sec-8edf0b65ccd2} Todo el código está disponible en el repositorio. Sólo tienes que copiar‑pegar. [Enlace directo al repositorio en GitHub](https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py) A continuación, los puntos clave. ### `_build_lookup_for`: respetar los prefijos de `search_fields` {#sec-25f0fe79cd1c} `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. ```python 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 {#sec-2432c8a5949d} 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_fields` → **field_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: ```python 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 ```python 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 ```python 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 {#sec-0eb3a92e04b6} ```python @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" {#sec-4fc456da292d} 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** - [Razones para ocultar el admin inmediatamente](/ko/whitedec/2025/11/10/admin-now-need-to-hide/)