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

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

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

Токен

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

В лексическом поиске токен — экземпляр последовательности символов, выделенный анализатором из конкретного поля документа или запроса. Токен может содержать текстовое значение, позицию, начальное и конечное смещения, тип и длину позиции. После фильтрации и нормализации его значение может быть записано как термин индекса.

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

В последовательных рекомендательных моделях идентификатор объекта можно считать токеном, когда модель явно обрабатывает историю взаимодействий как последовательность дискретных символов. Например, BERT4Rec скрывает отдельные объекты в истории и учится восстанавливать их.

Границы термина

В классической терминологии информационного поиска:

  • токен — конкретное вхождение последовательности символов в документе;
  • тип — класс одинаковых последовательностей;
  • термин — тип, возможно после нормализации, записанный в словарь поисковой системы.

Лексема — единица языка, объединяющая словоформы. Она не равна токену. Строки иду, шел и идти могут относиться к одной лексеме, но остаются разными токенами до лемматизации.

Субсловный токен — фрагмент слова или байтовой последовательности из словаря нейросетевой модели.

N-грамма — последовательность из n символов, токенов или других единиц. N-граммы могут создавать как токенизаторы, так и фильтры; они не обязаны возникать после выделения слов. Elasticsearch отдельно предоставляет ngram и edge_ngram токенизаторы.

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

Лексический поиск

Контур:

  1. Парсер извлекает текст из HTML, PDF, JSON или другого формата.
  2. Символьные фильтры меняют исходную строку.
  3. Токенизатор выделяет токены.
  4. Фильтры токенов меняют, удаляют или добавляют элементы потока.
  5. Значения токенов записываются в словарь поля и инвертированные списки.
  6. Запрос проходит собственную цепочку анализа.
  7. Поисковая функция извлекает документы и вычисляет оценки BM25, фразового или другого запроса.

Lucene получает уже извлеченный обычный текст. Разбор HTML, PDF и других форматов находится за пределами библиотеки. Внутри анализатора CharFilter меняет символы и корректирует смещения, Tokenizer выделяет токены, TokenFilter меняет поток после токенизации.

Вход:

  • строка Unicode;
  • имя поля;
  • версия анализатора;
  • правила нормализации;
  • при обработке документов — один вызов на индексируемое поле;
  • при поиске — один вызов на анализируемую часть запроса.

Выход:

[
A_f(x) = (\tau_1,\tau_2,\ldots,\tau_m),
]

где токен можно представить как

[
\tau_i =
(v_i,\ p_i,\ o_i^{start},\ o_i^{end},\ type_i,\ len_i).
]

Здесь:

  • (v_i) — текстовое значение;
  • (p_i) — позиция;
  • (o_i^{start}), (o_i^{end}) — смещения в исходном тексте;
  • (type_i) — тип;
  • (len_i) — число позиций, занимаемых токеном.

Из потока строится инвертированный список:

[
P_f(v)=
{(d,\ tf(v,d),\ positions(v,d),\ offsets(v,d))}.
]

Позиции нужны фразовым запросам и поиску по близости. Смещения нужны подсветке и привязке результата к исходному тексту. Фильтр, удаляющий токены, обязан корректно увеличить позиционный разрыв; иначе граф токенов будет поврежден.

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

Нейросетевой поиск

Контур:

  1. Текст нормализуется.
  2. Токенизатор разбивает его на элементы словаря.
  3. Элементы заменяются на ID.
  4. Энкодер вычисляет вектор текста.
  5. Векторный индекс извлекает кандидатов.
  6. Отдельная модель может переупорядочить кандидатов.

Здесь токен не становится термином инвертированного индекса. Он служит входом энкодера. После кодирования поиск выполняется по вектору документа или по набору токенных векторов — в зависимости от архитектуры.

Размер словаря, способ предварительного разбиения и правила нормализации должны совпадать с теми, которые использовались при обучении модели. Замена токенизатора без повторного обучения или совместимой перенастройки меняет входные ID и разрушает соответствие между строками и обученными векторными представлениями.

Рекомендательная система

Токен встречается в двух разных задачах.

Первая — обработка текстовых признаков объекта: названия, описания, отзывов. Здесь действует обычный нейросетевой токенизатор.

Вторая — последовательная рекомендация. История

[
(i_1,i_2,\ldots,i_T)
]

может подаваться в Transformer как последовательность идентификаторов объектов. Тогда каждый (i_t) выполняет роль дискретного символа словаря.

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

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

Первый — разделение токена, типа и термина в классическом информационном поиске. Оно отделило конкретное вхождение строки от словарной единицы индекса.

Второй — представление токена как объекта с атрибутами. Lucene обрабатывает поток токенов, где текстовое значение связано с позициями, смещениями и другими признаками. Это сделало ошибки токенизации наблюдаемыми в фразовом поиске, подсветке и графах синонимов.

Третий — переход нейросетевых моделей от словарей целых слов к субсловам. Sennrich, Haddow и Birch в 2016 году адаптировали BPE к сегментации редких слов в машинном переводе. Словарь остался ограниченным, а неизвестное слово стало представляться последовательностью известных частей.

Четвертый — вероятностная и независимая от пробелов сегментация. SentencePiece обучается непосредственно на необработанных предложениях, а Unigram LM выбирает или сэмплирует разбиения по вероятностной модели.

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

Лексическая токенизация

Базовый алгоритм:

  1. Получить строку Unicode.
  2. Выполнить заданную нормализацию.
  3. Найти границы токенов.
  4. Присвоить токенам позиции и смещения.
  5. Передать поток фильтрам.
  6. Записать итоговые значения в индекс или построить запрос.

Стандарт UAX #29 задает правила определения границ графем, слов и предложений. Эти правила являются исходным профилем, но не готовым решением для любого языка. Для тайского, лаосского, китайского и японского надежное выделение слов требует словаря или другого дополнительного механизма.

Для табличной реализации конечного автомата время обработки обычно линейно по числу символов:

[
T(n)=O(n).
]

Эта оценка не распространяется автоматически на произвольные регулярные выражения, словарный поиск, морфологический разбор или построение графа вариантов.

Дополнительная память потоковой реализации может быть ограничена размером рабочего буфера, но полный результат требует

[
O(m)
]

памяти для (m) токенов. Утверждать O(1) без оговорки о нематериализованном выходе нельзя.

Как токенизация меняет BM25

Для варианта BM25, соответствующего текущей реализации Lucene:

[
score(d,q)=
\sum_{t\in A(q)}
IDF(t),
\frac{tf(t,d)(k_1+1)}
{tf(t,d)+k_1\left(1-b+b\frac{|d|}{avgdl}\right)},
]

где

[
IDF(t)=
\ln\left(
1+
\frac{N-df(t)+0.5}
{df(t)+0.5}
\right).
]

Переменные:

  • (tf(t,d)) — число вхождений термина в документ;
  • (df(t)) — число документов с термином;
  • (N) — число документов;
  • (|d|) — длина поля в токенах;
  • (avgdl) — средняя длина поля;
  • (k_1) — скорость насыщения частоты;
  • (b) — сила нормализации по длине.

В Lucene значения по умолчанию равны (k_1=1.2) и (b=0.75). Его IDF использует положительную форму с log(1 + ...).

При росте (tf) вклад термина растет с насыщением. При росте (df) IDF падает. При увеличении длины документа нормализованный вклад уменьшается, если (b>0).

Изменение токенизатора может одновременно изменить:

  • множество совпадающих документов;
  • (tf);
  • (df);
  • (|d|);
  • (avgdl);
  • позиции;
  • словарь поля;
  • размер инвертированных списков.

Поэтому изменение анализатора нельзя оценивать только по тому, «лучше ли выглядят токены» в _analyze.

BPE

BPE состоит из обучения правил и применения этих правил.

Обучение

  1. Разбить обучающий корпус на базовые единицы: символы или байты, с учетом выбранного варианта.
  2. Посчитать частоты соседних пар.
  3. Найти пару
[
(a^*,b^*)=
\arg\max_{(a,b)} C(a,b).
]
  1. Заменить ее новым символом (a^*b^*).
  2. Сохранить порядок слияния.
  3. Повторять до достижения размера словаря или числа операций.

Применение

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

При обработке нового текста частоты пар по корпусу заново не обучаются. Репозиторий subword-nmt также разделяет команды обучения правил и их применения.

Оценка сложности зависит от структуры данных. Наивное повторное сканирование строки и всех правил может быть дорогим. Реальные реализации используют очереди, таблицы смежности, деревья или кеширование. Одной корректной оценки для всех вариантов BPE нет.

WordPiece

При кодировании WordPiece обычно применяет поиск самого длинного допустимого префикса:

  1. Взять остаток слова.
  2. Найти самый длинный префикс из словаря.
  3. Добавить его в результат.
  4. Продолжить с оставшейся частью.
  5. Если разбиение невозможно, выдать неизвестный токен или применить резервное правило конкретной реализации.

Наивный поиск может занимать (O(n^2)) или (O(nm)), где (n) — длина слова, а (m) — максимальная длина элемента словаря. Fast WordPiece использует префиксное дерево и ссылки отказа, получая линейное время относительно длины входа; авторы также приводят ускорение на своих наборах данных.

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

Unigram LM

Пусть строка (x) допускает разбиение

[
z=(z_1,z_2,\ldots,z_m),
]

где каждый (z_i) входит в словарь (V). Модель задает вероятность:

[
P(z)=\prod_{i=1}^{m}p(z_i).
]

Лучшее разбиение:

[
z^*=
\arg\max_{z\in Z(x)}
\sum_{i=1}^{m}\log p(z_i).
]

Его находят динамическим программированием, обычно алгоритмом Витерби.

Обучение начинается с большого набора возможных элементов. Модель оценивает их вероятности, после чего элементы, удаление которых меньше всего ухудшает целевую функцию, постепенно исключаются. Вероятностная постановка также допускает выбор нескольких разбиений во время обучения модели.

Длина нейросетевой последовательности

Пусть после токенизации текст содержит (L) элементов. Рост (L):

  • увеличивает число обращений к таблице векторных представлений;
  • увеличивает вычислительную стоимость энкодера;
  • повышает вероятность усечения;
  • уменьшает объем исходного текста, помещающийся в ограничение модели.

Поэтому два токенизатора нельзя сравнивать только по доле неизвестных элементов. Нужны распределение (L), доля усеченных входов и качество целевой задачи.

Простой пример

Запрос:

MIP-1-alpha

Документы:

IDТекстРелевантность
d1MIP-1alpha increases after treatment3
d2(MIP)-1alpha was observed in samples3
d3MIP-1 alpha is elevated2
d4MIP-1 beta, IFN-alpha and other markers0

Общий токенизатор разбивает запрос так:

[mip, 1, alpha]

Документ d4 содержит все три части в разных обозначениях. Он получает высокий BM25, хотя не описывает MIP-1-alpha.

Доменный анализатор объединяет допустимые варианты обозначения:

[mip1alpha]

После этого d4 перестает совпадать.

Результаты на искусственной выборке:

АнализаторPrecision@3Recall@3NDCG@4
Общий0,6670,6670,737
Доменный1,0001,0000,845

NDCG не достигает единицы: BM25 все еще ставит более короткий документ d3 выше документов с оценкой 3. Исправление токенизации убрало ложное совпадение, но не решило всю задачу ранжирования.

Пример на Python

Код сравнивает два анализатора на одном корпусе, строит BM25 и считает оценки ранжирования. Пример показывает причинную цепочку:

[
\text{границы токенов}
\rightarrow
tf,df,|d|
\rightarrow
BM25
\rightarrow
порядок документов
\rightarrow
NDCG.
]

Установка

# Внешние библиотеки не нужны.
# Требуется Python 3.11+.

Код

from __future__ import annotations

import math
import re
import unicodedata
from collections import Counter
from collections.abc import Callable


DOCS = {
    "d1": "MIP-1alpha increases after treatment",
    "d2": "(MIP)-1alpha was observed in samples",
    "d3": "MIP-1 alpha is elevated",
    "d4": "MIP-1 beta, IFN-alpha and other markers",
}

QUERY = "MIP-1-alpha"

# 0 — нерелевантный документ, 3 — максимально релевантный.
QRELS = {
    "d1": 3,
    "d2": 3,
    "d3": 2,
    "d4": 0,
}

# Буквы и цифры Unicode без символа подчеркивания.
WORD_RE = re.compile(r"[^\W_]+", re.UNICODE)

# Упрощенное доменное правило:
# буквы + разделители + число + разделители + буквы.
IDENTIFIER_RE = re.compile(
    r"\b([^\W\d_]{2,})"
    r"[\s\-_/()]*"
    r"([0-9]+)"
    r"[\s\-_/()]*"
    r"([^\W\d_]+)\b",
    re.UNICODE | re.IGNORECASE,
)


def normalize(text: str) -> str:
    """Совместимая нормализация Unicode и регистронезависимое сравнение."""
    return unicodedata.normalize("NFKC", text).casefold()


def tokenize_generic(text: str) -> list[str]:
    """Разбиение на последовательности букв и цифр Unicode."""
    return WORD_RE.findall(normalize(text))


def tokenize_domain(text: str) -> list[str]:
    """
    Объединяет варианты MIP-1-alpha, MIP-1alpha и MIP-1 alpha.
    Это узкое правило для демонстрации, а не общий биомедицинский анализатор.
    """
    text = normalize(text)
    text = IDENTIFIER_RE.sub(lambda match: "".join(match.groups()), text)
    return WORD_RE.findall(text)


def bm25_rank(
    docs: dict[str, str],
    query: str,
    tokenizer: Callable[[str], list[str]],
    k1: float = 1.2,
    b: float = 0.75,
) -> tuple[
    list[str],
    dict[str, list[str]],
    list[str],
    dict[str, float],
]:
    tokenized_docs = {
        doc_id: tokenizer(text)
        for doc_id, text in docs.items()
    }
    query_tokens = tokenizer(query)

    document_count = len(tokenized_docs)

    term_frequencies = {
        doc_id: Counter(tokens)
        for doc_id, tokens in tokenized_docs.items()
    }
    document_lengths = {
        doc_id: len(tokens)
        for doc_id, tokens in tokenized_docs.items()
    }
    average_document_length = (
        sum(document_lengths.values()) / document_count
    )

    document_frequency: Counter[str] = Counter()
    for frequencies in term_frequencies.values():
        document_frequency.update(frequencies.keys())

    def idf(term: str) -> float:
        df = document_frequency[term]
        return math.log(
            1.0
            + (document_count - df + 0.5) / (df + 0.5)
        )

    scores: dict[str, float] = {}

    for doc_id, frequencies in term_frequencies.items():
        document_length = document_lengths[doc_id]
        score = 0.0

        for term in query_tokens:
            frequency = frequencies[term]
            if frequency == 0:
                continue

            denominator = (
                frequency
                + k1
                * (
                    1.0
                    - b
                    + b
                    * document_length
                    / average_document_length
                )
            )

            score += (
                idf(term)
                * frequency
                * (k1 + 1.0)
                / denominator
            )

        scores[doc_id] = score

    ranking = sorted(
        scores,
        key=lambda doc_id: (-scores[doc_id], doc_id),
    )

    return query_tokens, tokenized_docs, ranking, scores


def precision_at_k(
    ranking: list[str],
    qrels: dict[str, int],
    k: int,
    relevance_threshold: int = 1,
) -> float:
    return sum(
        qrels.get(doc_id, 0) >= relevance_threshold
        for doc_id in ranking[:k]
    ) / k


def recall_at_k(
    ranking: list[str],
    qrels: dict[str, int],
    k: int,
    relevance_threshold: int = 1,
) -> float:
    relevant_count = sum(
        relevance >= relevance_threshold
        for relevance in qrels.values()
    )

    if relevant_count == 0:
        return 0.0

    retrieved_relevant_count = sum(
        qrels.get(doc_id, 0) >= relevance_threshold
        for doc_id in ranking[:k]
    )

    return retrieved_relevant_count / relevant_count


def ndcg_at_k(
    ranking: list[str],
    qrels: dict[str, int],
    k: int,
) -> float:
    def dcg(order: list[str]) -> float:
        return sum(
            (2 ** qrels.get(doc_id, 0) - 1)
            / math.log2(rank + 1)
            for rank, doc_id in enumerate(
                order[:k],
                start=1,
            )
        )

    ideal_ranking = sorted(
        qrels,
        key=lambda doc_id: (-qrels[doc_id], doc_id),
    )
    ideal_dcg = dcg(ideal_ranking)

    return dcg(ranking) / ideal_dcg if ideal_dcg else 0.0


def print_report(
    name: str,
    tokenizer: Callable[[str], list[str]],
) -> None:
    (
        query_tokens,
        tokenized_docs,
        ranking,
        scores,
    ) = bm25_rank(DOCS, QUERY, tokenizer)

    vocabulary = set().union(
        *(set(tokens) for tokens in tokenized_docs.values())
    )
    average_token_count = (
        sum(len(tokens) for tokens in tokenized_docs.values())
        / len(tokenized_docs)
    )

    print(f"=== {name} ===")
    print("query:", query_tokens)

    for doc_id in ranking:
        print(f"{doc_id}: {scores[doc_id]:.3f}")

    print("vocabulary:", len(vocabulary))
    print("avg tokens/doc:", f"{average_token_count:.1f}")
    print(
        "P@3:",
        f"{precision_at_k(ranking, QRELS, 3):.3f}",
    )
    print(
        "Recall@3:",
        f"{recall_at_k(ranking, QRELS, 3):.3f}",
    )
    print(
        "nDCG@4:",
        f"{ndcg_at_k(ranking, QRELS, 4):.3f}",
    )
    print()


if __name__ == "__main__":
    print_report("generic", tokenize_generic)
    print_report("domain", tokenize_domain)

Ожидаемый вывод

=== generic ===
query: ['mip', '1', 'alpha']
d3: 1.601
d4: 1.313
d1: 0.113
d2: 0.105
vocabulary: 18
avg tokens/doc: 6.0
P@3: 0.667
Recall@3: 0.667
nDCG@4: 0.737

=== domain ===
query: ['mip1alpha']
d3: 0.413
d1: 0.374
d2: 0.341
d4: 0.000
vocabulary: 16
avg tokens/doc: 4.5
P@3: 1.000
Recall@3: 1.000
nDCG@4: 0.845

Ограничения примера

Код не реализует:

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

Регулярное выражение может ошибочно объединять другие последовательности вида «буквы — число — буквы». В рабочей системе такое правило применяют только к полям или классам запросов, где эта структура действительно обозначает единый объект.

Метрики

Метрики потока токенов

Доля пустых запросов:

[
EmptyQueryRate=
\frac{
|{q:|A(q)|=0}|
}{
|Q|
}.
]

Рост показывает, что фильтры удаляют весь запрос.

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

[
ChangedQueryRate=
\frac{
|{q:A_{new}(q)\neq A_{old}(q)}|
}{
|Q|
}.
]

Она показывает масштаб изменения, но не его качество.

Коэффициент расширения:

[
ExpansionFactor=
\frac{
\sum_x |A_{new}(x)|
}{
\sum_x |A_{old}(x)|
}.
]

Он нужен при добавлении n-грамм, синонимов или составных разбиений.

Для субсловных моделей также считают:

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

Метрики индекса

  • число уникальных терминов;
  • общее число записей в инвертированных списках;
  • средняя длина поля;
  • размер индекса;
  • размер словаря;
  • время анализа документа;
  • число токенов в секунду;
  • пиковая память;
  • число сборок мусора;
  • задержка индексирования p95 и p99.

N-граммы способны резко увеличить число токенов. Elasticsearch ограничивает результат _analyze, поскольку чрезмерный поток может исчерпать память узла.

Метрики поиска

[
Recall@K=
\frac{
|\text{релевантные документы в первых }K|
}{
|\text{все известные релевантные документы}|
}.
]
[
DCG@K=
\sum_{i=1}^{K}
\frac{
2^{rel_i}-1
}{
\log_2(i+1)
}.
]
[
NDCG@K=
\frac{DCG@K}{IDCG@K}.
]

Дополнительно используют Precision@K, MRR и MAP. Метрики считают по запросам, затем усредняют. Среднее без разрезов скрывает поломки, поэтому результаты нужны отдельно для языков, классов запросов, длины запроса и типов сущностей. Определения и реализации стандартных мер собраны в ir-measures.

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

  • Hit Rate@K;
  • Recall@K;
  • NDCG@K;
  • покрытие каталога;
  • доля неизвестных объектов;
  • длина истории до и после усечения;
  • доля новых объектов без обученного представления;
  • задержка кодирования истории;
  • изменение кандидатов при обновлении словаря объектов.

Ошибки внедрения

1. Несовпадение анализа документа и запроса

Документ индексируется как mip1alpha, а запрос разбирается как mip, 1, alpha. Совпадения исчезают.

Разные анализаторы допустимы, когда расхождение спроектировано и покрыто тестами. Случайное расхождение — дефект конфигурации.

2. Нормализация Unicode без проверки коллизий

NFC, NFKC, приведение регистра и удаление диакритики могут объединить строки, которые различаются в конкретном домене. Для артикулов, формул, имен и смешанных алфавитов нужен отдельный набор тестов.

3. Разрыв идентификаторов

C++, B-52, MIP-1-alpha, адреса, версии, номера и артикулы плохо обрабатываются универсальным разбиением по неалфавитным символам. Классические учебники по поиску отдельно выделяют такие доменные последовательности как источник ошибок.

4. Удаление всех токенов запроса

Стоп-слова или фильтры длины могут превратить запрос в пустой поток. Поведение системы после этого должно быть задано явно: пустая выдача, показ популярных результатов или отдельный обработчик.

5. Взрыв числа n-грамм

Для строки длины (n) число всех символьных n-грамм в диапазоне ([a,b]) равно:

[
\sum_{k=a}^{b}\max(0,n-k+1).
]

При широком диапазоне число терминов, инвертированных записей и операций резко растет. Нужны ограничения длины поля, диапазона n-грамм и общего числа токенов.

6. Повреждение позиций

Фильтр удаляет токен, но не переносит его позиционный разрыв. После этого фразовые запросы начинают совпадать через удаленные слова или, наоборот, перестают находить допустимые фразы. Lucene требует корректно поддерживать PositionIncrement.

7. Повреждение смещений

Символьный фильтр меняет длину строки, но не корректирует смещения. Подсветка указывает на неправильные символы или выходит за границы исходного текста. CharFilter в Lucene существует в том числе для пересчета смещений.

8. Граф синонимов записывается в индекс

Многословные синонимы создают несколько путей через поток токенов. synonym_graph рассчитан на анализ запроса. Инвертированный индекс Lucene не хранит произвольный многопозиционный граф. flatten_graph делает поток индексируемым, но теряет часть структуры.

9. Обновление индексного анализатора без переиндексации

Изменение конфигурации не переписывает уже созданные термины и инвертированные списки. Старые и новые документы начинают жить в разных пространствах терминов.

Изменение индексного анализатора требует нового индекса или полной переиндексации. Изменение только анализатора запроса можно раскатывать отдельно, но оно все равно меняет множество кандидатов.

10. Несовпадение токенизатора и модели

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

В артефакте модели должны храниться:

  • версия токенизатора;
  • хеш словаря;
  • хеш правил нормализации;
  • специальные токены;
  • максимальная длина;
  • политика усечения.

11. Усечение создает систематическое смещение

Язык или домен, которому требуется больше субсловных элементов, теряет большую часть исходного текста при одинаковом ограничении длины. Средняя длина по всему корпусу это скрывает. Нужны разрезы по языкам, источникам и типам объектов.

12. Нестабильные ID в рекомендациях

Если ID объектов переиспользуются, меняются или зависят от выгрузки, обученная последовательная модель связывает новый объект со старым представлением. Словарь объектов должен быть версионирован, а удаленные ID нельзя незаметно выдавать другим объектам.

Развертывание в рабочей системе

  1. Зафиксировать текущие версии анализаторов, словарей и моделей.
  2. Собрать контрольную выборку из журналов запросов и оценок релевантности.
  3. Разбить ее по языкам и классам запросов.
  4. Сохранить старые и новые потоки токенов.
  5. Посчитать ChangedQueryRate, число токенов, пустые запросы и размер словаря.
  6. Построить отдельный теневой индекс.
  7. Повторно проиграть журналы запросов.
  8. Сравнить Recall@K, MRR, MAP, NDCG@K и состав кандидатов.
  9. Измерить время анализа, индексирования и размер индекса.
  10. Провести ограниченный онлайн-эксперимент.
  11. Держать прежний индекс и алиас для быстрого отката.
  12. После раскатки контролировать новые запросы, которых не было в контрольной выборке.

Среднее изменение NDCG без доверительного интервала или проверки по запросам не доказывает улучшение. Эксперимент должен хранить результаты каждого запроса, чтобы можно было найти классы, на которых новый анализатор проигрывает.

Токены и техническая поисковая оптимизация

Токенизация выполняется после извлечения текста. Она не заменяет обход, рендеринг, выбор канонической страницы, удаление дублей или построение документа для индекса.

Во внутренней поисковой системе технический оптимизатор может:

  • проверить поток через _analyze;
  • сопоставить анализ документа и запроса;
  • исследовать журналы запросов;
  • собрать оценки релевантности;
  • проверить артикулы, дефисы, числа и смешанные алфавиты;
  • измерить изменение кандидатов и ранжирования;
  • потребовать переиндексацию после изменения индексного анализатора.

Внешняя поисковая система обычно не предоставляет поток токенов конкретного документа как проверяемый интерфейс. Поэтому утверждения вида «поисковик разобьет эту строку на такие токены» без документации или воспроизводимого эксперимента остаются догадкой.

Для внешнего поиска полезны наблюдаемые действия:

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

Вывод

Токен задает единицу, с которой работает следующий этап системы.

В лексическом поиске его границы меняют словарь, инвертированные списки, частоты, длины документов, позиции и оценки BM25.

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

В последовательной рекомендательной модели словарь объектов задает пространство допустимых событий и связь идентификаторов с обученными представлениями.

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

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


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