Метод GET /api/v1/cook/recipes/search

Документация API: Умный подбор и ранжирование рецептов на основе пересечения остатков холодильника со справочником Master Data

Published

July 1, 2026

1 Функциональное назначение

Метод предназначен для автоматического подбора кулинарных рецептов, которые семья или сотрудники офиса могут приготовить прямо сейчас, исходя из текущего состава продуктов в холодильнике (home_group_id).

Метод решает три ключевые задачи: 1. Полнотекстовое и семантическое пересечение: Сверяет список уникальных канонических имен продуктов, находящихся на балансе пользователя, со списками ингредиентов в Мастер-базе рецептов. 2. Гибкий порог вхождения: Возвращает рецепт в поисковую выдачу, если есть совпадение минимум по одному продукту. Рецепты, для которых не хватает части ингредиентов, не отсекаются, а ранжируются ниже. 3. Защита от перегрузки (Performance Limit): Устанавливает жесткое ограничение на стороне СУБД — в ответ попадает максимум 500 рецептов, отсортированных по коэффициенту максимального совпадения. Это защищает оперативную память мобильного приложения (Flutter) от переполнения тяжелыми JSON-пакетами.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: GET
  • Маршрут: /api/v1/cook/recipes/search
  • Формат данных: application/json (только ответ)

2.1 Спецификация параметров запроса (Query Parameters)

Поскольку метод использует метод GET, параметры фильтрации передаются внутри URL.

Параметр Тип Обязательный Описание Пример значения
limit Integer Нет Ограничение размера страницы выдачи (Максимум = 500) 50
offset Integer Нет Смещение для постраничной навигации 0

2.2 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Authorization Да Токен авторизации (Access Token). Бэкенд извлекает из него home_group_id для получения состава холодильника. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для профилирования скорости тяжелого SQL-запроса req-rec-search-99aa

2.3 Спецификация структуры ответа (Response Body)

Метод возвращает массив рецептов. Для каждого рецепта рассчитывается match_percentage (сколько процентов ингредиентов рецепта уже есть в наличии).

Поле Тип Описание Пример значения
recipe_id Integer Уникальный ID рецепта в мастер-базе 412
title String Название блюда "Яблочный пирог классический"
match_percentage Float Процент совпадения имеющихся продуктов с рецептом 0.7500 (75%)
total_ingredients Integer Всего ингредиентов требуется в рецепте 4
matched_count Integer Сколько ингредиентов из требуемых есть в холодильнике 3

2.3.1 Пример JSON-ответа (Payload):

{
  "total_found": 142,
  "limit": 500,
  "offset": 0,
  "recipes": [
    {
      "recipe_id": 412,
      "title": "Яблочный пирог классический",
      "match_percentage": 0.75,
      "total_ingredients": 4,
      "matched_count": 3,
      "matched_items": ["Мука", "Масло сливочное 82%", "Апельсины"],
      "missing_items": ["Яйцо куриное"]
    }
  ]
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлен асинхронный процесс извлечения текущих остатков холодильника, сопоставления их со справочником рецептов на уровне СУБД с помощью агрегатных функций и выдачи упорядоченного списка во Flutter с жестким лимитом.

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Поиска)
    participant APP as Мобильное приложение (Flutter)
    participant API as Бэкенд-шлюз (FastAPI)
    participant DB as Реляционная СУБД (PostgreSQL)

    User->>APP: Открывает вкладку "Подбор рецептов"
    APP->>API: HTTP GET /api/v1/cook/recipes/search (С JWT токеном в Header)
    activate API
    
    note over API: Шаг 3: Извлечение home_group_id из JWT-токена
    
    API->>DB: Вызов сложного SQL-запроса (CTE) по home_group_id
    activate DB
    note over DB: Шаг 5: Сбор уникальных SKU из fridge_inventory (quantity > 0)
    note over DB: Шаг 6: Пересечение со словарем рецептов (Совпадение >= 1)
    note over DB: Шаг 7: Расчет match_percentage и применение LIMIT 500
    DB-->>API: Возвращает плоский массив подходящих рецептов (макс. 500 строк)
    deactivate DB
    
    API-->>APP: HTTP 200 OK (JSON с ранжированным списком рецептов)
    deactivate API
    note over APP: Экран мгновенно отрисовывает карточки: сверху рецепты с максимумом совпадений

4 Расшифровка шагов

  • Шаг 1 (User -> APP): Пользователь переходит на экран умного подбора рецептов. Приложение автоматически инициирует поиск без необходимости ввода текста — триггером выступает сам факт открытия экрана на основе текущего содержимого холодильника.
  • Шаг 2 (APP -> API): Flutter-приложение отправляет запрос HTTP GET /api/v1/cook/recipes/search. В заголовках передается Authorization: Bearer <JWT> и X-Request-ID для замера скорости выполнения тяжелого поискового запроса.
  • Шаг 3 (API -> API): Бэкенд FastAPI декодирует токен пользователя и извлекает параметр home_group_id. На основе этого идентификатора будет производиться фильтрация продуктов конкретной семьи или офиса.
  • Шаг 4 (API -> DB): Бэкенд отправляет в PostgreSQL оптимизированный SQL-запрос, использующий обобщенные табличные выражения (CTE) для параллельного подсчета совпадений.
  • Шаг 5 (DB -> DB): Внутри СУБД на первом этапе изолируется текущий срез холодильника: собираются все уникальные канонические имена продуктов (product_name) для данного home_group_id, у которых физический объем строго больше нуля (quantity > 0). Отрицательные виртуальные минусы на этом этапе игнорируются, чтобы не предлагать рецепты из полностью отсутствующих продуктов.
  • Шаг 6 (DB -> DB): На втором этапе СУБД производит соединение (JOIN) полученного набора продуктов со справочником ингредиентов рецептов. Срабатывает важнейший критерий: рецепт попадает в промежуточную выборку, если зафиксировано пересечение минимум по одному наименованию продукта.
  • Шаг 7 (DB -> DB): СУБД группирует данные по recipe_id и рассчитывает метрики: общее количество необходимых ингредиентов для блюда, количество совпавших позиций в холодильнике и коэффициент match_percentage (отношение имеющихся к требуемым). К выборке применяется жесткая сортировка ORDER BY match_percentage DESC (самые готовые рецепты идут вверх) и системный ограничитель LIMIT 500.
  • Шаг 8 (DB -> API): База данных отдает бэкенду отрендеренный и отсортированный массив данных, не превышающий 500 записей. Благодаря переносу расчетов на уровень индексов СУБД, время выполнения запроса составляет менее 40 мс.
  • Шаг 9 (API -> APP): FastAPI упаковывает результат в Pydantic-модель и возвращает клиенту статус HTTP 200 OK. Мобильное приложение Flutter мгновенно отрисовывает карточки рецептов. Пользователь сразу видит, что он может приготовить (например, пирог с совпадением 75%, где не хватает только одного яйца).

5 Спецификация серверной логики (SQL) и вилок исключений

5.1 1. Архитектурный SQL-запрос подбора рецептов (Шаги 5–7)

Для реализации бизнес-логики (совпадение минимум по 1 ингредиенту, расчет процента совпадения и жесткий лимит в 500 строк) на уровне PostgreSQL используется оптимизированный запрос с обобщенными табличными выражениями (CTE) и агрегацией.

WITH user_fridge AS (
    -- Шаг 5: Выделяем уникальный профиль доступных продуктов пользователя (без минусов)
    SELECT DISTINCT DISTINCT ON (LOWER(product_name)) LOWER(product_name) AS food_name
    FROM fridge_inventory
    WHERE home_group_id = \$1 AND quantity > 0
),
recipe_matching AS (
    -- Шаг 6: Сопоставляем ингредиенты рецептов с продуктами в холодильнике
    SELECT 
        ri.recipe_id,
        COUNT(ri.ingredient_name) AS total_needed,
        SUM(CASE WHEN uf.food_name IS NOT NULL THEN 1 ELSE 0 END) AS matched_count,
        ARRAY_AGG(ri.ingredient_name) FILTER (WHERE uf.food_name IS NOT NULL) AS matched_items,
        ARRAY_AGG(ri.ingredient_name) FILTER (WHERE uf.food_name IS NULL) AS missing_items
    FROM recipe_ingredients ri
    LEFT JOIN user_fridge uf ON LOWER(ri.ingredient_name) = uf.food_name
    GROUP BY ri.recipe_id
)
-- Шаг 7: Фильтруем (минимум 1 совпадение), ранжируем и накладываем жесткий лимит
SELECT 
    r.id AS recipe_id,
    r.title,
    (rm.matched_count::FLOAT / rm.total_needed::FLOAT) AS match_percentage,
    rm.total_needed AS total_ingredients,
    rm.matched_count,
    rm.matched_items,
    rm.missing_items
FROM recipe_matching rm
JOIN recipes r ON r.id = rm.recipe_id
WHERE rm.matched_count >= 1 -- Критерий: совпадение минимум по одному продукту
ORDER BY match_percentage DESC, rm.matched_count DESC
LIMIT 500; -- Защита от перегрузки оперативной памяти Flutter

5.2 2. Спецификация ответов сервера (Response Body)

5.2.1 HTTP 200 OK (Ответ на Шаге 9)

Возвращается при успешной выборке рецептов. Массивы matched_items и missing_items позволяют Flutter-приложению наглядно красить ингредиенты на экране (зеленый/красный цвет).

{
  "total_found": 3,
  "limit": 500,
  "offset": 0,
  "recipes": [
    {
      "recipe_id": 412,
      "title": "Яблочный пирог классический",
      "match_percentage": 0.75,
      "total_ingredients": 4,
      "matched_count": 3,
      "matched_items": ["Мука", "Масло сливочное 82%", "Яблоки"],
      "missing_items": ["Яйцо куриное"]
    },
    {
      "recipe_id": 105,
      "title": "Утренний омлет с зеленью",
      "match_percentage": 0.3333,
      "total_ingredients": 3,
      "matched_count": 1,
      "matched_items": ["Масло сливочное 82%"],
      "missing_items": ["Яйцо куриное", "Молоко 3.2%"]
    }
  ]
}

5.3 3. Вилки исключений и стратегии обработки ошибок

5.3.1 Сценарий А: Пустой холодильник (HTTP 200 OK с пустым массивом)

  • Условие: У данной семьи (home_group_id) в таблице fridge_inventory нет ни одной записи с quantity > 0 (холодильник пуст или все продукты в виртуальном минусе).
  • Действие системы: SQL-запрос отработает корректно и вернет 0 строк. Бэкенд возвращает статус 200 OK с пустым массивом recipes: []. Flutter перехватывает этот стейт и вместо падения рендерит заглушку: «В вашем холодильнике нет доступных продуктов. Пожалуйста, импортируйте ресурсы (сканируйте чек или добавьте фото), чтобы ИИ подобрал рецепты».

5.3.2 Сценарий Б: Превышение системного лимита запроса (HTTP 400 Bad Request)

  • Условие: Клиентское приложение попыталось передать через Query-параметры кастомный limit больше, чем зафиксировано в архитектурном стандарте системы (limit=1000).
  • Действие FastAPI: Срабатывает валидация Pydantic на бэкенд-шлюзе. Запрос отсекается до похода в БД.
{
  "error_code": "ERR-MAX-LIMIT-EXCEEDED",
  "message": "Передан некорректный параметр страницы. Максимальный лимит выдачи составляет 500 рецептов.",
  "details": {
    "requested_limit": 1000,
    "allowed_max_limit": 500
  }
}

5.3.3 Сценарий В: Таймаут СУБД из-за высокой параллельной нагрузки (HTTP 503)

  • Условие (Шаг 4): Пул соединений к PostgreSQL перегружен, и тяжелый аналитический JOIN не успевает выполниться за установленный таймаут в 2000 мс.
  • Действие FastAPI: Шлюз возвращает ошибку ERR-DATABASE-OFFLINE со статусом HTTP 503 Service Unavailable. На клиенте Dio перехватывает ошибку, не ломая интерфейс, и предлагает пользователю нажать кнопку «Повторить поиск».