Метод gRPC /analytics.v1.AffinityService/RegisterInteraction & Cron Decay

Документация API: Внутренний контур инкремента и алгоритм экспоненциального затухания разреженной матрицы Affinity

Published

July 2, 2026

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

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

  1. Динамический инкремент (gRPC-хук): Каждый раз, когда пользователь заносит продукты через Magnum/Small (Purchase) или фиксирует прием пищи (Consume), основные сервисы шлют gRPC-запрос. Система повышает балл сходства продукта на +5 пунктов (в жестком лимите от 0 до 100), обновляя таймстамп взаимодействия last_interaction.
  2. Экспоненциальное затухание по Крону (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-процесс автоматической пессимизации неактуальных товаров.

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


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.