1) 문제 제기: “아니, 왜 이렇게 쓸데없는 결과도 섞여나와?”

최근에 운영하던 블로그에 글들이 늘어나면서 체계적인 관리가 언젠가부터 필요하기 시작했습니다. 제 블로그는 다국어로 발행이 되기 때문에 같은 슬러그라도 해도 다양한 인스턴스가 사용하고 있기도 해서, 글들이 많아 질 수록 원하는 포스트를 콕 찝어 찾아내는데 한번에 찾기가 살짝 어려운 정도가 되었습니다. 블로그가 풍부해지고 있다는 좋은 신호이기도 하지만요.

그런데 이런 원하는 인스턴스를 한방에 찾기 어려운 문제는 Django Admin의 기본 검색(search_fields)이 검색 로직이 좀 두리뭉실 하기 때문이라고 생각했습니다.

마법의 돋보기를 들고 책을 찾는 도서관 사서

search_fields를 많이 잡아두면(그리고 운영 기능이 늘면 늘수록 보통 그렇게 되죠), 검색창에 단어 하나 넣는 순간:

  • 제목에도 걸리고
  • 설명에도 걸리고
  • 사용자 이름에도 걸리고
  • 엉뚱한 메모 필드에도 걸리고

결과가 “많이 나오는” 수준을 넘어 “내가 찾는 게 묻히는” 상태가 됩니다. 운영하다가 급하게 특정 포스트 하나 찾고 싶은데, 결과가 수십 개로 펼쳐지면… 솔직히 짜증이 날때도 있습니다.

그래서 “필드 하나만 딱 집어서 검색할 수 있으면 좋겠다”가 시작이었습니다.


2) 해결 아이디어: field:value 문법 + shlex로 따옴표 처리

아이디어는 단순합니다.

  • name:diff 처럼 필드를 지정해서 검색하기
  • title:"system design" 처럼 공백 포함 검색어도 지원하기

여기서 shlex.split()이 꽤 유용합니다. Admin 검색창은 보통 “그냥 공백으로 나누기”를 해버리는데, 그러면 "system design" 같은 입력이 깨져요. shlex를 쓰면 쉘처럼 따옴표를 존중해서 토큰을 나눠줍니다.

예를 들면:

  • title:"system design"["title:system design"] 같은 식으로 깔끔하게 처리 가능
  • name:diff python["name:diff", "python"]

즉, 필드 지정 검색일반 검색어를 섞어도 예쁘게 파싱할 수 있습니다.


3) 코드 설명

전체 코드는 아래 저장소에 올려뒀습니다. 복붙해서 쓰기 편하게 해놨어요.

깃허브 레포지토리 바로가기

블로그에는 핵심만 뽑아서 설명해볼게요.

_build_lookup_for: search_fields 접두사를 그대로 존중하기

Django Admin의 search_fields에는 접두사 규칙이 있죠.

  • =field → 정확히 일치(대소문자 무시) 쪽으로
  • ^field → prefix 매칭
  • 기본은 icontains

저는 이 규칙을 “새 문법을 도입하더라도” 그대로 유지하고 싶었습니다. 그래서 접두사를 보고 lookup을 결정하는 작은 헬퍼가 들어갑니다.

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'

예를들어 :

admin.py에서 Admin class들의 설정을 하실때, search_fields = ['=name', 'title']처럼 써두면

  • nameiexact
  • titleicontains

  • 그리고 field:value에서도 똑같은 lookup 정책이 적용됩니다.


get_search_results: 필드 지정은 AND, 일반 검색어는 기존처럼

핵심 로직은 get_search_results()에서 처리합니다.

  1. 먼저 입력을 shlex로 토큰화
  2. 토큰을 두 부류로 나눔
  • field:value 형태이면서 field가 search_fields에 있는 경우 → field_specific_terms
  • 그 외 → normal_terms 3. 필드 지정 검색은 AND로 누적 4. 일반 검색어는 Django Admin 기본 규칙을 따라가되(개념적으로)

  • 각 term은 여러 필드에 대해 OR

  • term들끼리는 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 같은 입력은 자연스럽게 name == diff AND status == active처럼 쌓입니다.

(2) 일반 검색어는 term마다 (필드 OR) → term끼리 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)

그래서 이런 입력이 됩니다:

  • name:diff python

  • name=diff AND

  • (name OR title OR description 중 하나에 python)

이 방식이 좋은 이유는 “기존 Admin 검색 감각”을 크게 안 깨기 때문이에요. field:value는 내가 의도를 명확히 한 거니까 정확히 좁혀주고, 나머지는 예전처럼 넓게 찾게 두는 거죠.


4) 사용 방법: Admin에 믹스인만 끼우면 끝

사용은 간단합니다.

@admin.register(MyModel)
class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin):
    search_fields = ['=name', 'title', 'description']

이제 Admin 검색창에서 이런 입력이 가능합니다.

  • name:diff
  • title:"system design"
  • name:diff python

그리고 사용시 도움말을 자신에 맞게 변경하려면 get_changelist_instance()를 고쳐서 쓰시면 됩니다.


5) 회고 및 한계: “편하긴 한데, 더 욕심이 난다”

이 믹스인을 만들고 제일 좋았던 건, 운영할 때 내가 원하는 범위로 검색을 ‘조절’할 수 있게 된 것이었습니다. 생각보다 체감상 편리함에 오는 만족감은 꽤 컸습니다.

다만 한계도 분명합니다.

  • Full-text search(@ 접두사) 는 여기서 제대로 다루지 않았습니다. PostgreSQL의 full-text를 제대로 연결하면 더 똑똑해질 수 있는데, 그건 또 세팅/가중치/tsvector 관리로 이야기가 커지더라고요. 지금은 일단 “운영 스트레스 줄이기”에 집중했습니다.
  • icontains 중심이라 데이터가 커지면 성능 이슈가 생길 수 있습니다. 특히 여러 필드에 OR를 걸고 term을 여러 개 쓰면 쿼리가 무거워질 수 있어요. (운영 환경에서는 검색어가 짧게 끝나는 경우가 많지만, 최악의 입력은 언제나 존재하죠.)
  • 다음으로 해보고 싶은 건 이런 것들입니다.

  • created_at:2026-01-01..2026-01-31 같은 날짜 범위 검색

  • status:active,-status:deleted처럼 부정 조건
  • field>10 같은 간단한 비교 연산자

근데 일단은… 지금 형태가 딱 “작고 확실하게 편해지는” 지점이라 마음에 듭니다. 운영 중에 내가 직접 짜증을 느낀 포인트를 해결한 도구라, 손이 자주 가고요.

고민과 아이디어에 약간의 수고를 더한 것만으로 업무의 효율이 영구적으로 높아지는 것은 정말 기분 좋은 일입니다.

이것이 바로 코딩의 매력이 아닌가 싶네요.


관련글