Conditional indexing. Оптимизируем процесс полнотекстового поиска



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

Ни для кого, кто работал с перечисленными выше технологиями, не секрет, что для полнотекстового поиска необходима индексация. Иначе говоря, при добавлении и изменении записей в БД необходимо добавлять/изменять индексы, по которым, собственно, и будет осуществляться полнотекстовый поиск. За данный процесс и отвечает Apache Lucene. А вот как мы уведомляем Люцену, что данную сущность необходимо индексировать:

@Entity
@Indexed
public class SomeEntity {
    @Id
    @GeneratedValue
    private Integer id;

    @Field
    private String indexedField;

    private String unindexedField;

    //getters and setters
}

В приведенном выше классе аннотация Indexed говорит о том, что данная сущность индексируется Люценой. Аннотация @Field указывает, какие именно поля будут индексироваться. Т.к. аннотация @Field надвешена только над полем indexedField, это значит, что мы сможем осуществлять полнотекстовый поиск только по этому полю.

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

Теперь давайте рассмотрим пример индексации некоторой сущности. Предположим, что у нас есть сайт объявлений. А вот и наша сущность:

@Entity
public class Ad {
    @Id
    @GeneratedValue
    private Integer id;

    private String text;

    private AdStatus status;

    //getters and setters
}

Мы хотим предоставить нашим пользователям возможность полнотекстового поиска по всем объявлениям сайта. Для этого добавляем соответствующие аннотации:

@Entity
@Indexed
public class Ad {
    @Id
    @GeneratedValue
    private Integer id;

    @Field
    private String text;

    private AdStatus status;

    //getters and setters
}

Теперь самое время упомянуть, что у объявления может быть один из следующих статусов: DRAFT, ACTIVE, ARCHIVE. После недолгого раздумья мы приходим к решению, что пользователям в результатах поиска необходимо отображать только объявления в статусе ACTIVE. Рассмотрим два варианта решения данной проблемы. Первый — в лоб. Добавляем аннотацию @Field над полем status. И каждый раз при поиске добавляем predicate, который и будет указывать, каким должен быть этот статус. Минусы данного решения: ощутимое падение производительности при большом количестве объявлений в статусе ARCHIVE и DRAFT, излишняя индексация сущностей, по которым уже не будет проводиться поиск.

Тут же в голову приходит другое решение — не индексировать/удалять существующие индексы для объявлений во всех статусах кроме ACTIVE. В этом нам и поможет такой механизм, как interceptors. Сначала поставим задачу. Мы хотим, чтобы при изменении сущности индексация производилась в зависимости от нового статуса объявления. Теперь приступаем к реализации. Создаем класс AdIndexInterceptor, который реализует интерфейс EntityIndexingInterceptor:

public class AdIndexInterceptor implements EntityIndexingInterceptor<Ad> {
    @Override
    public IndexingOverride onAdd(Ad entity) {
        if (entity.getStatus() == AdStatus.ACTIVE) {
            return IndexingOverride.APPLY_DEFAULT;
        }
        return IndexingOverride.SKIP;
    }

    @Override
    public IndexingOverride onUpdate(Ad entity) {
        if (entity.getStatus() == AdStatus.ACTIVE) {
            return IndexingOverride.UPDATE;
        }
        return IndexingOverride.REMOVE;
    }

    @Override
    public IndexingOverride onDelete(Ad entity) {
        return IndexingOverride.APPLY_DEFAULT;
    }

    @Override
    public IndexingOverride onCollectionUpdate(Ad entity) {
        return onUpdate(entity);
    }
}

Как видно выше, в классе должно быть реализовано 4 метода, которые будут вызываться при добавлении записи, редактировании записи, удалении и обновлении коллекции записей соответственно. Каждый из этих методов должен вернуть одно из значений IndexingOverride, который в свою очередь является enum. Всего имеется четыре значения данного enum. Распишу, что происходит при возврате каждого из них:

  • APPLY_DEFAULT — процесс индексации продолжается так, как бы он проходил при отсутствии interceptor’a.
  • SKIP — индексация не происходит.
  • UPDATE — обновляется существующий индекс.
  • REMOVE — удаляется существующий индекс, новый не создается.

Теперь вернемся к классу сущности. Для того, чтобы Люцена знала, что перед индексацией необходимо вызвать соответствующие методы interceptor’a, добавляем в аннотацию Indexed над сущностью атрибут interceptor:

@Entity
@Indexed(interceptor = AdIndexingInterceptor.class)
public class Ad {
    @Id
    @GeneratedValue
    private Integer id;

    @Field
    private String text;

    private AdStatus status;

    //getters and setters
}

Осталось только корректно задокументировать использование данного interceptor’a, чтобы поведение Люцены было ожидаемым и для ваших коллег по команде.

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

Ссылка на официальную документацию.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 3
    0
    На личном опыте вывел для себя «Первое правило в использовании Hibernate Search»: не использовать Hibernate Search. Никогда. Применял как Hibernate Search, так и «голый» Lucene, и понял что не вижу смысла существования Hibernate Search. Если вам не требуется тотально положить в индексы Lucene всю свою базу, т.е. если у вас только пара-тройка таблиц требующих неточного поиска — используйте чистый Lucene. Последний раз, когда я внедрял неточный поиск, отказ от использования Hibernate Search ускорил индексацию на порядок, и уменьшил размер индекса на 20-30%.

    Да и вообще, Hibernate Search, судя по всему, умирает. Давно уже вышел Lucene 4, но последний Hibernate Search до сих пор работает на 3-ей версии.
      0
      Пятый Hibernate Search, который к слову вышел меньше месяца назад, уже использует 4-ую Люцену.
        0
        Lucene 4: 2012-й год
        Hibernate Search 5: 2015-й год
        … без комментариев.

        В любом случае, проект не стал менее специфическим, я не очень представляю себе круг задач, для которых он предназначен. Огромный оверхед над Lucen-ом ради экономии, в лучшем случае, нескольких строчек кода, которая компенсируется дополнительными строчками конфигурации. Если пытаться полноценно использовать полнотекстовый поиск, то Hibernate Search не избавит от надобности изучать Lucene. А если вы разобрались с Lucene, то зачем тратить время на изучение обертки над ним? Это ведь не ассемблер, это полноценная библиотека с полным набором высокоуровневого API, зачем надстраивать над ней ещё что-то? Масло масляное…

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

    Самое читаемое