A/B-эксперименты
Поисковые эксперименты делят живой трафик между двумя профилями поиска с закреплением за посетителем, показывают клики и uplift и позволяют принять или откатить победителя. Также описано старое разделение через settings.abTest.
Работа над релевантностью остаётся догадками, пока её не измерили. A/B-эксперимент направляет каждый API-ключ всегда в одну из двух конфигураций и позволяет журналу запросов ответить на вопрос: какая конфигурация даёт меньше пустых выдач и выше оценки совпадений?
Поисковые эксперименты
Поисковый эксперимент относится к одному индексу и сравнивает две ветки, control и treatment, каждая из которых указывает на сохранённый профиль поиска (или на профиль индекса по умолчанию, если профиль не задан). Пока эксперимент в статусе running, каждый поиск через REST и каждый поиск виджета на этом индексе получает профиль одной из веток.
curl -sS -X POST "$AAC_API/experiments?index=documentation" \
-H "Authorization: Bearer $AAC_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"weights v2","controlProfileId":"prof_control","treatmentProfileId":"prof_weights_v2","treatmentWeight":50,"status":"running"}'| Поле | Значение |
|---|---|
name | Обязательное, до 200 символов |
controlProfileId / treatmentProfileId | Профили этого индекса; null — профиль индекса по умолчанию |
treatmentWeight | Доля единиц в treatment, 0–100, по умолчанию 50. 0 и 100 отправляют всех в одну ветку, не останавливая эксперимент |
controlName / treatmentName | Подписи, по умолчанию A и B |
status | draft (по умолчанию), running, paused, finished |
Все маршруты /experiments принимают ?index=<slug> и требуют ключ с settings:write. В консоли то же самое доступно на странице индекса Эксперименты.
| Маршрут | Что делает |
|---|---|
GET /experiments | Список экспериментов индекса |
POST /experiments | Создать эксперимент |
PATCH /experiments/{id} | Изменить поля или статус |
POST /experiments/{id}/promote | Сделать профиль treatment (или control, если treatment не задан) профилем индекса по умолчанию и завершить эксперимент |
POST /experiments/{id}/rollback | Вернуть профиль control по умолчанию и завершить эксперимент |
GET /experiments/{id}/report | По веткам: назначения, поиски, клики, CTR, конверсии, доля конверсий, выручка и uplift; days 1–90, по умолчанию 14 |
Смена статуса отправляет вебхуки experiment.started и experiment.finished.
Кто попадает в какую ветку
Назначение — это хэш ключа единицы, поэтому одна и та же единица всегда получает одну и ту же ветку на всех инстансах и после деплоев. Ключ единицы — первое из:
session_id— ваш постоянный анонимный id посетителя (client_idв виджете),user_id,visit_id,- иначе API-ключ (REST) или издатель сессии встраивания (виджет) — весь такой трафик попадает в одну ветку.
Передавайте стабильный session_id с фронтенда; без него весь серверный ключ — одна единица. Первое назначение единицы сохраняется и больше не меняется, так что посетитель остаётся в своей ветке на всё время эксперимента.
На индексе одновременно обслуживается только один эксперимент: если в статусе running несколько, используется один из них. Явный profile= в запросе всегда важнее эксперимента — так консоль показывает вариант в превью, не мешая эксперименту.
Как читать отчёт
Каждый поиск, решение по которому принял эксперимент, записывается в лог вместе с веткой, а каждое событие несёт query_id поиска, из которого оно пришло, — поэтому атрибуция точна для любого вида единицы (ключ, посетитель, пользователь, визит). По каждой ветке отчёт показывает назначения (единицы, попавшие в ветку), поиски, обслуженные веткой, и клики, конверсии и выручку (revenue_cents — сумма value_cents конверсий) по этим поискам. Передавайте query_id в событиях — события без него не атрибутируются. ctr и conversion_rate считаются на поиск; uplift — относительная разница CTR treatment к control, либо null, пока у одной из сторон нет данных. Поиски, записанные до появления этой атрибуции, не учитываются.
Старый механизм: settings.abTest
У индекса по-прежнему может быть старое разделение по ключам в настройках. Оно применяется, только если не запущен ни один эксперимент и запрос не указывает профиль. Для новых задач используйте поисковые эксперименты; остальная часть страницы описывает старый механизм.
Как работает разделение
Эксперимент — свойство индекса, хранится в его настройках:
{
"abTest": {
"enabled": true,
"profiles": ["control", "weights-v2"],
"split": 50
}
}| Поле | Значение |
|---|---|
enabled | Включает эксперимент |
profiles | Ровно два слага профилей; оба должны существовать у индекса |
split | Процент корзин, отправляемых в первый профиль, по умолчанию 50 |
Назначение детерминированное, а не случайное для каждого запроса:
const bucket = createHash("sha256").update(`${apiKey.keyId}:${index.id}`).digest()[0] % 100;
const profile = bucket < (abTest.split ?? 50) ? abTest.profiles[0] : abTest.profiles[1];Отсюда два следствия, и оба важны:
- Конкретный ключ всегда попадает в одну корзину. Пользователи не перескакивают между вариантами посреди сессии, а закешированная страница не противоречит последнему результату.
- Единица рандомизации — ключ, а не посетитель. Один серверный ключ означает одну корзину для всего его трафика: годится для сравнения конфигураций между тенантами, но бесполезно для разбиения по пользователям. Для последнего выпускайте по ключу на когорту или передавайте
profileявно.
Явно переданный profile всегда побеждает A/B-корзину — так можно посмотреть вариант в консоли, не сбивая эксперимент.
Чтение результатов
curl -sS "$AAC_API/analytics/experiments?days=30&index=documentation" \
-H "Authorization: Bearer $AAC_KEY"{
"days": 30,
"index": "documentation",
"buckets": [
{
"profile": "control",
"searches": 8240,
"zero_rate": 0.11,
"avg_latency_ms": 18,
"avg_top_score": 1720.4
},
{
"profile": "weights-v2",
"searches": 8106,
"zero_rate": 0.07,
"avg_latency_ms": 19,
"avg_top_score": 2145.8
}
],
"request_id": "req_3b5d7f9a1c3e5b7d9f1a3c5e"
}| Поле | Значение |
|---|---|
profile | Слаг профиля, с которым выполнялись запросы |
searches | Число запросов за период, отнесённых к профилю |
zero_rate | Доля запросов, вернувших пустую выдачу |
avg_latency_ms | Средняя задержка по профилю |
avg_top_score | Средняя оценка первого попадания — грубый прокси релевантности |
days ограничивается 1–90. Без index корзины агрегируются по всем индексам организации — удобно, когда одна и та же пара профилей применена везде.
В выборку попадают только запросы, выполненные под профилем, поэтому суммы будут меньше общего числа поисков: органический трафик по ключам вне эксперимента, вызовы semantic с явным профилем, предпросмотр в консоли и запросы подсказок сюда не входят.
Рабочий порядок
- Создайте профиль-кандидат. Скопируйте контрольный и измените одну вещь — обычно
query_by_weightsилиnum_typos, иногдаfacet_by. - Посмотрите на него. Отправьте
?profile=weights-v2на реальных запросах в консоли;GET /indexes/{slug}/widget-configперечисляет профили и их конфигурации, если нужно сравнить их программно. - Включите
abTestс обоими слагами и делением 50/50. Дайте ему поработать минимум неделю: за день трафика 11% и 7% пустых выдач не различить. - Прочитайте
/analytics/experimentsи сверьтесь с/analytics/overview, чтобы убедиться, что структура общего трафика за окно не изменилась. - Решите и уберите за собой. Переведите победителя в профиль по умолчанию, выключите
abTest, а проигравшего оставьте как документированный путь отката.
Подводные камни
- Два профиля, не больше. Схема — это пара плюс процент; для трёх вариантов нужен второй индекс или другой механизм.
- Неравный трафик нормален. Хеширование равномерно по ключам, а не по запросам: при трёх активных ключах деление 50/50 может дать 2:1.
avg_top_scoreнельзя сравнивать между разными запросами. Он сравнивает два профиля на одном и том же наборе запросов; сравнение с прошлым месяцем — это сравнение с другими запросами.- Задержка — не метрика решения. Гибридный профиль будет медленнее и лучше; решайте по релевантности, а потом проверяйте, приемлема ли цена по задержке.
- Правка профиля посреди эксперимента его обнуляет. Корзины вычисляются по сохранённой конфигурации во время запроса, поэтому изменение применяется к идущему трафику и размывает сравнение.
Где это настраивается
abTest живёт в объекте настроек индекса. Публичный REST API его не предоставляет: настройками индекса владеет консоль, а публичная поверхность по отношению к ним только для чтения. Задавайте его через настройки индекса в консоли или, для автоматизации, через oRPC-процедуры консоли из аутентифицированной сессии.