Анализ английского текста: стемминг и стоп-слова

В предыдущей статье мы разобрали анатомию анализатора — три стадии конвейера: char filters, токенизатор, token filters. Теперь посмотрим, как этот конвейер собран в конкретном встроенном анализаторе english, который Elasticsearch поставляет «из коробки» для английского языка.

Что такое анализатор english

Анализатор english — это предсобранный конвейер, специально настроенный под морфологию английского. В отличие от standard, который просто делит текст по пробелам и приводит к нижнему регистру, english умеет:

  • убирать притяжательные суффиксы (author'sauthor)
  • выбрасывать стоп-слова (the, is, are и ещё несколько десятков)
  • сводить слова к основе через стемминг (runningrun)

Именно эти три вещи делают разницу между «найти документ ровно с этим словом» и «найти документы по смыслу, в любых словоформах».

Состав анализатора english

Char filters: нет.

Tokenizer: standard — делит по пробелам и пунктуации, удаляет знаки препинания.

Token filters (по порядку):

1. english_possessive_stemmer — убирает притяжательный суффикс: author'sauthor, company'scompany

2. lowercase — всё в нижний регистр: Quickquick

3. english_stop — удаляет стоп-слова: the, a, is, are, in, on

4. english_stemmer — стемминг по алгоритму Snowball/Porter2: runningrun, foxesfox

flowchart TD A["Исходный текст<br/>'The foxes are running'"] --> B["Tokenizer: standard<br/>[The, foxes, are, running]"] B --> C["Filter 1: english_possessive_stemmer<br/>[The, foxes, are, running]"] C --> D["Filter 2: lowercase<br/>[the, foxes, are, running]"] D --> E["Filter 3: english_stop<br/>[foxes, running]"] E --> F["Filter 4: english_stemmer (Snowball)<br/>[fox, run]"] F --> G["Токены в инвертированном индексе:<br/>fox, run"]
flowchart TD
    A["Исходный текст<br/>'The foxes are running'"] --> B["Tokenizer: standard<br/>[The, foxes, are, running]"]
    B --> C["Filter 1: english_possessive_stemmer<br/>[The, foxes, are, running]"]
    C --> D["Filter 2: lowercase<br/>[the, foxes, are, running]"]
    D --> E["Filter 3: english_stop<br/>[foxes, running]"]
    E --> F["Filter 4: english_stemmer (Snowball)<br/>[fox, run]"]
    F --> G["Токены в инвертированном индексе:<br/>fox, run"]
Конвейер анализатора `english`: четыре фильтра последовательно обрабатывают токены

Порядок фильтров здесь неслучаен: сначала идёт lowercase, и только потом english_stop. Если бы стоп-слова фильтровались до приведения к нижнему регистру, слово The с заглавной буквы прошло бы мимо фильтра — в словаре стоп-слов оно записано строчными the. Это ровно тот принцип, который разобран в Анатомия анализатора: char filters, токенизатор, token filters.

Стемминг: слова к основе

Стемминг (stemming) — это обрезание слова до его корневой основы по набору правил. Алгоритм Snowball (Porter2) убирает суффиксы вроде -ing, -ed, -tion, -ness, -ly и другие.

Исходное словоПосле стемминга
runningrun
dogsdog
connectionconnect
happinesshappi
quicklyquick

Пример happi показывает важную особенность: результат стемминга не обязан быть настоящим словом. happi не существует в английском, но это не проблема — в индекс попадают не слова для чтения, а служебные токены для сравнения. Главное, что happiness, happy и happily все дадут один и тот же токен happi. Значит, поиск по любому из них найдёт документы со всеми тремя формами.

Check yourself
Какими токенами будут представлены слова `happiness` и `happily` в индексе после обработки анализатором `english`? Найдёт ли поиск по запросу `happy` документы с этими словами?

Стоп-слова: выбрасываем шум

Стоп-слова — это слова, которые встречаются в языке слишком часто, чтобы нести поисковую нагрузку: артикли, предлоги, местоимения, вспомогательные глаголы. В английском это a, an, the, is, are, was, were, in, on, at, of, to и ещё несколько десятков.

Зачем их убирать? Если бы the попало в инвертированный индекс, оно оказалось бы в тысячах документов и никак не помогло бы ранжированию. При этом занимало бы место и замедляло поиск. Стоп-слова удаляются и при индексации, и при поиске — поэтому запрос the quick brown fox в итоге превратится в три токена: [quick, brown, fox].

Check yourself
Запрос `match` получает строку `the quick brown fox`. Сколько токенов реально уйдёт в сравнение с индексом, если поле использует анализатор `english`? Перечислите их.

Проверяем english через _analyze

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

POST /_analyze
{
  "analyzer": "english",
  "text": "The quick brown foxes are running in the forest"
}

Ответ (упрощённо):

{
  "tokens": [
    { "token": "quick",  "position": 1 },
    { "token": "brown",  "position": 2 },
    { "token": "fox",    "position": 3 },
    { "token": "run",    "position": 5 },
    { "token": "forest", "position": 8 }
  ]
}

Обратите внимание на пропуски в позициях: The (0), are (4), in (6), the (7) — удалены как стоп-слова. Поле position сохраняет исходные номера — это важно для match_phrase, подробнее в Полнотекстовые запросы: match, match_phrase, multi_match.

foxes превратилось в fox, running — в run. Это означает: запрос по слову fox найдёт документы, где написано foxes, а запрос runs найдёт документы с running — оба дают токен run.

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

POST /_analyze
{
  "tokenizer": "standard",
  "filter": ["lowercase"],
  "text": "The quick brown foxes are running"
}

Потом добавьте "english_stop" в массив filter, потом "porter_stem" — и смотрите, как меняется результат на каждом шаге. Так быстро находится точка, где поведение расходится с ожиданием.

Используем english в маппинге

Задать анализатор полю — одна строчка в явном маппинге при создании индекса:

PUT /articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english"
      },
      "body": {
        "type": "text",
        "analyzer": "english"
      }
    }
  }
}

После этого запрос match автоматически применит тот же анализатор к строке поиска. Никакой дополнительной настройки не нужно:

GET /articles/_search
{
  "query": {
    "match": {
      "body": "running dogs"
    }
  }
}

Elasticsearch превратит running в run и dogs в dog, затем найдёт все документы, где встречаются эти токены — независимо от того, написано там run, runs, running, dog или dogs.

Когда english недостаточно

Встроенный анализатор хорош для общих задач, но у него есть пределы.

Слишком агрессивный стемминг. universityunivers — может показаться перебором. В таких случаях подходит менее агрессивный стеммер kstem или minimal_english. Это решается через кастомный анализатор — подробнее в Синонимы и кастомные анализаторы.

Специфические стоп-слова домена. Для технической документации слово not вовсе не стоп-слово: «does not work» несёт другой смысл. Список стоп-слов переопределяется в кастомном анализаторе.

Синонимы. Если car и automobile должны находить одно и то же — нужен фильтр synonym. Это следующая статья: Синонимы и кастомные анализаторы.

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

Check yourself
Что произойдёт, если при индексации поля использовался анализатор `english`, а при поиске (через параметр `search_analyzer`) — `standard`? Будут ли найдены документы по запросу `running`?

Итог

Анализатор english — четыре фильтра поверх стандартного токенизатора: убрать притяжательное 's, привести к нижнему регистру, выбросить стоп-слова, применить Snowball-стемминг. В результате запросы по одной форме слова находят документы по любой другой форме. Проверить — POST /_analyze. Подключить — одна строчка в маппинге.

Quick recall
В анализаторе `english` фильтр lowercase стоит ПЕРЕД english_stop. Почему порядок важен?
Quick recall
Запрос "the quick brown fox" с анализатором english даст какие токены в результате?
Quick recall
Запрос "running" найдёт документы с "runner" при поиске, если применён стемминг. Почему?

См. также