Синонимы и кастомные анализаторы

Морфология, стемминг и стоп-слова, разобранные в предыдущих статьях, решают одну проблему: разные словоформы одного слова. Но у поискового движка есть и другая слепая зона — бизнес-синонимы: слова с разным написанием, которые означают одно и то же. Пользователь набирает «ноутбук», а в каталоге товар называется «laptop» или «лэптоп». Пользователь пишет «купить», а нужно найти документы с «приобрести». Анализатор russian с этим не справится — обрезание суффиксов здесь бессильно.

Именно здесь в дело вступают token filter synonym и synonym_graph. А вместе с ними — возможность собирать кастомные анализаторы точно под задачу, комбинируя любые фильтры в нужном порядке.

Два формата правил синонимов

Elasticsearch поддерживает Solr-формат — простой текстовый список, по одному правилу на строку. Правила бывают двух типов.

Эквивалентные синонимы — все термины взаимозаменяемы:

ноутбук, laptop, лэптоп
смартфон, мобильник, телефон
купить, приобрести, заказать

По умолчанию (expand: true) такое правило разворачивается во все направления: запрос «laptop» найдёт документы с «ноутбук» и наоборот. При expand: false все термины схлопываются в первый — поиск ведётся только по нему.

Явное отображение (explicit mapping) — левая сторона отображается в правую, но не наоборот:

personal computer, пк => компьютер
нью-йорк => new york, ny
i-pod, ipod => ipod

Запросы «пк» или «personal computer» превратятся в «компьютер», но запрос «компьютер» не найдёт «пк» — связь односторонняя. Удобно для нормализации: пользователь вводит аббревиатуру или написание с дефисом, а в индексе хранится каноническая форма.

Check yourself
Правило «купить => приобрести» — это эквивалентный синоним или явное отображение? Найдёт ли запрос «приобрести» документы со словом «купить»?

synonym и synonym_graph: в чём разница

В Elasticsearch два фильтра для синонимов:

synonymsynonym_graph
Многословные синонимыМогут давать позиционные конфликтыКорректная обработка через граф токенов
Рекомендуется дляИндексного анализатораПоискового анализатора
Горячее обновлениеНе поддерживаетсяПоддерживается (updateable: true)

Проблема synonym с многословными синонимами конкретная: правило нью-йорк => new york создаёт токены «new» и «york» в одной позиции с «нью-йорк». Запрос match_phrase начинает вести себя непредсказуемо. Фильтр synonym_graph строит полный граф токенов с правильными позициями, поэтому фраза «нью-йорк» корректно распознаётся как единое двусловное понятие.

Порядок фильтров имеет значение. Если стеммер стоит до синоним-фильтра, он обработает и сами правила синонимов при загрузке. Правила тогда пришлось бы писать в «стемминговой» форме — неудобно. Рекомендуемый порядок: lowercase → stop → synonym_graph → stemmer — синонимы раскрываются в обычной форме слов, и только потом стеммер работает с результатом.

Check yourself
Вы добавляете правило «нью-йорк, new york» в search_analyzer. Какой фильтр выбрать — synonym или synonym_graph — и почему?

Индексация или поиск: когда применять синонимы

Синонимы можно добавить в анализатор на этапе индексации или только на этапе поиска — последствия разные:

Индексный анализаторПоисковый анализатор
Как работаетСинонимы «вшиты» в инвертированный индексПрименяются к тексту запроса в момент поиска
Обновление словаряТребует полного reindexГорячее обновление через _reload_search_analyzers
Размер индексаРастёт (больше токенов)Не меняется
РекомендуетсяРедко, только если словарь стабиленВ большинстве случаев

Рекомендуемый паттерн — два разных анализатора на одно поле: analyzer (только морфология) и search_analyzer (морфология + synonym_graph). Словарь синонимов можно обновить в любой момент без переиндексации всего индекса.

flowchart TD DOC([Документ]) --> IA QUERY([Запрос пользователя]) --> SA IA --> IDX[(Инвертированный индекс)] SA --> IDX subgraph IA["ru_index_analyzer"] direction LR A1[tokenizer] --> A2[lowercase] --> A3[stop] --> A4[stemmer] end subgraph SA["ru_search_analyzer"] direction LR B1[tokenizer] --> B2[lowercase] --> B3[stop] --> B4[synonym_graph] --> B5[stemmer] end
flowchart TD
    DOC([Документ]) --> IA
    QUERY([Запрос пользователя]) --> SA
    IA --> IDX[(Инвертированный индекс)]
    SA --> IDX

    subgraph IA["ru_index_analyzer"]
        direction LR
        A1[tokenizer] --> A2[lowercase] --> A3[stop] --> A4[stemmer]
    end

    subgraph SA["ru_search_analyzer"]
        direction LR
        B1[tokenizer] --> B2[lowercase] --> B3[stop] --> B4[synonym_graph] --> B5[stemmer]
    end
Два анализатора на одном поле: synonym_graph появляется только в search_analyzer и не меняет структуру индекса

Сборка кастомного анализатора: полный пример

Создадим индекс товарного каталога с двумя анализаторами:

PUT /catalog
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_stemmer": {
          "type": "stemmer",
          "language": "russian"
        },
        "product_synonyms": {
          "type": "synonym_graph",
          "updateable": true,
          "synonyms_path": "analysis/product_synonyms.txt"
        }
      },
      "analyzer": {
        "ru_index_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "ru_stop", "ru_stemmer"]
        },
        "ru_search_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "ru_stop", "product_synonyms", "ru_stemmer"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ru_index_analyzer",
        "search_analyzer": "ru_search_analyzer"
      }
    }
  }
}

Файл config/analysis/product_synonyms.txt содержит одно правило на строку:

ноутбук, laptop, лэптоп
смартфон, мобильный телефон, мобильник
телевизор, тв, tv
наушники, гарнитура

Тот же подход работает и для английского каталога — достаточно заменить ru_stop и ru_stemmer на английские варианты и наполнить словарь английскими синонимами; принципы конфигурации те же, что описаны в Анализ английского текста: стемминг и стоп-слова.

Проверим, как search-анализатор видит запрос «купить laptop»:

POST /catalog/_analyze
{
  "analyzer": "ru_search_analyzer",
  "text": "купить laptop"
}

При словаре выше «laptop» раскроется в эквивалентные токены «ноутбук», «laptop», «лэптоп» на одной позиции, затем стеммер обработает каждый из них. Документ с заголовком «Лёгкий лэптоп для офиса» будет найден, даже если пользователь написал «laptop».

Синонимы inline: только для прототипов

Если файл неудобен — например, при локальной разработке или в Docker с одной нодой — правила задаются прямо в настройках индекса:

"product_synonyms": {
  "type": "synonym_graph",
  "synonyms": [
    "ноутбук, laptop, лэптоп",
    "смартфон, мобильник, телефон"
  ]
}

Минус принципиальный: горячее обновление с updateable: true при inline-синонимах не работает. Для смены правил придётся пересоздать индекс или перейти на synonyms_path / Synonyms API.

Обновление словаря без переиндексации

Главное преимущество поискового анализатора с synonym_graph — словарь можно обновить «на горячую». Три обязательных условия:

  • Параметр updateable: true в фильтре
  • Фильтр используется только в search_analyzer, а не в analyzer
  • Правила хранятся в файле или через Synonyms API — не inline

Шаги обновления через файл:

# 1. Отредактировать файл на КАЖДОЙ ноде кластера
#    (автоматической синхронизации между нодами нет)

# 2. Перезагрузить search-анализаторы
POST /catalog/_reload_search_analyzers

# 3. Очистить request cache
POST /catalog/_cache/clear

Synonyms API (появился в ES 8.10) — управление словарём через REST без доступа к файловой системе нод. Идеально для Docker и облачных деплоев:

PUT /_synonyms/product_synonyms
{
  "synonyms_set": [
    { "id": "laptops", "synonyms": "ноутбук, laptop, лэптоп" },
    { "id": "phones",  "synonyms": "смартфон, мобильник, телефон" }
  ]
}

После обновления через API всё равно нужно вызвать POST /catalog/_reload_search_analyzers.

Check yourself
Вы обновили файл синонимов на всех нодах и вызвали _reload_search_analyzers. Нужно ли переиндексировать документы?

Синонимы и морфология: вместе сильнее

Если вы уже используете Hunspell или morphology-плагин (см. Анализ русского текста: морфология, стемминг, Hunspell), синонимы добавляются в ту же цепочку без конфликтов. Правило простое: синонимы до стеммера — правила пишем в обычной форме слов; синонимы после — в форме лемм. Первый вариант читаемее и проще в поддержке.

Синонимы хорошо уживаются и с векторным поиском. В Гибридный поиск: retrievers и RRF синонимы на BM25-стороне компенсируют терминологические разрывы, которые эмбеддинги иногда пропускают при коротких запросах.

Quick recall
Для многословного синонима вроде 'нью-йорк => new york' и корректной работы фразового поиска какой фильтр выбрать: `synonym` или `synonym_graph`?
Quick recall
В чём различие между эквивалентными синонимами (`ноутбук, laptop, лэптоп`) и явным отображением (`нью-йорк => new york`)?
Quick recall
Пользователь ищет 'laptop', в каталоге товар назван 'ноутбук'. Почему стемминг это не решит?

См. также