Метод GET /api/v1/affinity/suggestions

Документация API: Извлечение персонализированных рекомендаций для закупок на основе разреженной матрицы Affinity и продуктов в холодильнике

Published

July 2, 2026

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

Метод является ключевым элементом рекомендательного контура (Recall Engine) приложения, а также важной частью вероятностного контура (Stohastic Engine) симулятора и предназначен для вывода умных подсказок на экране «Импорт ресурсов / Покупки». Метод избавляет пользователя от необходимости вручную вбивать списки частых покупок, предугадывая его потребности. В симуляторе вектор параметров полученных методом используется в числе прочих для определения вероятности события.

Метод решает следующие архитектурные задачи:

  1. Экстраполяция разреженной матрицы (Sparse Matrix Optimization): Так как система хранит аффинити-скоры только для тех продуктов, которые пользователь реально покупал (200–250 уникальных SKU из 20 000 глобального справочника), метод выполняет LEFT JOIN с мастер-данными. Если личной записи нет, система налету подставляет общесистемный дефолтный вес продукта (например, Хлеб = 99, Авокадо = 15).
  2. Динамическая фильтрация по остаткам (Inventory Cross-Check): Метод сопоставляет аффинити-скоры с текущим живым холодильником группы (home_group_id). Продукты с высоким приоритетом, но имеющие физический остаток на складе quantity > 1.0 (или выше критического порога), автоматически исключаются из выдачи, чтобы не плодить дубликаты.
  3. Ранжирование дефицита: Формирует упорядоченный список из Топ-20 рекомендаций, где на самом верху находятся любимые продукты пользователя, которые прямо сейчас полностью закончились (quantity == 0) или находятся в критическом минимуме (на исходе).

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

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

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

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

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

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

Поле Тип Описание Пример значения
master_product_id Integer Глобальный ID товара в справочнике Master Data 1024
affinity_score Integer Итоговый вычисленный балл предпочтения (от 0 до 100) 94
trigger_reason String Маркер причины рекомендации для UI "STOCK_EMPTY"

2.2.1 Перечень допустимых маркеров trigger_reason:

  • STOCK_EMPTY — продукт полностью закончился в холодильнике (баланс = 0.0), но имеет высокий affinity-скор.
  • STOCK_CRITICAL_LOW — продукт на исходе (остался критический float-минимум, например, менее 10% от стандартной закупки).

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

{
  "home_group_id": "group-772-alpha",
  "suggestions_count": 2,
  "suggestions": [
    {
      "master_product_id": 1024,
      "product_name": "Молоко 3.2% пастеризованное",
      "affinity_score": 94,
      "current_stock": 0.0,
      "unit": "л",
      "trigger_reason": "STOCK_EMPTY"
    },
    {
      "master_product_id": 2048,
      "product_name": "Яблоки Гренни Смит",
      "affinity_score": 82,
      "current_stock": 0.150,
      "unit": "кг",
      "trigger_reason": "STOCK_CRITICAL_LOW"
    }
  ]
}

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

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

WarningВажное примечание

В настоящей публичной документации отображены не все шаги и сценарии для приложения в частности и для системы цифровых симуляторов бизнес-процессов в общем.

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Закупок)
    participant APP as Мобильное приложение (Flutter)
    participant AS as Сервис Аффинити (FastAPI)
    participant DB as База Данных (PostgreSQL)

    User->>APP: Открывает экран "Импорт ресурсов / Покупки"
    APP->>AS: HTTP GET /api/v1/affinity/suggestions (JWT в Header)
    activate AS
    
    note over AS: Шаг 3: Извлечение home_group_id и контекста из JWT-токена
    
    AS->>DB: Вызов SQL-запроса кросс-валидации матриц и остатков
    activate DB
    note over DB: Шаг 5: LEFT JOIN разреженной таблицы пользователя и глобальных SKU
    note over DB: Шаг 6: LEFT JOIN с fridge_inventory (Срез текущих живых остатков)
    note over DB: Шаг 7: Фильтрация (Остаток == 0 ИЛИ < 10%), сортировка по score и LIMIT 20
    DB-->>AS: Возвращает упорядоченную матрицу рекомендаций (макс. 20 строк)
    deactivate DB
    
    AS-->>APP: HTTP 200 OK (JSON с ранжированными подсказками закупок)
    deactivate AS
    note over APP: Сверху экрана покупок отрисовывается карусель пресетов быстрой вставки

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

  • Шаг 1 (User -> APP): Пользователь открывает раздел закупок или планирования корзины (экран «Импорт ресурсов») во Flutter-приложении. Интерфейс автоматически, без дополнительных кликов со стороны пользователя, инициирует фоновый запрос для генерации персональных рекомендаций.
  • Шаг 2 (APP -> AS): Мобильное приложение отправляет запрос HTTP GET /api/v1/affinity/suggestions. Сквозной логирующий заголовок X-Request-ID и Access Token авторизации передаются в стандартном виде.
  • Шаг 3 (AS -> AS): Микросервис (FastAPI) декодирует JWT Claims токена и выделяет home_group_id. Логика аффинити используется как для домашнего аккаунта HOME, так и для аккаунта компании OFFICE , метод работает для любого типа аккаунта.
  • Шаг 4 (AS -> DB): Сервис отправляет в базу данных PostgreSQL аналитический SQL-запрос, реализующий кросс-проверку трех независимых таблиц для бесшовного развертывания разреженной матрицы.
  • Шаг 5 (DB -> DB): На уровне СУБД запускается алгоритм LEFT JOIN. Базовая таблица глобальных продуктов (master_products, 20 000 строк) соединяется с таблицей личных предпочтений пользователя (user_product_affinity) по составному ключу группы. Благодаря индексам, СУБД мгновенно вытаскивает кастомный балл для тех 200 позиций, которые семья покупала, а для остальных 19 800 строк налету подставляет системный дефолтный балл с помощью функции COALESCE().
  • Шаг 6 (DB -> DB): Полученная матрица предпочтений повторно соединяется через LEFT JOIN с таблицей текущего холодильника (fridge_inventory) по совпадению имен продуктов, чтобы получить текущие остатки.
  • Шаг 7 (DB -> DB): Фильтрация и ранжирование: СУБД отсекает товары, которых в холодильнике прямо сейчас достаточно. В выборку пропускаются только те продукты, у которых баланс на складе равен 0.0 (полный дефицит) или упал ниже критического float-порога (на исходе). Строки сортируются по убыванию affinity score (ORDER BY affinity_score DESC) и ограничиваются жестким лимитом LIMIT 20.
  • Шаг 8 (DB -> AS): База данных отдает сервису отранжированный и очищенный от дубликатов массив из 20 целевых подсказок. Время выполнения сложной выборки составляет менее 35 мс.
  • Шаг 9 (AS -> APP): FastAPI упаковывает результат в Pydantic-модель и возвращает статус HTTP 200 OK. Мобильное приложение Flutter принимает JSON-датасет. Стейт-менеджер мгновенно отрисовывает горизонтальную карусель быстрых пресетов сверху экрана покупок. Пользователь видит любимое молоко и яблоки с плашками «Закончилось» и может в один тап добавить их в планируемый список, не прибегая к ручному поиску по буквам.

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

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

Для реализации бизнес-логики (бесшовное развертывание разреженной матрицы, LEFT JOIN с текущими остатками холодильника и жесткий LIMIT 20) на уровне PostgreSQL используется аналитический запрос со слиянием через COALESCE.

WITH current_fridge_stock AS (
    -- Агрегируем текущие остатки холодильника пользователя по каноническим SKU
    SELECT LOWER(product_name) AS food_name, SUM(quantity) AS total_qty, unit
    FROM fridge_inventory
    WHERE home_group_id = \$1
    GROUP BY LOWER(product_name), unit
)
SELECT 
    mp.id AS master_product_id,
    mp.product_name,
    -- Развертывание Sparse Matrix: если личного скора нет, берем системный дефолт
    COALESCE(upa.score, mp.default_system_affinity) AS affinity_score,
    COALESCE(cfs.total_qty, 0.0)::FLOAT AS current_stock,
    mp.unit,
    -- Динамическое определение триггера для интерфейса Flutter
    CASE 
        WHEN COALESCE(cfs.total_qty, 0.0) = 0.0 THEN 'STOCK_EMPTY'
        ELSE 'STOCK_CRITICAL_LOW'
    END AS trigger_reason
FROM master_products mp
-- Шаг 5: LEFT JOIN с разреженной таблицей личного аффинити пользователя
LEFT JOIN user_product_affinity upa 
    ON upa.master_product_id = mp.id AND upa.home_group_id = \$1
-- Шаг 6: LEFT JOIN со срезом текущих остатков склада
LEFT JOIN current_fridge_stock cfs 
    ON cfs.food_name = LOWER(mp.product_name) AND cfs.unit = mp.unit
-- Шаг 7: Фильтрация дефицита и ранжирование по силе предпочтения
WHERE 
    COALESCE(cfs.total_qty, 0.0) = 0.0 -- Продукт полностью закончился
    OR (COALESCE(cfs.total_qty, 0.0) > 0.0 AND COALESCE(cfs.total_qty, 0.0) < mp.critical_min_threshold) -- Продукт на исходе
ORDER BY affinity_score DESC, current_stock ASC
LIMIT 20; -- Защита от перегрузки экрана карусели пресетов во Flutter

5.2 Спецификация структуры таблиц PostgreSQL (Sparse Matrix Architecture)

Схема базы данных поддерживает концепцию разреженной матрицы: записи в user_product_affinity генерируются точечно строго в моменты транзакций.

-- Таблица 1: Глобальный справочник продуктов (Мастер-данные, 20 000+ строк)
CREATE TABLE master_products (
    id SERIAL PRIMARY KEY,
    product_name VARCHAR(255) NOT NULL UNIQUE,
    default_system_affinity INT NOT NULL DEFAULT 15,       -- Дефолтный скор (Хлеб=99, Авокадо=15)
    critical_min_threshold NUMERIC(10, 4) NOT NULL DEFAULT 0.1000, -- Порог дефицита (например, < 0.1 кг)
    unit VARCHAR(20) NOT NULL                              -- "кг", "шт", "л"
);

-- Таблица 2: Разреженная матрица личного сходства пользователя (Sparse Matrix)
CREATE TABLE user_product_affinity (
    id BIGSERIAL PRIMARY KEY,
    home_group_id VARCHAR(50) NOT NULL,
    master_product_id INT NOT NULL REFERENCES master_products(id) ON DELETE CASCADE,
    score INT NOT NULL CHECK (score >= 0 AND score <= 100), -- Балл предпочтения от 0 до 100
    last_interaction TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Дата для Крон-затухания
    CONSTRAINT unique_group_product_affinity UNIQUE (home_group_id, master_product_id)
);

-- Составной индекс для мгновенных LEFT JOIN операций
CREATE INDEX idx_user_affinity_composite ON user_product_affinity(home_group_id, master_product_id);

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

5.3.1 Сценарий А: Новая семья / Чистый профиль (HTTP 200 OK на базе системных дефолтов)

  • Условие: Новая группа пользователей только что зарегистрировалась, в таблице user_product_affinity нет ни одной строки (абсолютно пустая Sparse Matrix).
  • Действие системы: SQL-запрос отработает корректно за счет COALESCE. СУБД подтянет топ-20 продуктов, имеющих наивысший default_system_affinity (Хлеб, Молоко, Яйца). Семья получит базовый качественный набор пресетов первой необходимости.

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

  • Условие: Пользователь забил холодильник продуктами до отказа, физический объем абсолютно всех SKU на складе строго выше порога critical_min_threshold.
  • Действие системы: Фильтр WHERE отсечет все строки. Метод вернет статус 200 OK с пустым массивом suggestions: []. Интерфейс Flutter скроет горизонтальную карусель пресетов с экрана покупок, не перегружая UI лишними элементами.

5.3.3 Сценарий В: Таймаут СУБД при тяжелой аналитической сборке (HTTP 503 Service Unavailable)

  • Условие: База данных перегружена параллельными транзакциями списания, и JOIN-операция по 20 000 строкам превысила таймаут пула соединений в 1500 мс.
  • Действие FastAPI: Шлюз перехватывает исключение и возвращает код ошибки ERR-AFFINITY-TIMEOUT. Flutter-приложение гасит карусель рекомендаций, позволяя пользователю продолжить покупки через ручной ввод, не блокируя основной процесс импорта ресурсов.