Анализ русского текста: морфология, стемминг, Hunspell

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

Почему русская морфология — особая задача

Английское слово run имеет четыре формы: run, runs, ran, running. У русского глагола «купить» уже только личных форм — дюжина, а если прибавить причастия, деепричастие и видовую пару «покупать» — счёт идёт на десятки:

ФормаПримеры
Прош. время (м/ж/с/мн)купил, купила, купило, купили
Будущее времякупит, купишь, купят, купите…
Причастиекупивший, купившая, купившие
Деепричастиекупив
Видовая парапокупать, покупал, покупаю…

Для поискового движка все эти формы должны находить друг друга. Алгоритм, который хорошо работает для английского (runningrun), сталкивается с куда более запутанной системой чередований и приставок в русском.

Проверь себя
Если применить анализатор `standard` к тексту «купил купила купят купившая», сколько уникальных токенов получится и найдут ли они друг друга при поиске по слову «купить»?

Встроенный анализатор russian

Elasticsearch поставляет готовый анализатор russian. Его полный состав:

ШагКомпонентЧто делает
ТокенизаторstandardДелит по пробелам и пунктуации
Фильтр 1lowercaseВсё в нижний регистр
Фильтр 2russian_stopУдаляет русские стоп-слова («и», «в», «на», «с»…)
Фильтр 3russian_keywordsЗащищает отдельные слова от стемминга (по умолчанию пуст)
Фильтр 4russian_stemmerSnowball-стемминг для русского

russian_keywords — это keyword_marker фильтр: он позволяет задать список слов, которые стеммер обходит стороной. По умолчанию список пуст, фильтр ничего не делает. Принцип тот же, что описан в Анатомия анализатора: char filters, токенизатор, token filters.

Проверим через _analyze — точно так же, как в статье про английский:

POST /_analyze
{
  "analyzer": "russian",
  "text": "Пользователи покупали товары в нашем магазине"
}

Примерный ответ:

{
  "tokens": [
    { "token": "пользовател", "position": 0 },
    { "token": "покупа",      "position": 1 },
    { "token": "товар",       "position": 2 },
    { "token": "магазин",     "position": 5 }
  ]
}

Стоп-слова «в» и «нашем» выброшены (позиции 3 и 4 пропущены). Оставшиеся слова прошли через Snowball: «пользователи» → «пользовател», «покупали» → «покупа». Обратите внимание: «покупа» — не настоящее слово. Алгоритм просто обрезал суффикс по правилу, не зная ничего о смысле.

Главный изъян: запрос по слову «купить» даст другой токен, чем «покупа», — и документ найден не будет. Snowball не видит связь между «купить» и «покупать», потому что приставки меняют форму основы.

Проверь себя
Анализатор `russian` превратил «покупали» в «покупа». Как думаешь, какой токен он создаст из слова «купить» — совпадёт ли он с «покупа»?

Несмотря на это, встроенный russian — нулевая сложность настройки. Для небольшого блога или новостного сайта его часто достаточно. Подключается в маппинге одной строчкой:

PUT /articles
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "russian"
      }
    }
  }
}

Hunspell: словарная морфология

Hunspell — формат словарей, который используется в LibreOffice, Firefox, macOS для проверки орфографии. В Elasticsearch встроен token filter hunspell, работающий с этими словарями.

В отличие от Snowball, Hunspell смотрит каждое слово в словаре и возвращает его базовую форму (лемму). «Купили», «купишь», «купившая» — при наличии нужных записей в словаре все они приводятся к «купить». Это принципиально другой уровень точности.

Шаг 1. Добавить файлы словаря

Файлы .aff (правила аффиксов) и .dic (список слов) нужно разместить в директории config/hunspell/ru_RU/ на каждой ноде кластера — автоматической синхронизации между нодами нет:

<путь к ES>/config/hunspell/ru_RU/ru_RU.aff
<путь к ES>/config/hunspell/ru_RU/ru_RU.dic

Файлы можно взять из пакета LibreOffice или из репозиториев словарей для OpenOffice.

Шаг 2. Создать кастомный анализатор

PUT /products
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_hunspell": {
          "type": "hunspell",
          "locale": "ru_RU",
          "dedup": true
        }
      },
      "analyzer": {
        "russian_hunspell_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "ru_stop", "ru_hunspell"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "description": {
        "type": "text",
        "analyzer": "russian_hunspell_analyzer"
      }
    }
  }
}

"stopwords": "_russian_" — встроенный идентификатор русского списка стоп-слов, тот же набор, что в анализаторе russian. Параметр "dedup": true убирает дубли, если словарь для одного слова возвращает несколько лемм.

Проверяем:

POST /products/_analyze
{
  "analyzer": "russian_hunspell_analyzer",
  "text": "Пользователи покупали товары в нашем магазине"
}

При правильно настроенном словаре результат значительно точнее: «пользователи» → «пользователь», «покупали» → «покупать», «магазине» → «магазин». Теперь запрос «купить» и документ со словом «покупали» имеют шанс встретиться — если словарь содержит нужные формы.

Ограничения Hunspell:

  • Файлы словаря нужно распределять по нодам вручную — при Docker/облачном деплое это дополнительная оркестрация.
  • Скорость индексации ниже, чем у Snowball: словарный поиск дороже правилового.
  • Качество зависит от полноты конкретного словаря.

Сторонние плагины: настоящая лемматизация

Для профессионального поиска на русском существуют плагины на базе словарей проекта AOT (Автоматическая обработка текста). Самый известный для ES 8.x — community-плагин nickyat/elasticsearch-analysis-morphology на GitHub.

Устанавливается стандартной командой:

bin/elasticsearch-plugin install \
  https://github.com/nickyat/elasticsearch-analysis-morphology/releases/download/<version>/analysis-morphology-<version>.zip

После перезапуска нод доступен тип фильтра russian_morphology:

PUT /products
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_morph": {
          "type": "russian_morphology"
        },
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        }
      },
      "analyzer": {
        "ru_morphology_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "ru_stop", "ru_morph"]
        }
      }
    }
  }
}

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

Сравнение трёх подходов

flowchart TD A["Входное слово: «покупали»"] --> B{Какой анализатор?} B -->|"russian — Snowball"| C["Токен: покупа"] B -->|"Hunspell ru_RU"| D["Токен: покупать"] B -->|"Morphology-плагин"| E["Токен: покупать"] C --> F["Запрос 'купить' → другой токен\nДокумент НЕ найден ❌"] D --> G["Запрос 'купить' → та же лемма\nДокумент найден ✓"] E --> H["Запрос 'купить' → та же лемма\nДокумент найден ✓"]
flowchart TD
    A["Входное слово: «покупали»"] --> B{Какой анализатор?}
    B -->|"russian — Snowball"| C["Токен: покупа"]
    B -->|"Hunspell ru_RU"| D["Токен: покупать"]
    B -->|"Morphology-плагин"| E["Токен: покупать"]
    C --> F["Запрос 'купить' → другой токен\nДокумент НЕ найден ❌"]
    D --> G["Запрос 'купить' → та же лемма\nДокумент найден ✓"]
    E --> H["Запрос 'купить' → та же лемма\nДокумент найден ✓"]
Как три подхода обрабатывают одно и то же слово «покупали» и что это означает для поиска
russian (Snowball)Hunspell ru_RUMorphology-плагин
Точность лемматизацииНизкаяХорошаяВысокая
Сложность настройкиНулеваяСловари на каждой нодеУстановка плагина
Скорость индексацииБыстроМедленнееМедленнее
Официальная поддержка Elastic✗ (community)
Работает в Elastic CloudЗависит от вариантаЧасто нет

Snowball для русского vs английского: в чём разница

Для английского Snowball работает хорошо, потому что морфология бедна: суффиксальных правил хватает для большинства случаев. В русском Snowball срезает окончания по общим правилам, но не видит:

  • Чередования в корне: купить / покупать — разные основы с точки зрения алгоритма;
  • Приставочные глаголы: написать, переписать, записать — Snowball не связывает их с писать;
  • Нестандартные формы: супплетивные («плохой» / «хуже»), усечённые основы.

Это же справедливо для других флективных языков с богатой морфологией: немецкого, польского, чешского. Чем сложнее морфология — тем больше выигрыш от словарных подходов по сравнению с Snowball.

Что выбрать на практике

  • Быстрый старт / прототип: встроенный russian. Нет зависимостей, работает из коробки.
  • Продуктовый поиск по товарам и контенту: Hunspell с ru_RU — приемлемое качество без внешних зависимостей.
  • Высокое качество / корпоративный поиск: morphology-плагин, но закладывайте время на поддержку при обновлениях ES.
  • Гибридный сценарий: если добавить векторный поиск, требования к точности морфологии снижаются — подробнее в Гибридный поиск: retrievers и RRF.
  • Синонимы поверх морфологии: независимо от выбранного анализатора, для бизнес-терминов добавьте фильтр синонимов — об этом в Синонимы и кастомные анализаторы.
Проверь себя
Ты строишь поиск по каталогу интернет-магазина: запрос «купить ноутбук» должен находить товары с текстом «покупка ноутбука» и «ноутбуки для работы». Какой анализатор выбрать и почему?
Быстрое повторение
Как Hunspell обрабатывает словоформы типа «купили», «купишь», «купившая»?
Быстрое повторение
Почему Snowball неправильно связывает русские слова «купить» и «покупать»?
Быстрое повторение
Почему русская морфология представляет большую сложность для поисковых движков, чем английская?

См. также