Skip to content

Фильтрация коллекций по HTTP#

В этом руководстве описаны параметры запроса, которые понимает эндпоинт коллекции (метод FILTER), и то, как они превращаются в фильтры DM.

Фильтрация включается на уровне ресурса:

class TaggedController(controllers.BaseResourceController):
    __resource__ = resources.ResourceByRAModel(
        TaggedModel,
        process_filters=True,
    )

Без process_filters=True параметры передаются как есть, строками, и не парсятся и не валидируются вовсе.


Скалярные поля#

Параметр, названный по имени поля, фильтрует по равенству; значение парсится в тип поля, поэтому значение, которое тип не принимает, — это 400, а не сравнение, которое молча ни с чем не совпадёт:

GET /v1/vms/?name=web-1          ->  EQ("web-1")
GET /v1/vms/?name=web-1&name=web-2  ->  In(["web-1", "web-2"])

Повтор параметра, таким образом, читается как «любое из».


Поля-массивы#

Поле-массив (типичный случай — ModelWithTags и его tags) ведёт себя иначе, и на этой разнице легко споткнуться:

GET /v1/tagged/?tags=env:prod

означает 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:

?q=(tags:"a" AND tags:"b") OR tags:"c"   ->  tags @> ARRAY['a','b'] OR tags @> ARRAY['c']

Из-за этого слияния форме с query-параметрами нужны два имени оператора, а выражению — ни одного: у списка параметров нет ни AND, ни OR, чтобы выразить разницу.

Вместе с параметрами-полями#

Они складываются через AND, поэтому смешивать их можно свободно:

GET /v1/vms/?state=active&q=tags:"env:prod" OR tags:"env:staging"

За что выражение отвергается#

Каждый из случаев — 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-индекс в той же миграции, что создаёт таблицу:

CREATE INDEX idx_tagged_tags ON tagged USING GIN (tags);

Для селективного значения — тега, который несёт малая доля строк, а именно так выглядит тег владельца или идентичности, — планировщик отвечает из индекса. Для значения, которое есть почти у каждой строки, он пойдёт по первичному ключу и применит тег как фильтр; под LIMIT это более дешёвый план, а не потерянный индекс.


Фильтрация вместе с пагинацией#

Пагинация складывается с фильтрами: граница курсора добавляется через AND к тому, что задал вызывающий, и дальше они едут вместе.

GET /v1/tagged/?project_id=<uuid>&page_limit=50

Селективный случай остаётся на GIN-индексе, а курсор применяется как обычный фильтр к строкам, которые индекс вернул.

Выражение пагинируется так же: курсор строится по параметрам-полям, а выражение добавляется через AND сразу после него:

GET /v1/tagged/?q=tags:"env:prod"&page_limit=50&page_marker=<uuid>

Присылайте одно и то же выражение на каждой странице. Курсор что-то значит только относительно того фильтра, под который был выдан; смена фильтра посреди обхода пропустит или повторит строки. Учтите ещё, что keyset- пагинация опирается на индекс под ORDER BY, и OR по нескольким полям может увести планировщик с него — сужайте параметрами-полями там, где можете.


Кастомные свойства#

Фильтры по кастомным (не хранимым) свойствам применяются в Python уже после запроса, и там поддержаны только EQ и In — всё остальное, включая операторы массивов, даёт 400.

Параметр-выражение до них не достаёт вовсе. Они фильтруются по строкам, которые запрос уже вернул, а выражение нельзя разрезать на половину для хранилища и половину для Python: stored = 1 OR custom = 2 негде вычислить. Упоминание такого свойства в q — это 400; ?custom=2 по-прежнему работает.


См. также#