1) Het probleem: “Waarom krijg ik zoveel onnodige resultaten?”

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

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

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

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

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:

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:

  • nameiexact
  • titleicontains

get_search_results: veld‑specifiek = AND, algemene termen = OR/AND

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_fieldsfield_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)
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

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

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

@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

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