1) Mise en contexte : « Pourquoi j’obtiens tant de résultats inutiles ? »
Avec l’augmentation du nombre d’articles sur mon blog, la gestion structurée est devenue indispensable. Le site étant multilingue, plusieurs instances採 utilisent le même slug, ce qui rend la localisation d’un article précis de plus en plus ardue. C’est un bon signe de croissance, mais cela complique la recherche.
Le problème réside, à mon avis, dans la logique de recherche par défaut de Django Admin (search_fields) qui est un peu floue.

Lorsque l’on définit beaucoup de search_fields (et cela se produit souvent au fur et à mesure que les fonctionnalités s’accumulent), un simple mot dans la barre de recherche :
- correspond au titre,
- à la description,
- au nom d’utilisateur,
- voire à un champ de notes hors sujet,
et les résultats ne sont plus « nombreux » mais « noyés ».
Quand on a besoin rapidement d’un domaine précis et que des dizaines d’entrées s’affichent, on finit par être agacé.
C’est ainsi qu’est née l’idée : « Et si je pouvais cibler un champ précis ? »
2) Concept : syntaxe field:value + gestion des guillemets avec shlex
L’idée est simple.
- Rechercher en spécifiant le champ :
name:diff - Autoriser les espaces dans la requête :
title:"system design"
shlex.split() s’avère très pratique ici. L’interface d’administration sépare habituellement les mots par espaces, ce qui brise les requêtes comme "system design". shlex respecte les guillemets comme le ferait un shell.
Par exemple :
title:"system design"→["title:system design"]name:diff python→["name:diff", "python"]
On peut donc mêler recherche par champ et termes généraux sans problème.
3) Explication du code
Le dépôt complet est disponible ici : GitHub – admin_fieldscopedsearch.py.
_build_lookup_for : respecter les préfixes de search_fields
search_fields accepte des préfixes :
=field→ correspondance exacte (insensible à la casse)^field→ recherche de préfixe- par défaut :
icontains
Le helper suivant conserve ces règles même avec la nouvelle syntaxe :
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'
Ainsi, dans admin.py :
search_fields = ['=name', 'title']
nameutiliseiexacttitleutiliseicontains
Le même comportement s’applique aux requêtes field:value.
get_search_results : les champs spécifiques sont combinés avec AND, les termes généraux restent OR
Le cœur de la logique se trouve dans get_search_results() :
- Tokenisation avec
shlex - Séparation en deux catégories :
*
field:valuedont le champ figure danssearch_fields→ field_specific_terms * le reste → normal_terms - Les recherches par champ s’accumulent en AND
- Les termes généraux conservent la règle Django Admin : * chaque terme → OR entre les champs * entre les termes → AND
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)
Les champs inconnus ou les valeurs vides sont simplement traités comme des termes normaux.
(1) field:value → accumulation en AND
for field, value in field_specific_terms:
lookup = field_lookup_map[field]
qs = qs.filter(Q(**{f"{field}{lookup}": value}))
name:diff status:active devient name = diff AND status = active.
(2) Termes généraux → (champ OR) puis AND entre les termes
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)
Exemple : name:diff python
name = diffAND(name OR title OR description) contains python
Cette approche conserve l’expérience de recherche native tout en offrant un filtrage précis grâce aux champs spécifiés.
4) Utilisation : il suffit d’ajouter le mixin à votre admin
@admin.register(MyModel)
class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin):
search_fields = ['=name', 'title', 'description']
Vous pouvez alors saisir :
name:difftitle:"system design"name:diff python
Pour personnaliser l’aide affichée, surchargez simplement get_changelist_instance().
5) Bilan et limites : "C’est pratique, mais j’ai encore des envies"
Ce mixin m’a permis de maîtriser la portée de mes recherches en production, ce qui humaines sensiblement le quotidien.
Points à noter :
- Le full‑text search (
@préfixe) n’est pas encore couvert ; l’intégrer avec PostgreSQL nécessiterait une configuration supplémentaire. - L’accent mis sur
icontainspeut entraîner des problèmes de performance sur de gros volumes, surtout avec plusieurs champs en OR. - Idées futures :
- Recherche de plages de dates (
created_at:2026-01-01..2026-01-31) - Conditions négatives (
status:active,-status:deleted) - Opérateurs de comparaison simples (
field>10)
Pour l’instant, la solution reste petite, fiable et immédiatement utile. Résoudre un point de friction que j’ai moi‑même identifié rend l’outil indispensable au quotidien.
Un petit effort d’idée et de mise en œuvre peut transformer durablement l’efficacité au travail. C’est là toute la magie du code.
Articles associés