sequenceDiagram
autonumber
%% КОНТУР А: СИНХРОННЫЙ ИНКРЕМЕНТ
Note over CS, AS: Контур А: Синхронное обновление при покупке/потреблении
CS->>AS: gRPC RegisterInteraction(home_group_id, master_product_id)
activate AS
AS->>DB: UPSERT user_product_affinity SET score = score + 5, last_interaction = NOW()
activate DB
DB-->>AS: Подтверждение записи (Новый score = 85)
deactivate DB
AS-->>CS: gRPC AffinityInteractionResponse(status="UPSERTED_SUCCESS", score=85)
deactivate AS
%% КОНТУР Б: АСИНХРОННЫЙ КРОН
Note over CRON, DB: Контур Б: Ночное затухание неактуальных весов (03:00)
CRON->>DB: UPDATE user_product_affinity SET score = score * 0.95 WHERE last_interaction < NOW() - 14 days
activate DB
DB-->>CRON: ЛОГ: "Успешно пессимизировано 1,420 устаревших строк"
deactivate DB
Метод gRPC /analytics.v1.AffinityService/RegisterInteraction & Cron Decay
Документация API: Внутренний контур инкремента и алгоритм экспоненциального затухания разреженной матрицы Affinity
1 Функциональное назначение
Контур предназначен для непрерывной адаптации и калибровки весов личных предпочтений пользователя в разреженной матрице (user_product_affinity). Сервис отслеживает жизненный цикл продуктов и решает две ключевые задачи:
- Динамический инкремент (gRPC-хук): Каждый раз, когда пользователь заносит продукты через Magnum/Small (
Purchase) или фиксирует прием пищи (Consume), основные сервисы шлют gRPC-запрос. Система повышает балл сходства продукта на +5 пунктов (в жестком лимите от 0 до 100), обновляя таймстамп взаимодействияlast_interaction. - Экспоненциальное затухание по Крону (Exponential Decay): Если пользователь перестает покупать или есть продукт, его интерес угасает. Фоновый воркер по расписанию снижает балл старых записей, очищая карусель Flutter от неактуальных рекомендаций.
2 Протокол gRPC-взаимодействия (Синхронный контур)
- Протокол:
gRPCповерхHTTP/2 - Тип вызова: Unary RPC (вызывается шлюзом FastAPI в фоне после коммитов списания/закупки)
- Пакет (Package):
analytics.v1 - Сервис (Service):
AffinityService - Метод:
RegisterInteraction
2.1 Спецификация контракта (Protocol Buffers)
Контракт описывается в файле affinity.proto. Шлюз передает идентификатор группы и ID продукта из глобального каталога Master Data.
syntax = "proto3";
package analytics.v1;
service AffinityService {
// Вызывается бэкенд-шлюзом при покупке или потреблении товара пользователем
rpc RegisterInteraction (AffinityInteractionRequest) returns (AffinityInteractionResponse);
}
message AffinityInteractionRequest {
string home_group_id = 1; // ID семьи или офиса из JWT-токена
int32 master_product_id = 2; // Целевой продукт из глобального справочника mp.id
}
message AffinityInteractionResponse {
string status = 1; // "UPSERTED_SUCCESS"
int32 updated_score = 2; // Новый балл в базе данных (например, 85)
}3 Схема обработки и затухания весов (Mermaid)
Диаграмма описывает два параллельных процесса: синхронное наращивание баллов при действиях пользователя и ночной Cron-процесс автоматической пессимизации неактуальных товаров.
4 Алгоритм экспоненциального затухания по Крону (СУБД Логика)
Фоновый системный планировщик (Cron-job) запускается каждые 24 часа ровно в 03:00 ночи, когда нагрузка на реляционную базу данных минимальна. Он сканирует разреженную матрицу и применяет формулу затухания интереса к тем продуктам, с которыми не было взаимодействий (закупок, утилизаций, съедений) более 14 дней.
Балл умножается на понижающий коэффициент 0.95 (пессимизация на 5% в сутки). Чтобы база данных не выполняла пустую работу, процесс игнорирует строки, у которых балл уже опустился до базового порога отсечения (score <= 10).
4.1 Архитектурный SQL-запрос Крон-воркера:
-- Шаг 2 (Контур Б): Ночное каскадное снижение устаревших весов предпочтений
UPDATE user_product_affinity
SET
score = FLOOR(score * 0.95)::INT, -- Уменьшаем текущий балл на 5% с округлением вниз
last_interaction = NOW() -- Сдвигаем таймстамп, чтобы не зациклить апдейт завтра
WHERE
last_interaction < NOW() - INTERVAL '14 days' -- Критерий: к товару не прикасались 2 недели
AND score > 10; -- Порог отсечения: не пессимизируем в абсолютный ноль5 Вилки исключений и Fail-Safe стратегии
5.0.1 Сценарий 1: Первая покупка экзотического SKU (Автогенерация ячейки Sparse Matrix)
Условие: Пользователь впервые купил Авокадо (
mp.default_system_affinity = 15). В таблице личного аффинити этой семьи строки с даннымmaster_product_idфизически не существует.Действие СУБД (Upsert на Шаге 2): Благодаря конструкции
ON CONFLICTбаза данных автоматически создает новую физическую ячейку в разреженной матрице. Первичное значение рассчитывается как:Дефолт из справочника (15) + Инкремент за действие (5) = 20 баллов.SQL-выражение для gRPC метода:
INSERT INTO user_product_affinity (home_group_id, master_product_id, score, last_interaction) VALUES (\$1, \$2, (SELECT default_system_affinity FROM master_products WHERE id = \$2) + 5, NOW()) ON CONFLICT (home_group_id, master_product_id) DO UPDATE SET score = LEAST(user_product_affinity.score + 5, 100), last_interaction = NOW();
5.0.2 Сценарий 2: Достижение абсолютного максимума (Потолок Affinity)
- Условие: Семья пьет молоко каждый день, и балл предпочтения достиг верхнего лимита
score = 100. - Fail-Safe стратегия: Функция СУБД
LEAST(score + 5, 100)блокирует переполнение разрядности типа данных. Балл жестко фиксируется на отметке 100, предотвращая технические коллизии переполнения и некорректное поведение Recall Engine.