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

• реализацию фильтров

• написание логики выборки под каждый сценарий

Вместо этого я:

  1. ввёл систему типов
  2. подключил FOR
  3. реализовал 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 и извлекать данные из модели

Масштабирование через абстракцию

За счёт такой структуры достигается важный эффект.

Добавление нового типа данных включает:

  1. создание ORM-модели
  2. добавление таблицы в базе данных

После этого:

• поиск уже работает

• API уже готов

• дополнительная логика минимальна

Это возможно потому, что весь механизм поиска уже реализован на уровне абстракции.

Использование в бизнес-логике

Несмотря на то, что FOR изначально ориентирован на поиск, он удобно применяется и в бизнес-логике.

По мере развития системы:

  • в Filter добавляются подзапросы
  • усложняются условия
  • появляются новые сценарии

При этом структура остаётся прежней, а сложность растёт управляемо.

Практическое следствие

В результате получается система, в которой:

  • добавление новых типов данных практически не требует времени
  • поиск реализован единообразно
  • код не дублируется
  • поведение предсказуемо

И, что особенно важно: система становится доступной не только разработчикам

При наличии соответствующих интерфейсов и ограничений:

  • модераторы
  • операторы
  • другие участники системы

могут:

  • создавать новые представления данных
  • настраивать существующие
  • работать с фильтрами

без необходимости писать код.

Разумеется, это возможно только при условии, что и остальные части системы сведены к таким же простым и абстрактным конструкциям.

Заключение

FOR — это попытка упростить не код, а мышление о данных.

Он вводит:

• единый способ описания поиска

• чёткое разделение ответственности

• независимость от инфраструктуры

И за счёт этого даёт:

• ускорение разработки

• снижение сложности

• возможность расширения без переписывания системы

Это не инструмент в привычном смысле, а скорее набор принципов, который можно реализовать по-разному.

Но как только поиск в системе перестаёт быть тривиальным — появляется смысл задуматься о том, чтобы описывать его как отдельный язык.

Начать дискуссию