FOR: архитектура поиска данных как самостоятельный слой системы
В любой системе, которая живёт дольше нескольких месяцев, поиск данных постепенно перестаёт быть вспомогательной функцией и начинает определять архитектуру.
Сначала это один endpoint с фильтрами. Потом — несколько экранов с похожей логикой. Затем появляются исключения, особые условия, дополнительные поля, связанные сущности.
И в какой-то момент становится заметно: поиск реализуется не как единая система, а как набор разрозненных решений.
Именно в этот момент возникает потребность не в очередной оптимизации, а в переосмыслении — вынести поиск в отдельный уровень абстракции.
Так появилась концепция, которую я называю FOR (Filter / Options / Response).
Поиск как язык, а не как код
Ключевая идея FOR заключается в очень простой мысли: поиск данных должен описываться, а не программироваться
Это означает, что вместо написания кода под каждый сценарий мы вводим универсальный способ описания запроса, который может быть:
- сериализован (например, в JSON),
- передан между слоями системы,
- интерпретирован в любом контексте.
Таким образом, поиск становится не функцией backend-а, а контрактом, понятным всей системе.
Три составляющие запроса
FOR разбивает любой запрос на три независимые части:
• Filter — что искать
• Options — как расширить данные
• Response — как обработать результат
Это разделение может показаться очевидным, но именно оно позволяет избежать накопления сложности.
Filter: минимальный строительный блок
В основе Filter лежит максимально простой примитив:
объект — оператор — значение
Это не попытка упростить реальность, а сознательное ограничение. Любой фильтр, каким бы сложным он ни казался, можно разложить на такие атомы.
Важно, что объект здесь — это не просто поле в таблице, а элемент некоторой системы типов. Это позволяет работать не с конкретной базой данных, а с абстрактной моделью.
Фильтры могут объединяться, но не через усложнение синтаксиса, а через композицию:
- подзапросы (sub_query)
- явные логические связи (AND, OR)
Такой подход даёт предсказуемость: мы не создаём язык, который нужно изучать, мы собираем выражения из простых блоков.
Options: расширение без усложнения
Одна из распространённых проблем — попытка “всё уместить в фильтр”. JOIN’ы, связанные данные, дополнительные вычисления — всё это начинает проникать в условия поиска и делает их громоздкими.
В FOR это решается иначе.
Filter отвечает только на вопрос “что выбрать”,
Options — на вопрос “что к этому добавить”.
Options позволяют “насытить” результат:
- добавить связанные сущности
- подтянуть дополнительные данные
- выполнить вспомогательные операции
При этом сам фильтр остаётся чистым и понятным.
Response: управление результатом
Третья часть — Response — отвечает за то, что происходит после получения данных.
Здесь могут быть:
- агрегации
- преобразования
- форматирование
Это позволяет не смешивать:
- логику получения данных
- и логику их представления
И снова — за счёт разделения снижается сложность.
Provider как точка интерпретации
FOR сам по себе — это описание. Чтобы он начал работать, нужен интерпретатор.
Эту роль выполняет Provider.
Provider:
- принимает FOR-запрос
- интерпретирует его
- взаимодействует с конкретным источником данных
Важно, что Provider работает поверх абстракций:
- моделей ORM
- прямых запросов к базе
- внешних API
Это означает, что сама система не зависит от способа хранения данных.
Если в какой-то момент:
- ORM становится узким местом
- или меняется технология хранения
Достаточно изменить Provider, не затрагивая остальную систему.
Контракты и система типов
FOR особенно хорошо раскрывается там, где есть строгая система типов.
Контракт в данном случае — это описание:
- допустимых объектов
- их свойств
- возможных операций
Он может быть реализован по-разному, но ключевая идея в том, что: система знает, с какими данными она работает
Динамические поля при этом не запрещены, но меняют характер системы. Частые изменения структуры приводят к необходимости пересмотра правил, и в таких сценариях FOR теряет часть своих преимуществ.
Практический эффект: скорость разработки
Одно из наиболее ощутимых последствий использования FOR — это изменение скорости разработки.
Недавно я столкнулся с задачей: нужно было быстро реализовать backend для нового сервиса.
В классическом подходе это означало бы:
• проектирование API
• реализацию фильтров
• написание логики выборки под каждый сценарий
Вместо этого я:
- ввёл систему типов
- подключил FOR
- реализовал Provider
После этого поиск по всем сущностям стал доступен практически сразу.
Мне не пришлось:
- писать отдельные обработчики
- продумывать каждый кейс
- дублировать логику
- создавать эндпойнты под разные кейсы
- описывать request/response у них (всегда приходит всегда тип в полноте данных)
Система начала работать благодаря своей абстрактности.
Дальше:
- был добавлен слой RBAC
- ограничения доступа встроились поверх моделей
И всё это — без переписывания поисковой логики.
Повторное использование на уровне интерфейса
Ещё одно важное следствие — возможность переиспользования.
Если в системе есть приложения вроде:
- таблицы
- канбан-доски
- графики
- деревья
Они могут работать с одним и тем же источником данных.
Меняется только FOR-запрос.
Это даёт возможность:
- создавать новые представления данных
- не привлекая разработчиков
- а просто изменяя фильтры
Фактически, пользователь начинает работать не с кодом, а с моделью данных.
Где FOR действительно полезен
Лучшие результаты достигаются в системах:
- со сложной моделью данных
- с большим количеством сценариев поиска
- с необходимостью гибкой настройки
Особенно хорошо он работает там, где:
- есть строгие контракты
- структура данных стабильна
- важна унификация
Ограничения и границы применимости
FOR не претендует на универсальность.
Он не предназначен для:
- сложной бизнес-логики
- транзакционных процессов
- вычислительных задач
Также он может быть избыточен:
- в небольших проектах
- в простых CRUD-сценариях
Важно понимать его роль: это не замена архитектуры, а её специализированный слой
Практика: как это выглядит в реальной системе
Теперь имеет смысл перейти от концепции к практике и ввести более предметный контекст.
Представим задачу: необходимо быстро разработать небольшое программное обеспечение, в котором есть 10–20 типов данных (это условность — подход масштабируется практически без ограничений) и десятки различных представлений этих данных.
Сами данные связаны между собой:
- через внешние ключи
- через иерархии
- через различные доменные зависимости
В качестве примера возьмём систему управления тестовой документацией.
Для MVP определим следующий набор типов:
- typePrice
- typeTeam
- typeTeamMember
- typeTeamUserRole
- typeUser
- typeProject
- typeMilestone
- typeTestCase
- typeTestCaseStep
- typeTestPlan
- typeTestRun
- typeTestRunResult
- typeTestSuite
- typeTestAccount
Даже на этом уровне уже становится очевидно: количество связей и сценариев выборки будет быстро расти.
Базовые структуры FOR
Внутри системы вводится базовое описание FOR:
export class typeFOR extends familyService
{
filters;
options;
response;
}
export class typeFilter extends familyService
{
object = {
attribute: false,
};
operator;
value;
}
Здесь важно не столько само объявление, сколько идея:
- Filter — универсальный критерий поиска
- Options и Response — расширяемые части, адаптируемые под конкретный проект
Response
Обычно сюда удобно выносить:
- лимиты
- пагинацию
- сортировки
- группировки
Options
Options отвечают за “насыщение” данных.
Например, если мы работаем с массивом typeTestCase, то поле typeTestCase.test_suite_id (которое является числовым идентификатором) может быть заменено на полноценный объект typeTestSuite.
Таким образом, вместо join-логики на уровне фильтра мы явно управляем обогащением результата.
API как единая точка входа
Далее весь API-слой оборачивается в единый сервис, например ApiService.
Каждый тип данных получает унифицированный набор операций:
ApiService.[element_type].[search / create / update / delete]
В контексте поиска нас интересует:
ApiService.typeTestCase.search(payload)
С точки зрения frontend это даёт предельно простую и единообразную модель взаимодействия.
Независимо от того, где именно в приложении выполняется запрос, код выглядит одинаково:
let filters = [
new typeFilter({
project_id: project_id
}),
];
let payload = new typeFOR({
filters: filters,
...
});
Далее запрос передаётся в API, и в ответ приходит:
- либо коллекция инстанцированных объектов
- либо ошибка
На этом уровне взаимодействие становится максимально предсказуемым.
Любой frontend-разработчик, в любой части приложения, работает с backend через одну и ту же абстракцию: Api + FOR.
Backend: где происходит основная работа
Теперь перейдём к backend-части — именно здесь раскрывается ключевая ценность подхода.
Основной элемент — это слой DataProvider.
Он получает:
- ORM-модель (или другую абстракцию данных)
- FOR-запрос
И выполняет поиск по переданным атрибутам.
При этом:
- вся логика поиска сведена к абстракции
- код для разных типов данных практически идентичен
Рядом с DataProvider (или внутри моделей) размещается всё остальное:
- проверка прав доступа
- логирование
- дополнительные правила
- насыщение данных (Options)
Сам DataProvider при этом остаётся максимально простым: он умеет только интерпретировать FOR и извлекать данные из модели
Масштабирование через абстракцию
За счёт такой структуры достигается важный эффект.
Добавление нового типа данных включает:
- создание ORM-модели
- добавление таблицы в базе данных
После этого:
• поиск уже работает
• API уже готов
• дополнительная логика минимальна
Это возможно потому, что весь механизм поиска уже реализован на уровне абстракции.
Использование в бизнес-логике
Несмотря на то, что FOR изначально ориентирован на поиск, он удобно применяется и в бизнес-логике.
По мере развития системы:
- в Filter добавляются подзапросы
- усложняются условия
- появляются новые сценарии
При этом структура остаётся прежней, а сложность растёт управляемо.
Практическое следствие
В результате получается система, в которой:
- добавление новых типов данных практически не требует времени
- поиск реализован единообразно
- код не дублируется
- поведение предсказуемо
И, что особенно важно: система становится доступной не только разработчикам
При наличии соответствующих интерфейсов и ограничений:
- модераторы
- операторы
- другие участники системы
могут:
- создавать новые представления данных
- настраивать существующие
- работать с фильтрами
без необходимости писать код.
Разумеется, это возможно только при условии, что и остальные части системы сведены к таким же простым и абстрактным конструкциям.
Заключение
FOR — это попытка упростить не код, а мышление о данных.
Он вводит:
• единый способ описания поиска
• чёткое разделение ответственности
• независимость от инфраструктуры
И за счёт этого даёт:
• ускорение разработки
• снижение сложности
• возможность расширения без переписывания системы
Это не инструмент в привычном смысле, а скорее набор принципов, который можно реализовать по-разному.
Но как только поиск в системе перестаёт быть тривиальным — появляется смысл задуматься о том, чтобы описывать его как отдельный язык.