Рассказ пойдет об одной новой, общедоступной Java/Kotlin библиотеке, для работы с русским языком. Она позволяет получить исходные формы + морфологическую информацию для большинства слов русского языка. Статья предназначена для тех, кто создает ботов, обрабатывает сообщения, занимается поиском и извлечением смысла текста. Для справки, ключевое отличие лемматизации от стеммизации (урезания до нормализованной формы) состоит в том, что лемма удовлетворяет правилам языка, например для слова "яблоками" леммой будет "яблоко", а не просто урезанный корень.

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

Источник данных

В первую очередь, стоит выразить благодарность проекту AOT (автоматическая обработка текста) за морфологические словари русского языка. Библиотека содержит их с обновлениями бинарного формата и API, упрощенным подключением в Gradle и Maven проекты без сторонних зависимостей и быстрым (!) поиском.

Преобразование данных

Так как оригинальные aot-словари в исходном текстовом виде достаточно неудобны для быстрого поиска, то они предварительно преобразуются в собственный бинарный формат (GitHub) при помощи компилятора написанного на Kotlin. Такое преобразование позволяет очень быстро загрузить словарь в память и осуществлять мгновенный поиск по нему. Конкретные бенчмарки загрузки словаря из бинарного формата будут отличаться для каждой машины, однако в среднем запуск при инициализации занимает около четырех секунд, после чего получение морфологии для любого слова работает также быстро как HashMap.

Подключение библиотеки

Библиотека совместима с Java 8+, а также протестирована со всеми версиями Kotlin 1.5.*. В этом примере используется система сборки Gradle, примеры для Maven и других систем сборки столь же тривиальны и их можно посмотреть здесь.

// в build.gradle.kts
repositories {
  // (1) Подключим репозиторий jitpack
  maven("https://jitpack.io")
}
dependencies {
  // (2) Добавим зависимость от библиотеки
  implementation("com.github.demidko:aot:2021.09.19")
}

Пример работы

Пример работы приводится для Java, однако библиотека также будет работать и с Kotlin:

import static java.lang.System.out;
import static com.github.demidko.aot.WordformMeaning.lookupForMeanings;

class Example {

  public static void main(String[] args) {
    var meanings = lookupForMeanings("люди");

    out.println(meanings.size());
    /* 1 */

    out.println(meanings.get(0).getMorphology());
    /* [С, мр, им, мн] */

    out.println(meanings.get(0).getLemma());
    /* человек */

    for (var t : meanings.get(0).getTransformations()) {
      out.println(t.toString() + " " + t.getMorphology());
      /*
       * человек [С, мр, им, ед]
       * человека [рд, С, мр, ед]
       * человеку [С, мр, ед, дт]
       * человека [С, мр, ед, вн]
       * человеком [тв, С, мр, ед]
       * человеке [С, мр, ед, пр]
       * люди [С, мр, им, мн]
       * людей [рд, С, мр, мн]
       * человек [рд, С, мр, мн]
       * людям [С, мр, мн, дт]
       * человекам [С, мр, мн, дт]
       * людей [С, мр, мн, вн]
       * людьми [тв, С, мр, мн]
       * человеками [тв, С, мр, мн]
       * людях [С, мр, мн, пр]
       * человеках [С, мр, мн, пр]
       */
    }
  }
}

Как видно, для каждого слова легко получить набор словоформ различных смыслов, после чего можно получать морфологическую информацию (род, падеж, склонение и т. п). Здесь может возникнуть вопрос, почему метод lookupForMeanings("...") возвращает набор словоформ, а не одну? Это сделано по причине наличия в русском языке коллизий словоформ разных смыслов, например "замок" это одновременно производная леммы "замокнуть" (под дождем например) и устройство для запирания дверей и строение. Так как одно и тоже слово является производной разных лемм, библиотека вернет список подходящих различных словоформ с разными морфологическими характеристиками.

Как это вообще работает?

Библиотека загружает из бинарного словаря особую структуру, HashDictionary (низкоуровневое API), которая после загрузки состоит из:

_массив наборов морфологий_
морфология 1 # напр. [рд, С, мр, ед]
морфология 2 # напр. [С, мр, ед, дт]
...
морфология N # напр. [рд, С, мр, мн]

_массив всех строк_
строка 1 # напр. яблоками
строка 2 # напр. яблоко
...
строка N # напр. груша

_массив всех лемм с индексами преобразований в массиве строк и морфологий_
(индекс строки, индекс морфологии) (индекс строки, индекс морфологии)... (индекс строки, индекс морфологии) (индекс строки, индекс морфологии)
(индекс строки, индекс морфологии) (индекс строки, индекс морфологии)... (индекс строки, индекс морфологии) (индекс строки, индекс морфологии)
...
(индекс строки, индекс морфологии) (индекс строки, индекс морфологии)... (индекс строки, индекс морфологии) (индекс строки, индекс морфологии)

_словарь хешей (коллизии проверяются в рантайме, нет смысла отделяеть их во время компиляции, т. к. могут быть и внешние коллизии)_
хеш, индекс леммы, индекс леммы
хеш, индекс леммы, индекс леммы, индекс леммы
хеш, индекс леммы, индекс леммы, индекс леммы, индекс леммы
...
хеш, индекс леммы, индекс леммы, индекс леммы
(напр. хеш яблоки, индекс леммы яблоко)

После получения слова в методе lookupForMeanings("...") нам остается только нормализовать слово в lower case, прогнать через словарь хешей, считать все преобразования, и избавиться от коллизий хеша (если они есть).

Исходный код

Отправить issue или поставить звездочку можно здесь.

Вопросы и предложения также можно задавать в комментариях к этой статье, постараюсь на все ответить.

Комментарии (13)


  1. alexdoublesmile
    19.09.2021 20:42
    +1

    крутой проект! как этот, так и тот, что у Сокирко. Очень интересная тема, имхо


  1. mikhailian
    19.09.2021 23:44

    Опять Старостин в гробу перевернулся.


  1. mih-kopylov
    20.09.2021 07:55

    А почему файл в ресурсах запакован в gz? Как его обновлять-то? Как смотреть изменения в пул реквестах?


    1. Reformat Автор
      20.09.2021 07:57
      +1

      В ресурсах лежит скомпилированный бинарный файл, его обновлять не нужно. Обновления/исправления происходят в более человекочитаемом текстовом формате здесь:
      https://github.com/demidko/aot-binary


  1. plotn1
    21.09.2021 09:31
    +1

    Приветствую! А вашу библиотеку можно для своей читалки использовать? У нас (KnownReader, если интересно - есть статьи на хабре) есть достаточно богатая функциональность по поддержке офлайн и онлайн словарей. Ключевая фича здесь - выбрал слово и отправил в словарь. При этом не все словари поддерживают словоформы, соответственно было бы неплохо (опционально) дать возможность (перед отправкой в словарь) привести слово в начальный вид.

    И, если да, то вопрос второй - в исходном проекте есть и английский и немецкий языки, можно вас попросить доработать ещё и на них, например в виде:

    lookupForMeanings("люди", "ru");

    Лучше бы вообще дать возможность загрузить нужный словарь из Stream (а не изнутри библиотеки), а потом организовать поиск в нём.

    Спасибо.


    1. Reformat Автор
      21.09.2021 10:29
      +1

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

      Библиотеку проверял с Kotlin (JVM). Как поведет себя под Android не знаю.


      1. plotn1
        21.09.2021 10:45
        +1

        Ну я проверю как раз. Ок, давайте я поковыряю, если совсем не пойдёт, приду к вам с тупыми вопросами, вдруг не откажете)


  1. Contender
    21.09.2021 10:27

    Пример работы приводится для Java, однако библиотека также будет работать и с Kotlin

    Точно не наоборот?


    1. Reformat Автор
      21.09.2021 10:28
      +1

      Точно.


      1. Contender
        21.09.2021 10:36

        Но

        class Example {

        public static void main(String[] args) { var meanings = lookupForMeanings("люди");

        не Java.


        1. Reformat Автор
          21.09.2021 12:39
          +1

          Почему вы так думаете?


          1. Contender
            21.09.2021 13:12
            +1

            Я смотрю, действительно в Java с 10 версии появился спецификатор var.

            Не знал. Посыпаю голову пеплом.


            1. Sultansoy
              01.10.2021 19:42
              +1

              Я бы даже подметил, что с 9-ой. А до этого в ломбоке был :)