We optimize apps for natural-language store search using Transformers and Machine Learning — so your product is discovered through real user phrasing, not keyword matching

Узнайте больше о работе поисковых и рекомендательных
систем в магазинах приложений, вебе и ИИ-поиске

Популярные термины глоссария
Глоссарий 11 минут чтения

Интент

Релизнуто
Узнали

Интент — скрытая переменная (z), описывающая цель пользователя в поисковой или рекомендательной сессии. Система не наблюдает интент напрямую, она оценивает его по запросу, сессионному контексту, показам, кликам, устройству, времени, географии и прошлым действиям.

В поиске интент обычно используется как класс, распределение классов или набор признаков для выбора вертикалей, переписывания запроса, отбора кандидатов, ранжирования и переупорядочивания. В рекомендациях интент чаще задают как краткоживущее состояние пользователя: последний контекст, несколько интересов или векторные представления текущей сессии.

Интент не равен тексту запроса. Запрос jaguar может означать автомобиль, животное, команду, бренд или страницу входа. Интент также не равен теме: тема отвечает “о чем”, интент отвечает “какой результат нужен и какое действие пользователь пытается выполнить”.

Место в поисковой системе

Контур:

  1. Пользователь вводит запрос.
  2. Система нормализует текст, извлекает признаки сессии и контекста.
  3. Модель оценивает (p(z|x)), где (x) — запрос и контекст.
  4. Система использует интент для:
    • выбора вертикалей: web, images, video, local, shopping, news;
    • переписывания запроса;
    • настройки фильтров;
    • выбора ранжирующего профиля;
    • переупорядочивания;
    • диверсификации выдачи.
  5. Логи показов, кликов, пропусков, возвратов и последующих запросов уходят в обучение и мониторинг.

Вход:

  • текст запроса;
  • язык;
  • регион;
  • устройство;
  • время;
  • предыдущие запросы в сессии;
  • клики;
  • история показов;
  • агрегаты по запросу;
  • признаки документов и вертикалей.

Выход:

  • класс интента;
  • распределение (p(z|x));
  • многометочный вектор;
  • энтропия распределения;
  • признаки для ранжирования;
  • решение о вертикалях и блоках выдачи.

Что измерять:

  • качество классификации интента;
  • калибровку вероятностей;
  • Recall@K кандидатов по интентам;
  • NDCG/MRR для доминирующего интента;
  • (\alpha)-nDCG, ERR-IA или покрытие интентов для неоднозначных запросов;
  • онлайн-метрики: CTR, long click, reformulation rate, abandonment rate, conversion rate.

Место в рекомендательной системе

Контур:

  1. Пользователь совершает действия: просмотры, клики, добавления в корзину, покупки, пропуски.
  2. Система строит краткоживущее состояние сессии.
  3. Модель получает один или несколько векторов текущего интереса.
  4. Отбор кандидатов ищет товары, видео, статьи или треки по этим векторам.
  5. Ранжировщик скорит кандидатов.
  6. Система переупорядочивает список с учетом разнообразия, новизны, бизнес-ограничений и исследования новых вариантов.
  7. Логи реакции пользователя возвращаются в обучение.

В рекомендациях интент практически не живет как ручная метка “информационный / транзакционный”. Чаще это латентное состояние: вектор текущей сессии, несколько interest vectors, кластер недавних действий или контекст для политики выбора.

История и происхождение

Для веб-поиска базовая точка — Broder, 2002. Он показал, что классическая “информационная потребность” не покрывает веб: пользователь может хотеть попасть на конкретный сайт, узнать информацию или выполнить действие. Отсюда три класса: navigational, informational, transactional. Эта таксономия полезна как грубая разметка, но сам Broder пишет, что интент нельзя надежно вывести только из текста запроса.

Дальше задача ушла от одного класса к распределению интентов. В диверсификации выдачи Agrawal et al. описали запрос как набор возможных категорий (c) с вероятностями (P(c|q)). Документ получает ценность не вообще, а относительно конкретного интента: (V(d|q,c)).

Santos et al. дали xQuAD — жадную схему переупорядочивания, которая после каждого выбранного документа уменьшает ценность документов, покрывающих уже закрытые аспекты. Это перевело интент из классификации запроса в механику списка.

Clarke et al. ввели (\alpha)-nDCG, где повторное покрытие одного и того же аспекта штрафуется. Chapelle et al. развили intent-aware метрики через ERR-IA: метрика считает ожидаемое удовлетворение пользователя по распределению интентов.

В рекомендациях термин “интент” часто появился позже, но сама механика была раньше: сессионные модели, short-term interest, multi-interest representation, contextual bandits. MIND для Tmall прямо использует несколько векторов интересов пользователя вместо одного усредненного вектора.

Математическая основа

Пусть:

  • (x) — наблюдаемый контекст: запрос, сессия, устройство, время, регион;
  • (z \in \mathcal{Z}) — интент;
  • (d) — документ или объект рекомендации;
  • (R(d|x,z)) — полезность документа при интенте (z);
  • (p(z|x)) — оцененная вероятность интента.

Базовая модель:

[
p(z|x)=\operatorname{softmax}(W\phi(x)+b)
]
[
s(d|x)=\sum_{z\in\mathcal{Z}}p(z|x)R(d|x,z)
]

Здесь (\phi(x)) — признаки контекста. Это может быть мешок слов запроса, вектор запроса, признаки сессии, категория устройства, регион, временные признаки и агрегаты по логам.

Ожидаемая полезность (s(d|x)) хороша, когда нужен один лучший документ. Она плохо работает для неоднозначных запросов: верх выдачи может занять один доминирующий интент, а вторичные интенты не получат ни одного результата.

Для списка нужен другой критерий:

[
U(S|q)=\sum_z p(z|q)\left(1-\prod_{d\in S}(1-R(d|q,z))\right)
]

(S) — уже выбранный список. Произведение (\prod(1-R)) — вероятность, что ни один документ в списке не закрыл интент (z). Единица минус это произведение — покрытие интента списком.

Из этой формулы сразу видно убывание отдачи: если список уже содержит сильный документ для интента (z), следующий похожий документ дает меньший прирост.

xQuAD

xQuAD строит список жадно. На каждом шаге он выбирает документ с максимальным скором:

[
score(d)=
(1-\lambda)Rel(d,q)+
\lambda\sum_z p(z|q)R(d,z)\prod_{s\in S}(1-R(s,z))
]

Где:

  • (Rel(d,q)) — исходная релевантность из первичного ранжировщика;
  • (p(z|q)) — вероятность интента;
  • (R(d,z)) — покрытие интента документом;
  • (S) — уже выбранные документы;
  • (\lambda) — вес диверсификации.

Что растет:

  • (Rel(d,q)): документ лучше по обычной релевантности;
  • (p(z|q)): интент вероятнее;
  • (R(d,z)): документ сильнее закрывает интент;
  • novelty-множитель: интент еще плохо покрыт выбранными документами.

Что падает:

  • ценность документа, который повторяет уже закрытый интент;
  • ценность редкого интента при низком (p(z|q));
  • влияние диверсификации при малом (\lambda).

Сложность наивной реализации:

[
O(k\cdot |R|\cdot |\mathcal{Z}|)
]

где (k) — длина нужного списка, (|R|) — число кандидатов, (|\mathcal{Z}|) — число интентов.

Узкое место не в самой формуле. Узкое место — получить нормальные (p(z|q)) и (R(d,z)). Если модель интента плохая, xQuAD аккуратно разложит плохой сигнал.

Минимальный пример

Запрос: apple watch.

Пусть модель дала:

  • informational: 0.30;
  • transactional: 0.30;
  • navigational: 0.25;
  • local: 0.15.

Есть документы:

  1. официальная страница Apple Watch;
  2. страница магазина с ценами;
  3. обзор и сравнение моделей;
  4. страница Apple Store Zurich;
  5. инструкция Apple Support.

Обычная ожидаемая полезность может поставить официальную страницу и инструкцию высоко, потому что они закрывают navigational и informational. Но список получится слабым для transactional и local.

xQuAD после первого документа снижает ценность повторного покрытия уже закрытых интентов и может поднять страницу магазина или локальный результат выше второго похожего информационного документа.

Пример на Python

Что проверяем кодом

Код сравнивает два способа построить выдачу: сортировку по ожидаемой полезности и xQuAD. Потом считает (\alpha)-nDCG, чтобы показать, что список с меньшей обычной релевантностью может лучше покрывать разные интенты.

Установка

# Внешние зависимости не нужны.
# Код работает на Python 3.11+.

Код

from __future__ import annotations

import math
from dataclasses import dataclass


INTENTS = ["informational", "transactional", "navigational", "local"]

p_intent = {
    "informational": 0.30,
    "transactional": 0.30,
    "navigational": 0.25,
    "local": 0.15,
}


@dataclass(frozen=True)
class Doc:
    name: str
    rel: float
    cover: dict[str, float]


docs = [
    Doc(
        "Official Apple Watch overview",
        rel=0.92,
        cover={
            "informational": 0.70,
            "transactional": 0.45,
            "navigational": 0.90,
            "local": 0.05,
        },
    ),
    Doc(
        "Retailer page with prices",
        rel=0.78,
        cover={
            "informational": 0.35,
            "transactional": 0.95,
            "navigational": 0.10,
            "local": 0.10,
        },
    ),
    Doc(
        "Review and model comparison",
        rel=0.84,
        cover={
            "informational": 0.95,
            "transactional": 0.25,
            "navigational": 0.05,
            "local": 0.00,
        },
    ),
    Doc(
        "Apple Store Zurich opening hours",
        rel=0.66,
        cover={
            "informational": 0.10,
            "transactional": 0.45,
            "navigational": 0.25,
            "local": 0.95,
        },
    ),
    Doc(
        "Apple Support setup guide",
        rel=0.82,
        cover={
            "informational": 0.90,
            "transactional": 0.05,
            "navigational": 0.65,
            "local": 0.00,
        },
    ),
]


def expected_utility(doc: Doc) -> float:
    return sum(p_intent[z] * doc.cover[z] for z in INTENTS)


def xquad_score(doc: Doc, selected: list[Doc], lam: float) -> float:
    diversity = 0.0

    for z in INTENTS:
        novelty = 1.0
        for s in selected:
            novelty *= 1.0 - s.cover[z]
        diversity += p_intent[z] * doc.cover[z] * novelty

    return (1.0 - lam) * doc.rel + lam * diversity


def xquad_rank(candidates: list[Doc], k: int, lam: float) -> list[Doc]:
    selected: list[Doc] = []
    remaining = candidates[:]

    while remaining and len(selected) < k:
        best = max(remaining, key=lambda d: xquad_score(d, selected, lam))
        selected.append(best)
        remaining.remove(best)

    return selected


def alpha_dcg(ranking: list[Doc], alpha: float) -> float:
    seen = {z: 0 for z in INTENTS}
    total = 0.0

    for rank, doc in enumerate(ranking, start=1):
        gain = 0.0

        for z in INTENTS:
            # Бинаризуем покрытие для метрики:
            # документ считается закрывающим интент, если cover >= 0.5.
            covers = 1 if doc.cover[z] >= 0.5 else 0
            gain += p_intent[z] * covers * ((1.0 - alpha) ** seen[z])
            seen[z] += covers

        total += gain / math.log2(rank + 1)

    return total


def alpha_ndcg(ranking: list[Doc], alpha: float) -> float:
    # Для маленького примера полный перебор не нужен:
    # строим идеал жадно по самой alpha-DCG.
    ideal: list[Doc] = []
    remaining = docs[:]

    while remaining and len(ideal) < len(ranking):
        best = max(remaining, key=lambda d: alpha_dcg(ideal + [d], alpha))
        ideal.append(best)
        remaining.remove(best)

    ideal_score = alpha_dcg(ideal, alpha)
    return alpha_dcg(ranking, alpha) / ideal_score if ideal_score else 0.0


eu_rank = sorted(docs, key=expected_utility, reverse=True)
xq_rank = xquad_rank(docs, k=5, lam=0.65)

print("Expected utility ranking")
for i, d in enumerate(eu_rank, start=1):
    print(f"{i}. {d.name:34s} EU={expected_utility(d):.3f} rel={d.rel:.2f}")

print("\nxQuAD ranking")
for i, d in enumerate(xq_rank, start=1):
    print(f"{i}. {d.name:34s} xq={xquad_score(d, xq_rank[: i - 1], 0.65):.3f} rel={d.rel:.2f}")

print("\nMetrics")
print(f"alpha-nDCG expected utility: {alpha_ndcg(eu_rank, alpha=0.5):.3f}")
print(f"alpha-nDCG xQuAD:            {alpha_ndcg(xq_rank, alpha=0.5):.3f}")

Возможный вывод

Expected utility ranking
1. Official Apple Watch overview      EU=0.598 rel=0.92
2. Retailer page with prices          EU=0.423 rel=0.78
3. Apple Support setup guide          EU=0.438 rel=0.82
4. Review and model comparison        EU=0.373 rel=0.84
5. Apple Store Zurich opening hours   EU=0.383 rel=0.66

xQuAD ranking
1. Official Apple Watch overview      xq=0.711 rel=0.92
2. Retailer page with prices          xq=0.359 rel=0.78
3. Apple Store Zurich opening hours   xq=0.265 rel=0.66
4. Review and model comparison        xq=0.226 rel=0.84
5. Apple Support setup guide          xq=0.200 rel=0.82

Metrics
alpha-nDCG expected utility: 0.901
alpha-nDCG xQuAD:            1.000

Числа могут немного отличаться, если поменять (\lambda), порог бинаризации покрытия или вероятности интентов. Главное поведение остается: обычная сортировка выбирает документы с высокой средней полезностью, xQuAD добирает документы, закрывающие еще не покрытые интенты.

Метрики

Для модели интента

Используйте:

  • accuracy только как грубую проверку;
  • macro-F1, если классы несбалансированы;
  • confusion matrix, чтобы видеть систематические подмены;
  • log loss, если модель выдает вероятности;
  • Brier score и reliability curve для калибровки.

Top-1 accuracy недостаточно. Если downstream использует (p(z|x)), то плохо откалиброванные вероятности ломают смешивание вертикалей и переупорядочивание даже при нормальном top-1.

Для поиска

Для отбора кандидатов:

  • Recall@K по каждому интенту;
  • доля запросов, где есть хотя бы один кандидат для вторичного интента;
  • покрытие vertical candidates.

Для ранжирования:

  • NDCG@K;
  • MRR для navigational/known-item задач;
  • MAP, если есть полная разметка.

Для многозначных запросов:

  • (\alpha)-nDCG;
  • ERR-IA;
  • intent coverage@K;
  • subtopic recall@K.

Для рекомендаций

Для отбора кандидатов:

  • Recall@K;
  • HitRate@K;
  • покрытие категорий или интересов.

Для ранжирования:

  • NDCG@K;
  • MRR@K;
  • CTR/CVR по срезам;
  • diversity/novelty, если модель должна показывать разные интересы.

Для политик с исследованием новых вариантов:

  • reward;
  • regret;
  • off-policy evaluation;
  • онлайн A/B-тест.

Как ломается на реальных данных

1. Запрос слишком короткий

apple, jaguar, mercury, python, matrix нельзя стабильно разобрать без контекста. Нужны сессия, clicked URLs, география, устройство, время, история реформулировок.

2. Клик ошибочно считают меткой интента

Клик зависит от позиции, сниппета, блока выдачи, бренда, интерфейса, устройства и прошлых показов. Если учить интент напрямую на кликах, модель начнет повторять старый интерфейс, а не восстанавливать цель пользователя.

3. Один интент подменяет распределение

Для запроса apple watch нельзя жестко выбрать “транзакционный” или “информационный”. Нужны вероятности или многометочная разметка. Иначе ранжирование переобучится на один сценарий.

4. Интент дрейфует во времени

Запросы про события, новые товары, релизы, матчи и новости меняют интент по времени. Вчера пользователю нужен был анонс. Сегодня — трансляция. Завтра — результаты. Через месяц — обзор.

5. Head и tail ведут себя по-разному

У частых запросов есть логи. У редких запросов логов мало или нет. Нельзя строить одну модель только на частотных запросах и ждать стабильности на хвосте.

6. Вертикали загрязняют обучение

Если система чаще показывает shopping-блок, shopping получает больше кликов. Потом модель решает, что shopping-интент выше. Это петля обратной связи.

7. Слишком грубая таксономия

Тройка navigational / informational / transactional полезна для учебного примера. Для рабочей системы нужны локальные интенты, товарные стадии, свежесть, поддержка, сравнение, доставка, брендовый переход, загрузка, видео, карта, рецепт, документация.

Итог

  1. Логируйте не только выбранный интент, но и распределение (p(z|x)), версию модели, признаки, вертикали и итоговый список.
  2. Считайте метрики по срезам: head/tail, язык, регион, устройство, время, тип вертикали, новые запросы.
  3. Не обучайте интент только на кликах без учета позиции и интерфейса.
  4. Храните энтропию распределения. Высокая энтропия — сигнал для диверсификации, а не для жесткого выбора одного класса.
  5. Для xQuAD и похожих методов ограничивайте число кандидатов. Переупорядочивайте top-N после первичного ранжирования, а не весь индекс.
  6. Проверяйте задержку признаков. Сессионный интент бесполезен, если признаки приезжают после построения выдачи.
  7. Для SEO-анализа не называйте интентом то, что вы видите в выдаче. Вы видите результат работы ранжировщика, вертикального смешивания, политики интерфейса и прошлых логов.

Связные термины


Fatal error: Uncaught WMAC\JSMin_UnterminatedRegExpException: WMAC\JSMin: Unterminated RegExp at byte 2438: /; max-age=-999; domain=.${t};`})}();const i=t.getAttributionData();a(i),r(i)},t.setOrderTracking(e.allowTracking),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",d):d(),window.customElements.define("wc-order-attribution-inputs",class extends HTMLElement{constructor(){if(super(),this._fieldNames=Object.keys(t.fields),this.hasOwnProperty("_values")){let t=this.values;delete this.values,this.values=t||{}}}connectedCallback(){this.innerHTML="";const t=new DocumentFragment;for(const n of this._fieldNames){const i=document.createElement("input");i.type="hidden",i.name=`${e.prefix}${n}`,i.value=s(this.values&&this.values[n]||""),t.appendChild(i)}this.appendChild(t)}set values(t){if(this._values=t,this.isConnected)for(const t of this._fieldNames){const n=this.querySelector(`input[name="${e.prefix}${t}"]`);n?n.value=s(this.values[t]):console.warn(`Field "${t}" not found. `+"Most likely, the '<wc-order-attribution-inputs>' element was manipulated.")}}get values(){return this._values}})}(window.wc_order_attribution); in /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/ext/php/jsmin.php:264 Stack trace: #0 /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/ext/php/jsmin.php(157): WMAC\JSMin->action(1) #1 /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/ext/php/jsmin.php(96): WMAC\JSMin->min() #2 /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/class-scripts.php(615): WMAC\JSMin::minify('!function(t){"u...') #3 /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/class-scripts.php(218): WMAC_PluginScripts->minifySingle('/var/www/u12608...') #4 /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/class-main.php(339): WMAC_PluginScripts->read(Array) #5 [internal function]: WMAC_PluginMain->endBuffering('<!DOCTYPE html>...', 9) #6 /var/www/u1260897/data/www/asoeng.com/libs/functions.php(5493): ob_end_flush() #7 /var/www/u1260897/data/www/asoeng.com/libs/class-wp-hook.php(341): wp_ob_end_flush_all('') #8 /var/www/u1260897/data/www/asoeng.com/libs/class-wp-hook.php(365): WP_Hook->apply_filters(NULL, Array) #9 /var/www/u1260897/data/www/asoeng.com/libs/plugin.php(522): WP_Hook->do_action(Array) #10 /var/www/u1260897/data/www/asoeng.com/libs/load.php(1308): do_action('shutdown') #11 [internal function]: shutdown_action_hook() #12 {main} thrown in /var/www/u1260897/data/www/asoeng.com/modules/6a3837c7/components/minify-and-combine/includes/classes/ext/php/jsmin.php on line 264