В этой статье я хочу поговорить про интеграцию 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. В официальной документации разработчики указывают, что данная фича является экспериментальной и ее функционирование может измениться в зависимости от обратной связи с пользователями.
Ссылка на официальную документацию.
Автор: cortwave