1) Проблема: “Почему появляются лишние результаты?”



С ростом количества записей в моём блоге стало необходимым более систематическое управление контентом. Блог публикуется на нескольких языках, поэтому один и тот же slug может использоваться в разных инстансах, и поиск нужного поста стал слегка затруднительным. С одной стороны – хороший сигнал о росте блога, с другой – поиск нужного материала усложнился.

Я обнаружил, что основной поиск Django Admin (search_fields) работает слишком расплывчато.

Библиотекарь с волшебной лупой ищет книгу

Когда в search_fields добавляется много полей (а это обычная практика при росте функционала), ввод одного слова в строку поиска приводит к тому, что результат появляется в:

  • заголовке,
  • описании,
  • имени пользователя,
  • даже в каком‑то случайном поле заметок.

В итоге результаты не просто “много”, а “переполняют нужный элемент”. При срочном поиске конкретного домена в десятках записей это становится действительно раздражающим.

Так родилась идея: выбирать конкретное поле и искать только в нём.


2) Идея решения: синтаксис field:value + shlex для кавычек

Суть проста.

  • name:diff – указать поле, в котором искать.
  • title:"system design" – поддержать запросы с пробелами.

Здесь на помощь приходит shlex.split(). Стандартный поиск в админке просто разбивает строку по пробелам, из‑за чего запросы вроде "system design" ломаются. shlex учитывает кавычки, как в оболочке, и делит строку корректно.

Например:

  • title:"system design"["title:system design"]
  • name:diff python["name:diff", "python"]

Таким образом поле‑специфичный поиск и обычные термы могут смешиваться иtda парситься без проблем.


3) Объяснение кода



Полный код опубликован в репозитории, откуда его можно просто скопировать.

Ссылка на GitHub

Ниже – ключевые части.

_build_lookup_for: сохраняем префиксы search_fields

В search_fields Django поддерживает префиксы:

  • =field – точное совпадение (без учёта регистра),
  • ^field – поиск по префиксу,
  • без префикса – icontains.

Мы хотим, чтобы эти правила оставались действительными и для нового синтаксиса, поэтому вводим небольшую вспомогательную функцию.

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 указано search_fields = ['=name', 'title'], то:

  • name будет искать через iexact,
  • title – через icontains.

То же самое применяется к запросам вида field:value.


get_search_results: поле‑специфичные термы объединяются через AND, остальные – как обычно

Основная логика реализована в get_search_results().

  1. Токенизируем ввод с помощью shlex.
  2. Делим токены на две группы: * field:value, где поле присутствует в search_fieldsfield_specific_terms; * всё остальное → normal_terms.
  3. Поиск по полям комбинируется через AND.
  4. Обычные термы работают по привычному правилу Admin: каждый термин ищется в нескольких полях через OR, а между разными термами – 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) Обычные термы – (поле OR) → term AND term

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

Такой подход сохраняет привычную «чувствительность» поиска в админке, но добавляет возможность точно ограничивать запросы через field:value.


4) Как использовать: достаточно добавить миксин

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

Теперь в строке поиска админки можно вводить:

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

При желании можно переопределить get_changelist_instance() и изменить подсказку для пользователей.


5) Итоги и ограничения: “Удобно, но хочется большего”

Создав этот миксин, я получил возможность точно регулировать область поиска в рабочем режиме, что заметно сократило стресс от «переполненных» результатов.

Однако есть ограничения:

  • Full‑text search (префикс @) пока не поддерживается. Интеграция с PostgreSQL full‑text могла бы добавить умный ранжир, но требует отдельной настройки.
  • Поиск основан на icontains, поэтому при большом объёме данных может возникнуть проблема производительности – особенно при множественных OR‑условиях.
  • В дальнейшем хотелось бы добавить:
  • Диапазон дат, например created_at:2026-01-01..2026-01-31;
  • Отрицательные условия, типа status:active,-status:deleted;
  • Простейшие сравнения field>10.

Тем не менее текущая реализация решает именно ту задачу, которая меня беспокоила в работе, и делает админский поиск более предсказуемым и быстрым.

Немного усилий – постоянный прирост эффективности. Именно в этом, по‑моему, и заключается прелесть программирования.


Связанные статьи