Conditional indexing. Оптимізуємо процес повнотекстового пошуку



У цій статті я хочу поговорити про інтеграцію Apache Lucene і Hibernate Search. Якщо бути більш точним, то про один з механізмів Hibernate Search, який може здорово збільшити продуктивність на проекті з повнотекстовим пошуком.

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

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

@Field
private String indexedField;

private String unindexedField;

//getters and setters
}

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

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

Тепер давайте розглянемо приклад індексації деякої сутності. Припустимо, що у нас є сайт оголошень. А ось і наша сутність:

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

private String text;

private AdStatus status;

//getters and setters
}

Ми хочемо надати нашим користувачам можливість повнотекстового пошуку по всіх оголошеннях сайту. Для цього додаємо відповідні анотації:

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

@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 id Integer;

@Field
private String text;

private AdStatus status;

//getters and setters
}

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

p.s. В офіційній документації розробники вказують, що ця фіча є експериментальною і її функціонування може змінитися в залежності від зворотного зв'язку з користувачами.

Посилання на офіційну документацію.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.