## 1) Het probleem: “Waarom krijg ik zoveel onnodige resultaten?” {#sec-b345eb02ca5e} Naarmate mijn blog groeide, werd het steeds lastiger om de juiste posts snel te vinden. Omdat de blog meertalig is, bestaan er verschillende instanties met dezelfde slug, waardoor een eenvoudige zoekopdracht vaak te veel resultaten oplevert. De standaard zoekfunctie van Django Admin (`search_fields`) is hier te vaag. ![Een bibliothecaris die met een magische vergrootglas een boek zoekt](/media/editor_temp/6/12d4b1c2-0844-485d-b9ef-0e1f766b18ca.png) Wanneer je veel velden in `search_fields` opneemt, gebeurt er het volgende zodra je een woord intypt: * Het wordt gevonden in de titel * Het wordt gevonden in de beschrijving * Het wordt gevonden in de gebruikersnaam * Het wordt zelfs gevonden in ongerelateerde notitie‑velden In plaats van een gerichte lijst krijg je een overdaad aan resultaten, waardoor het zoeken naar één specifiek item vaak frustrerend is. Daarom begon ik na te denken over een manier om **slechts één veld** te selecteren voor de zoekopdracht. --- ## 2) Oplossingsidee: `field:value`‑syntaxis + `shlex` voor aanhalingstekens {#sec-5381e00c4696} Het concept is simpel: * Zoek met een opgegeven veld, bv. `name:diff` * Ondersteun zoektermen met spaties, bv. `title:"system design"` `shlex.split()` komt hierbij goed van pas. De admin‑zoekbalk splitst normaal gesproken op spaties, waardoor `"system design"` verkeerd wordt geïnterpreteerd. Met `shlex` wordt de invoer echter behandeld als een shell‑commando, waardoor aanhalingstekens gerespecteerd worden. Voorbeelden: * `title:"system design"` → `["title:system design"]` * `name:diff python` → `["name:diff", "python"]` Zo kun je zowel **veld‑specifieke zoekopdrachten** als **algemene termen** elegant combineren. --- ## 3) Code‑uitleg {#sec-8edf0b65ccd2} De volledige implementatie staat in de repository. Kopieer‑en‑plak is voldoende. [GitHub‑repo] (https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py) ### `_build_lookup_for`: respecteer de prefixen uit `search_fields` {#sec-25f0fe79cd1c} `search_fields` ondersteunt prefixen: * `=field` → exacte (case‑insensitive) match * `^field` → prefix‑match * Standaard → `icontains` De helper `_build_lookup_for` behoudt dit gedrag ook voor de nieuwe syntaxis: ```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' ``` Zo werkt `search_fields = ['=name', 'title']` nog steeds als: * `name` → `iexact` * `title` → `icontains` ### `get_search_results`: veld‑specifiek = AND, algemene termen = OR/AND {#sec-2432c8a5949d} De kernlogica zit in `get_search_results()`: 1. Tokeniseer de invoer met `shlex` 2. Splits tokens in twee groepen * `field:value` waarbij `field` in `search_fields` → **field_specific_terms** * Alles andere → **normal_terms** 3. Veld‑specifieke termen worden **gecumuleerd met AND** 4. Algemene termen volgen de standaard Admin‑logica (elke term wordt OR over alle velden, termen onderling 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) ``` #### (1) `field:value` → 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` wordt dus `name = diff AND status = active`. #### (2) Algemene termen → (veld OR) per term, termen AND ```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) ``` Voor `name:diff python` betekent dit: * `name = diff` **AND** * `(name OR title OR description) bevat python` Deze aanpak behoudt de vertrouwde Admin‑zoekervaring, terwijl je met `field:value` heel gericht kunt filteren. --- ## 4) Gebruik: alleen de mixin toevoegen {#sec-0eb3a92e04b6} ```python @admin.register(MyModel) class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin): search_fields = ['=name', 'title', 'description'] ``` Nu kun je in de admin‑zoekbalk bijvoorbeeld invoeren: * `name:diff` * `title:"system design"` * `name:diff python` Wil je de help‑tekst aanpassen, dan kun je `get_changelist_instance()` overschrijven. --- ## 5) Reflectie & beperkingen {#sec-4fc456da292d} Met deze mixin kun je de zoekresultaten precies afstemmen op je operationele behoeften – een enorme tijdsbesparing in de dagelijkse administratie. Beperkingen: * **Full‑text search** (`@`‑prefix) wordt niet ondersteund. Een PostgreSQL‑full‑text‑index zou hier een flinke boost kunnen geven, maar brengt extra configuratie met zich mee. * Omdat de queries vooral `icontains` gebruiken, kan de performance afnemen bij grote datasets of complexe OR‑combinaties. * Toekomstige uitbreidingen die ik graag zie: * Datum‑range zoekopdrachten, bv. `created_at:2026-01-01..2026-01-31` * Negatieve filters, bv. `status:active,-status:deleted` * BLE‑achtige vergelijkingsoperatoren, bv. `field>10` Voor nu levert de mixin een kleine, maar zeer nuttige verbetering. Een paar extra regels code die de frustratie van ondoelmatige zoekopdrachten wegnemen, maken het werk een stuk aangenamer – en dat is precies wat programmeren zo bevredigend maakt. --- **Gerelateerde artikelen** - [Waarom je de admin‑interface nu moet verbergen](/nl/whitedec/2025/11/10/admin-now-need-to-hide/)