Фильтры и поиск
Один кортеж, шесть видов элементов управления — и два типа полей, по которым FastFort отказывается фильтровать, с указанием причины.
Доступно с версии v0.1.0
Вы никогда не указываете элемент управления для фильтра. list_filter называет
колонки; тип самой колонки определяет элемент управления.
class ProductAdmin(admin.ModelAdmin):
list_filter = ("availability", "is_active", "category", "supplier",
"price", "released_on")
Что создаёт каждый тип
| Тип колонки | Элемент управления |
|---|---|
| Enum | Выпадающий список собственных значений колонки, считанных с неё |
| Boolean | Три состояния: любое, да, нет — никогда не чекбокс |
| Nullable boolean | Четыре, потому что «не задано» — тоже самостоятельный ответ |
| Внешний ключ · один-к-одному | Селектор; с автодополнением, если целевая таблица перерастает выпадающий список |
| Integer · float · decimal · money | Две границы, от и до, каждая необязательна |
| Date · datetime | Диапазон с готовыми пресетами — сегодня, последние 7 дней, этот месяц |
| Time · duration | Та же форма с двумя границами, в собственных единицах колонки |
| UUID | Точное совпадение |
Два отказа
Два типа полей нельзя фильтровать, и отказ происходит на этапе сборки
ModelAdmin, а не на странице:
ConfigurationError: SupplierAdmin is misconfigured:
- list_filter names 'country'; free-text and multi-valued fields
cannot be offered as a filter
Свободный текст
Колонка String или Text. Её выпадающий список состоял бы из каждого
отдельного значения в таблице — для customers.name это по одному варианту
на каждого клиента, и они запрашивались бы при каждой загрузке страницы.
Это отсеивает и то, что выглядит фильтруемым. Код страны String(2) —
замкнутое множество у вас в голове, а с точки зрения колонки — обычный
свободный текст:
country: Mapped[str] = mapped_column(sa.String(2), default="UZ") # not filterable
Решение — сделать множество явным, и тогда фильтрация заработает:
class Country(enum.StrEnum):
UZ = "UZ"
KZ = "KZ"
TR = "TR"
country: Mapped[Country] = mapped_column(sa.Enum(Country, name="country"))
Многие-ко-многим
Поле с несколькими значениями — сравнивать фильтр не с чем, единого значения
нет. tags у товара — это список; вопрос «товары, у которых тег bestseller»
некорректен, пока не решено, имеется в виду любой или все.
Оба типа остаются доступными для поиска, сортировки и отображения. Закрыта для них только панель фильтров.
Поле поиска
search_fields = ("id", "sku", "name", "description")
Без search_fields поле поиска вообще не отрисовывается — это лучше, чем
поле, которое молча ничего не находит.
Подходят два типа колонок, и сопоставляются они по-разному.
Текст сопоставляется по подстроке, без учёта регистра, на любой базе
данных: STRING, TEXT, EMAIL, URL. INET намеренно исключён, хотя
выглядит как текст, потому что в PostgreSQL нет LIKE для inet —
icontains по такой колонке падает с ошибкой operator does not exist, а
поле поиска, отдающее 500-ю ошибку, хуже, чем поле, которое просто пропускает
колонку.
Идентификаторы сопоставляются точно: INTEGER, BIGINT и UUID. Это
как раз то, что все на самом деле вводят в поле поиска админки — id приходит
из тикета в поддержку, счёта или строки лога — и search_fields = ("id", "name") раньше был ошибкой конфигурации, а не той очевидной строкой, какой
кажется.
Именно точно, а не по подстроке, по двум причинам. Поиск 1 должен вернуть
строку 1, а не строки 1, 12, 21 и 100; а равенство использует индекс
первичного ключа, тогда как приведение bigint к тексту для LIKE '%1%' — это
полное сканирование таблицы, для которой колонка id и есть лучший индекс.
Термин, который в принципе не может быть значением типа колонки, просто
выбрасывается из этой половины запроса. Поиск pixel по ("id", "name")
ищет только по name; поиск 42 — по обоим. Поиск слова, когда id —
единственное поле поиска, возвращает ничего — и это правильный ответ, а не
то же самое, что вернуть всё.
Всё остальное по-прежнему ошибка на этапе сборки, с указанием причины:
- search_fields names 'price', which is a decimal column. Search matches
text fields by substring and integer and UUID fields exactly; there is
no useful reading of a search term for this one. Offer it through
list_filter instead.
По связям можно искать через их собственные колонки, указав путь:
search_fields = ("id", "name", "category__name")
Фильтрация по URL
ListQuery.from_params — единственное место, где сырая строка запроса
превращается в запрос. Ключи сортировки, поля фильтров и операторы
проверяются по спискам разрешённых значений, построенным на основе
спецификации; ничего дальше по цепочке повторно не проверяет — и не должно.
?availability=in_stock exact
?price__gte=50000&price__lte=200000 a range
?released_on__range=2026-01-01,2026-06-30
?category__isnull=true
?q=cable the search box
?o=-created_at ordering
?ps=100 page size, capped at admin.max_page_size
Доступные операторы: exact, iexact, ne, lt, lte, gt, gte, in,
isnull, range, icontains, istartswith, iendswith.
Всё, чего нет в списке разрешённого для этого поля, отклоняется, а не пропускается дальше — фильтр по колонке, которую админка не предлагала, недостижим даже через строку адреса.
Сохранённые представления
Поскольку фильтр — это строка запроса, она уже доступна для передачи — скопируйте URL, отправьте его. Не хватало только имени для того, к которому стоит возвращаться.
Меню «Представления» рядом с кнопкой фильтров хранит набор именованных URL-адресов целиком в браузере: на сервере ничего не записывается, потому что строка запроса уже была источником истины, а её вторая копия стала бы лишь ещё одним местом, где эти два источника могут разойтись. Возврат к сохранённому представлению всегда начинается с первой страницы — номер страницы никогда не был частью того, что представление означает.
Никакой опции ModelAdmin для этого не нужно — она и не требуется. Любой
список, который уже отображает собственный ModelAdmin проекта, получает
меню «Представления» бесплатно, точно так же, как получает Export.
Видимость столбцов
Широкая модель заставляет платить за каждый столбец на каждой строке,
независимо от того, важен ли он тому, кто читает этот экран. Меню
«Столбцы» — по одному переключателю на каждый отображаемый столбец —
скрывает то, что не стоит места, и запоминает это для каждого списка, в
каждом браузере, в localStorage.
Это настройка отображения, а не запрос: скрытый столбец всё равно
запрашивается и всё равно доступен для экспорта, просто не отрисовывается.
Здесь тоже не нужна конфигурация ModelAdmin — по той же причине, что и
сохранённым представлениям: она уже есть у каждого списка.
Пространственные фильтры
На геометрической или географической колонке:
?catchment__within=POLYGON((...))
?catchment__intersects=POINT(69.28 41.31)
?last_seen__dwithin=41.3111,69.2797,5000
?catchment__bbox=69.1,41.2,69.4,41.4
Также contains, overlaps, touches, crosses.
С dwithin нужно быть осторожным, и FastFort проявляет эту осторожность за
вас: третье число — это метры для колонки geography и единицы SRID —
градусы, для 4326 — для geometry. Спецификация несёт флаг geography
именно для того, чтобы эти два случая нельзя было перепутать, и запрос
приводит типы там, где это необходимо.
Векторный поиск
На колонке pgvector:
?embedding__near=[0.4,0.1,0.9]&embedding__k=5
Ранжирует от ближайшего к дальнему, и этот порядок важнее всего остального, что было запрошено у страницы — «ближайшее, а при равенстве — по имени», а не по алфавиту.
cosine (по умолчанию), l2, l1 и inner выбирают оператор расстояния,
также доступна граница:
?embedding__near=[0.4,0.1,0.9]&embedding__metric=l2&embedding__within=0.35