## 1) Mise en contexte : « Pourquoi j’obtiens tant de résultats inutiles ? » {#sec-b345eb02ca5e} 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**. ![Un bibliothécaire cherchant un livre avec une loupe magique](/media/editor_temp/6/12d4b1c2-0844-485d-b9ef-0e1f766b18ca.png) 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` {#sec-5381e00c4696} 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 {#sec-8edf0b65ccd2} Le dépôt complet est disponible ici : [GitHub – admin_fieldscopedsearch.py](https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py). ### `_build_lookup_for` : respecter les préfixes de `search_fields` {#sec-25f0fe79cd1c} `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 : ```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' ``` Ainsi, dans `admin.py` : ```python search_fields = ['=name', 'title'] ``` * `name` utilise `iexact` * `title` utilise `icontains` 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 {#sec-2432c8a5949d} Le cœur de la logique se trouve dans `get_search_results()` : 1. Tokenisation avec `shlex` 2. Séparation en deux catégories : * `field:value` dont le champ figure dans `search_fields` → **field_specific_terms** * le reste → **normal_terms** 3. Les recherches par champ s’accumulent **en AND** 4. Les termes généraux conservent la règle Django Admin : * chaque terme → OR entre les champs * entre les termes → AND ```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) ``` Les champs inconnus ou les valeurs vides sont simplement traités comme des termes normaux. #### (1) `field:value` → accumulation en AND ```python 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 ```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) ``` Exemple : `name:diff python` * `name = diff` **AND** * `(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 {#sec-0eb3a92e04b6} ```python @admin.register(MyModel) class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin): search_fields = ['=name', 'title', 'description'] ``` Vous pouvez alors saisir : * `name:diff` * `title:"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" {#sec-4fc456da292d} 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 `icontains` peut 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** - [Pourquoi il faut masquer l’admin dès maintenant](/ko/whitedec/2025/11/10/admin-now-need-to-hide/)