# Цель документа
Проектирование механизма сортировки категорий по данным VisitorInterest
Ранее было проведено предварительное проектирование: https://gl.retailrocket.ru/projectsdocumentation/rr-25587-categoriesrecobyvi
# Терминология
1. Персональная вероятность для категории - значение вероятности покупки полученное из веса интереса сохраненного в коллекции интересов VI.
2. Общая вероятноcть для категории - значение вероятности покупки с нулевым значением веса, рассчитанное только из данных об intercept для категории общего каталога.
# Use-case'ы в домене VI
Было принято решение сделать Use-cases в домене VI максимально простыми, и определить основную логику формирования отсортированного списка категорий в домене рекомендаций.
Вероятность покупки необходимо получать:
- по интересу посетителя (если такой интерес существует)
- по intercept для категории в общем каталоге (если интереса посетителя к данной категории нет, либо вероятность, полученная по интересу посетителя < zeroProbability)
В таком случае можно разделить получение персональных вероятностей для списка категорий и получение общих вероятностей для списка категорий
## 1. Use-case получения пресональных вероятностей для категорий.
```
PersonalCategoryProbabilitiesQuery(
string PartnerId,
string VisitorId,
HashSet<long> CategoryIds,
)
returns list of CategoryProbability(
long CategoryId,
double(0.##) Probability
)
```
- Получает интересы к категориям из БД. (Не валидные категории уже отфильтрованы в процессе сохранения интересов в VI)
- Считает по этим интересам вероятности
- Возвращает список партнерских категорий с посчитанными вероятностями по интересам посетителя
## 2. Use-case получения общих вероятностей для категорий.
```
CategoryProbabilitiesQuery(
string PartnerId,
HashSet<long> CategoryIds,
)
returns list of CategoryProbability(
long CategoryId,
double(0.##) Probability
)
```
- Валидирует и матчит партнерские категории из запроса c категориями общего каталога
- Получает коэффиценты к сматченным категориям
- Считает по полученным intercept вероятности (считаем, что вес из интереса `personalWeight` = 0)
- Возвращает список партнерских категорий с посчитанными вероятностями по сматченным категориям общего каталога
- Вероятности для невалидных категорий не будут возвращены
# Use-case'ы в домене рекомендаций
## Use-case получения сортированного списка вероятностей для категорий.
```
VisitorInterestProbabilitiesQuery(
string partnerId,
List<long> categoryIds,
Visitor visitor
)
// Альтернативные варианты:
// - CategoryInterestProbabilitiesQuery
// - CategoryProbabilitiesQuery
returns list of VisitorInterestProbability(
long CategoryId,
double(0.##) Probability
)
```
- Запрашивает AlgorithmProbability для партнера по алгоритму Popular.
- Получает персональные вероятности используя первый use-case (PersonalCategoryProbabilitiesQuery).
- Выбирает из исходного списка категорий такие, что не попали в выдачу из первого use-case и такие для которых полученная вероятность меньше ZeroProbability.
- Для выбранных категорий запрашивает общие вероятности используя второй use-case (CategoryProbabilitiesQuery), с предварительной проверкой в кэше.
- Формируется список вероятностей для категорий:
- для категорий, у которых персональная вероятность больше ZeroProbability, учитывается персональная вероятность
- для остальных валидных категорий используется общая вероятность
- категории, отсутствующие в общем каталоге, не попадают в выдачу
- Список сортируется по убыванию вероятности.
## Как реализовать кросс-девайсность?
### Как получить visitorId?
На данный момент use-case'ы рекомендаций получают visitorId из cdv.api по сессии с помощью `CrossDeviceRecommendationDecorator`. Чтобы использовать этот декоратор use-case должен реализовывать интерфейс `IRecomQuery`.
<details>
<summary>IRecomQuery</summary>
```
public interface IRecomQuery<TResult> : IQuery<TResult>
{
string PartnerId { get; }
int Limit { get; }
Visitor Visitor { get; }
RecomAlgorithm Algorithm { get; }
string StockId { get; }
RecomCustomization Customization { get; }
IEnumerable<RecomModification> Modifications { get; }
IRecomQuery<TResult> Transform(AbTestFeatures abTestFeatures);
IRecomQuery<TResult> SetCustomization(RecomCustomization customization);
IRecomQuery<TResult> SetVisitor(Visitor visitor);
}
```
</details>
`IRecomQuery` имеет лишние свойства и методы, которые не потребуются в use-case получения сортированного списка категорий. Тем не менее, это самый простой способ получения visitorId, который позволяет использовать существующий код. В будущем планируется отказ от использования `IRecomQuery` в рекомендациях.
Минус использования существующего декоратора - общее ограничение по времени на выполнение запроса в cdv.api - 100ms (и fallback). В данном юз-кейсе не имеет смысл такое ограничение, мы хотим получать данные именно по visitorId, поэтому можно установить большее значение timeout (в рекомендациях есть общий fallback, который срабатывает при выполнении запроса более 1с, поэтому можно установить timeout для cdv.api например 800ms)
Альтернативные варианты получения visitorId:
- получать visitorId прямо в use-case, используя `CrossDeviceVisitorQueryHandler`
- написать отдельный декоратор, который не будет связан с `IRecomQuery`
Решили, что можно реализовать получение visitorId прямо в use-case, используя `CrossDeviceVisitorQueryHandler`. Соответственно timeout для срабатывания fallback при получении visitorId можно будет установить отдельно для этого use-case.
### Что делать, если visitorId не получен
Сейчас реализован механизм fallback, который позволяет получить рекомендации по одной сессии посетителя, если не удалось быстро получить visitorId.
Для use-case `VisitorInterestProbabilitiesQuery` можно рассмотреть два варианта действий, в случае, если visitorId не получен:
- Сортировать все категории по общим вероятностям
- Использовать дополнительный use-case VI, который возвращает персональные вероятности по интересам по сессии
С учетом того, что от коллекции VI по сессиям планируется отказаться, а доля fallback при обращении к cdv.api достаточно мала (а также timeout обращения в cdv.api будет увеличен), кажется можно обойтись сортировкой списка категорий по общим вероятностям
# Эндпойнты в RecommendationWebApi
## 1. Получение сортированного списка вероятностей для категорий по Id категорий.
`3.0/partnerRecommendations/sortedCategoryProbabilities`
Request:
```
[Required, ObjectId] string PartnerId
[ObjectId] string SessionId
[Required] List<long> CategoryIds
Link link
```
Response:
```
List<CategoryIdProbabilityModel> probabilities
```
Models:
```
CategoryIdProbabilityModel
{
long CategoryId,
double Probability
}
```
- Получает visitorId используя CrossDeviceVisitorQueryHandler
- Используют UseCase VisitorInterestProbabilitiesQuery напрямую для получения отсортированных вероятностей
- Формирует response (список categorId c вероятностями)
## 2. Получение сортированного списка вероятностей для категорий по CategoryPath.
`3.0/partnerRecommendations/sortedCategoryProbabilitiesByCategoryPath`
Request:
```
[Required, ObjectId] string PartnerId
[ObjectId] string SessionId
[Required] List<string> CategoryPaths
Link link
```
Response:
```
List<CategoryPathProbabilityModel> probabilities
```
Models:
```
CategoryPathProbabilityModel
{
string CategoryPath,
double Probability
}
```
- Получает visitorId используя CrossDeviceVisitorQueryHandler
- Хеширует CategoryPath
- Использует UseCase CategoryInterestProbabilityQuery для формирования списка категорий
- Мапит значения вероятностей к исходным CategoryPath
- Формирует response (список CategoryPath с вероятностями)
# Полезные метрики и логи
- Число категорий в запросе
- Количество вероятностей посчитанных по персональным интересам
- Количество вероятностей посчитанных по общему каталогу
- Метрики на Use-Case (VI, Рекомендации)
# Какой механизм кеширования мы могли бы использовать?
1. Кеширование сматченной категории из продуктового каталога. (10 мин). *Параметр фабрики use-case в VI*
2. Кэширование коэффицeнта к категории из общего каталога (1 час). *Параметр фабрики use-case в VI*
2. Кэшировать значений ZeroProbability для партнеров (1 час).
3. Более глубокое кэширование (например, кэширование выдачи интересов или персональных вероятностей) сейчас делать не предлагается, так как в такой кэш могут не попасть последние актуальные обработынные в VI события. В текущих VI рекомендациях никакого кэширования нету, а масштабное распространнеие этой фичи не планируется.
# Вопросы для обсуждения.
1. Значение вероятности полученное по Intercept всегда меньше вероятности полученной по интересам посетителя (т.к. intercept < 0, а вес интереса всегда > 0), отсюда вопрос, настолько ли важен учет ZeroProbability?
2. Стоит ли ограничивать число категорий в запросе? Число категорий в общем каталоге в данный момент ~3.5k
3. В случае если не удалось получить visitorId (404), стоит выполнить сортировку по общим вероятностям покупки?
4. Можно обсудить необходимость наследования IRecomQuery и следовательно способ получения VisitorId?
# Задачи
1. [VI] Реализовать Use-case получения пресональных вероятностей для категорий
- spec + тесты
- реализация
- проверить, что nuget-package корректно опубликован
2. [VI] Use-case получения общих вероятностей для категорий
- spec + тесты
- реализация
- проверить, что nuget-package корректно опубликован
3. [Recommendations] Реализовать use-case, эндпойнт и тесты для сортировки категорий
- тесты
- дешборды/метрики
- реализация
- тестирование на партнере