Pull to refresh

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

Reading time 3 min
Views 7.5K


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

Ссылка на официальную документацию.
Tags:
Hubs:
+9
Comments 3
Comments Comments 3

Articles