Фильтрация коллекций по HTTP#
В этом руководстве описаны параметры запроса, которые понимает эндпоинт
коллекции (метод FILTER), и то, как они превращаются в
фильтры DM.
Фильтрация включается на уровне ресурса:
class TaggedController(controllers.BaseResourceController):
__resource__ = resources.ResourceByRAModel(
TaggedModel,
process_filters=True,
)
Без process_filters=True параметры передаются как есть, строками, и не
парсятся и не валидируются вовсе.
Скалярные поля#
Параметр, названный по имени поля, фильтрует по равенству; значение
парсится в тип поля, поэтому значение, которое тип не принимает, — это
400, а не сравнение, которое молча ни с чем не совпадёт:
Повтор параметра, таким образом, читается как «любое из».
Поля-массивы#
Поле-массив (типичный случай — ModelWithTags и его tags) ведёт себя
иначе, и на этой разнице легко споткнуться:
означает tags = ARRAY['env:prod'] — весь массив равен этому одному
элементу. Это допустимый фильтр, но это не поиск: строка с тегами
['env:prod', 'region:eu'] не совпадёт.
Поиск по элементу — это то, для чего нужно выражение-фильтр ниже:
?q=tags:"env:prod". Параметра-поля, который делал бы то же самое, нет, и
причина в значениях: тег выглядит как owner:user:<uuid>, со всей своей
пунктуацией, а параметр, значение которого несёт собственные разделители,
не может нести ещё и оператор. Выражение может — у него есть кавычки.
Выражения-фильтры#
Параметры выше — это AND из равенств и ничего больше. Там, где этого мало (OR, группировка, отрицание, диапазоны), эндпоинт коллекции принимает ещё и выражение — на подмножестве AIP-160, в одном параметре:
GET /v1/vms/?q=name = "web-1" AND size > 10
GET /v1/vms/?q=tags:"env:prod" OR tags:"env:staging"
GET /v1/vms/?q=NOT (state = error) AND created_at >= "2026-07-01T00:00:00Z"
По умолчанию параметр называется q. Там, где язык включён, это имя
изымается из пространства имён полей, поэтому ресурс с полем q
переименовывает параметр — или выключает язык, и тогда параметр снова
значит то же, что значил раньше:
class VmController(controllers.BaseResourceController):
__filter_param__ = "filter" # или None, чтобы выключить язык
Операторы#
| Как пишется | Во что превращается | |
|---|---|---|
name = "web-1" |
EQ |
|
name != "web-1" |
NE |
|
size > 10, >=, <, <= |
GT, GE, LT, LE |
|
name = null |
Is(None) |
!= null даёт IsNot(None) |
tags:"env:prod" |
ContainsAll |
массив содержит элемент |
description:* |
IsNot(None) |
поле заполнено |
spec.kind = "totp" |
JSONFields |
один ключ внутри колонки jsonb |
AND OR NOT (...) |
AND OR NOT |
ключевые слова заглавными |
Две вещи регулярно удивляют, и обе унаследованы от AIP-160.
OR связывает сильнее, чем AND: a = 1 OR b = 2 AND c = 3 читается
как (a = 1 OR b = 2) AND c = 3. Сомневаетесь — ставьте скобки.
Пробел между двумя ограничениями — это неявный AND, так что
name = web-1 size > 10 есть конъюнкция.
Кавычки#
Значение, несущее :, . или пробел, обязано быть в кавычках, иначе его
пунктуация прочитается как ещё немного синтаксиса:
?q=tags:"env:prod" # верно
?q=tags:env:prod # 400 — второе : это оператор
?q=created_at >= "2026-07-01T00:00:00Z"
Без кавычек null, true, false и * — литералы языка; в кавычках это
просто такие строки.
Оба или любой#
Отдельного оператора для ContainsAny нет, и он не нужен. tags:"a" —
это tags @> ARRAY['a'], а значит булевы операторы его уже выражают:
?q=tags:"env:prod" AND tags:"region:eu" -> tags @> ARRAY[...] оба
?q=tags:"env:prod" OR tags:"env:staging" -> tags && ARRAY[...] любой
Клаузы по одному полю сливаются в один оператор массива, поэтому любая
из форм — это один поход в индекс, а не по одному на элемент.
name = a OR name = b сливается так же, в name = ANY(...).
Слияние никогда не меняет того, о чём просит выражение. @> по нескольким
элементам — не объединение этих элементов, поэтому containment, уже
расширенный через AND, сохраняет собственный оператор, когда его дальше
объединяют через OR:
Из-за этого слияния форме с query-параметрами нужны два имени оператора, а выражению — ни одного: у списка параметров нет ни AND, ни OR, чтобы выразить разницу.
Вместе с параметрами-полями#
Они складываются через AND, поэтому смешивать их можно свободно:
За что выражение отвергается#
Каждый из случаев — 400, а не фильтр, который молча делает что-то другое:
- поля, которого у ресурса нет, или значения, которое не принимает его тип;
- поля, которое ресурс скрывает из ответов — через
hidden_fieldsилиPermissions.HIDDENдля методаFILTER. Сравнение с полем, которое API никогда не возвращает, выдавало бы его по биту за запрос, поэтому скрытое поле отвечает ровно так же, как несуществующее. То же правило действует для параметра-поля (?secret=x) и дляsort_key, который упорядочивает коллекцию по значению, которого не показывает; - выражение, называющее кастомное свойство (см. ниже);
- оператор, который не умеет диалект хранилища:
:иspec.kind— только PostgreSQL; - больше одного
qв запросе: складывать их через AND или через OR, в запросе не сказано; - выражение больше 100 узлов, вложенное глубже 8 или длиннее 4096
символов — пределы задаются
__filter_max_nodes__,__filter_max_depth__и__filter_max_length__на контроллере, и они существуют потому, что строка фильтра — недоверенный ввод. Длина проверяется первой: остальные два предела считаются при разборе, а разбор начинается со сканирования всей строки.
Сознательно не поддержаны: функции (f(x)), голый литерал как нечёткий
поиск по всем полям, - как синоним NOT, wildcard'ы в =, сравнение
поля с полем и траверс глубже одного ключа.
Индексы#
Фильтр по массиву хорош ровно настолько, насколько хорош его индекс. Объявляйте GIN-индекс в той же миграции, что создаёт таблицу:
Для селективного значения — тега, который несёт малая доля строк, а именно
так выглядит тег владельца или идентичности, — планировщик отвечает из
индекса. Для значения, которое есть почти у каждой строки, он пойдёт по
первичному ключу и применит тег как фильтр; под LIMIT это более дешёвый
план, а не потерянный индекс.
Фильтрация вместе с пагинацией#
Пагинация складывается с фильтрами: граница курсора добавляется через AND к тому, что задал вызывающий, и дальше они едут вместе.
Селективный случай остаётся на GIN-индексе, а курсор применяется как обычный фильтр к строкам, которые индекс вернул.
Выражение пагинируется так же: курсор строится по параметрам-полям, а выражение добавляется через AND сразу после него:
Присылайте одно и то же выражение на каждой странице. Курсор что-то значит
только относительно того фильтра, под который был выдан; смена фильтра
посреди обхода пропустит или повторит строки. Учтите ещё, что keyset-
пагинация опирается на индекс под ORDER BY, и OR по нескольким полям
может увести планировщик с него — сужайте параметрами-полями там, где
можете.
Кастомные свойства#
Фильтры по кастомным (не хранимым) свойствам применяются в Python уже
после запроса, и там поддержаны только EQ и In — всё остальное,
включая операторы массивов, даёт 400.
Параметр-выражение до них не достаёт вовсе. Они фильтруются по строкам,
которые запрос уже вернул, а выражение нельзя разрезать на половину для
хранилища и половину для Python: stored = 1 OR custom = 2 негде
вычислить. Упоминание такого свойства в q — это 400; ?custom=2
по-прежнему работает.
См. также#
- Справочник по фильтрам — сами классы
клауз, включая
JSONFieldsдля колонокjsonb. - Справочник по моделям —
ModelWithTags. - Базовый CRUD — откуда берётся
process_filters.