Введение в Hibernate Search

1. обзор

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

Когда нам нужно реализовать функцию полнотекстового поиска, использование инструментов, с которыми мы уже хорошо разбираемся, всегда является плюсом.

В случае, если мы уже используем Hibernate и JPA для ORM, мы всего в одном шаге от Hibernate Search.

Hibernate Search integrates Apache Lucene, a high-performance and extensible full-text search-engine library written in Java. Это сочетает мощь Lucene с простотой Hibernate и JPA.

Проще говоря, нам просто нужно добавить некоторые дополнительные аннотации к нашим доменным классам, иthe tool will take care of the things like database/index synchronization.

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

3. Конфигурации

3.1. Maven Зависимости

Перед тем как начать, нам сначала нужно добавить необходимыйdependencies к нашемуpom.xml:


    org.hibernate
    hibernate-search-orm
    5.8.2.Final

Для простоты мы будем использоватьH2 в качестве нашей базы данных:


    com.h2database
    h2
    1.4.196

3.2. Конфигурации

Мы также должны указать, где Lucene должна хранить индекс.

Это можно сделать с помощью свойстваhibernate.search.default.directory_provider.

Мы выберемfilesystem, что является наиболее простым вариантом для нашего варианта использования. Дополнительные параметры перечислены вofficial documentation. Filesystem-master/filesystem-slave and infinispan are noteworthy for clustered applications, where the index has to be synchronized between nodes.

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

hibernate.search.default.directory_provider = filesystem
hibernate.search.default.indexBase = /data/index/default

4. Модельные классы

После настройки мы готовы указать нашу модель.

On top of the JPA annotations @Entity and @Table, we have to add an @Indexed annotation. Он сообщает Hibernate Search, что объектProduct должен быть проиндексирован.

After that, we have to define the required attributes as searchable by adding a @Field annotation:

@Entity
@Indexed
@Table(name = "product")
public class Product {

    @Id
    private int id;

    @Field(termVector = TermVector.YES)
    private String productName;

    @Field(termVector = TermVector.YES)
    private String description;

    @Field
    private int memory;

    // getters, setters, and constructors
}

АтрибутtermVector = TermVector.YES потребуется позже для запроса «Еще как это».

5. Построение индекса Lucene

Перед запуском фактических запросовwe have to trigger Lucene to build the index initially:

FullTextEntityManager fullTextEntityManager
  = Search.getFullTextEntityManager(entityManager);
fullTextEntityManager.createIndexer().startAndWait();

After this initial build, Hibernate Search will take care of keeping the index up to date. I. e. мы можем создавать, управлять и удалять объекты черезEntityManager, как обычно.

Примечание:we have to make sure that entities are fully committed to the database before they can be discovered and indexed by Lucene (кстати, это также причина, по которой первоначальный импорт тестовых данных в нашемexample code test cases осуществляется в специальном тестовом примере JUnit, помеченном@Commit).

6. Построение и выполнение запросов

Теперь мы готовы к созданию нашего первого запроса.

В следующем разделеwe’ll show the general workflow for preparing and executing a query.

После этого мы создадим несколько примеров запросов для наиболее важных типов запросов.

6.1. Общий рабочий процесс для создания и выполнения запроса

Preparing and executing a query in general consists of four steps:

На шаге 1 мы должны получить JPAFullTextEntityManager и из негоQueryBuilder:

FullTextEntityManager fullTextEntityManager
  = Search.getFullTextEntityManager(entityManager);

QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory()
  .buildQueryBuilder()
  .forEntity(Product.class)
  .get();

На шаге 2 мы создадим запрос Lucene через DSL запроса Hibernate:

org.apache.lucene.search.Query query = queryBuilder
  .keyword()
  .onField("productName")
  .matching("iphone")
  .createQuery();

На шаге 3 мы превратим запрос Lucene в запрос Hibernate:

org.hibernate.search.jpa.FullTextQuery jpaQuery
  = fullTextEntityManager.createFullTextQuery(query, Product.class);

Наконец, на шаге 4 мы выполним запрос:

List results = jpaQuery.getResultList();

Note: по умолчанию Lucene сортирует результаты по релевантности.

Шаги 1, 3 и 4 одинаковы для всех типов запросов.

Далее мы сосредоточимся на шаге 2, т.е. e. как создавать разные типы запросов.

6.2. Запросы по ключевым словам

Самый простой вариант использования -searching for a specific word.

Это то, что мы фактически делали уже в предыдущем разделе:

Query keywordQuery = queryBuilder
  .keyword()
  .onField("productName")
  .matching("iphone")
  .createQuery();

Здесьkeyword() указывает, что мы ищем одно конкретное слово,onField() сообщает Lucene, где искать, аmatching() - что искать.

6.3. Нечеткие запросы

Нечеткие запросы работают как запросы по ключевым словам, за исключениемwe can define a limit of “fuzziness”, выше которого Lucene принимает два термина как совпадающие.

ПоwithEditDistanceUpTo(),we can define how much a term may deviate from the other. Он может быть установлен на 0, 1 и 2, при этом значение по умолчанию равно 2 (note: это ограничение исходит из реализации Lucene).

ЧерезwithPrefixLength() мы можем определить длину префикса, который должен игнорироваться нечеткостью:

Query fuzzyQuery = queryBuilder
  .keyword()
  .fuzzy()
  .withEditDistanceUpTo(2)
  .withPrefixLength(0)
  .onField("productName")
  .matching("iPhaen")
  .createQuery();

6.4. Запросы с подстановочными знаками

Hibernate Search также позволяет нам выполнять подстановочные запросы, т.е. e. запросы, для которых часть слова неизвестна.

Для этого мы можем использовать «?” для одиночного символа и«*” для любой последовательности символов:

Query wildcardQuery = queryBuilder
  .keyword()
  .wildcard()
  .onField("productName")
  .matching("Z*")
  .createQuery();

6.5. Фразовые запросы

Если мы хотим найти более одного слова, мы можем использовать фразы-запросы. При необходимости мы можем посмотретьfor exact or for approximate sentences, используяphrase() иwithSlop(). Коэффициент наклона определяет количество других слов, разрешенных в предложении:

Query phraseQuery = queryBuilder
  .phrase()
  .withSlop(1)
  .onField("description")
  .sentence("with wireless charging")
  .createQuery();

6.6. Простые запросы строки запроса

С предыдущими типами запросов мы должны были явно указать тип запроса.

Если мы хотим дать пользователю больше возможностей, мы можем использовать простые запросы строки запроса:by that, he can define his own queries at runtime.

Поддерживаются следующие типы запросов:

  • логическое (И используя «+», ИЛИ используя «|», НЕ используя «-»)

  • префикс (префикс *)

  • фраза («некоторая фраза»)

  • приоритет (с помощью скобок)

  • нечеткий (нечеткий ~ 2)

  • оператор ближнего для запросов фраз («некоторая фраза» ~ 3)

В следующем примере будут объединены нечеткие, фразовые и логические запросы:

Query simpleQueryStringQuery = queryBuilder
  .simpleQueryString()
  .onFields("productName", "description")
  .matching("Aple~2 + \"iPhone X\" + (256 | 128)")
  .createQuery();

6.7. Запросы диапазона

Range queries search for avalue in between given boundaries. Это может быть применено к числам, датам, временным меткам и строкам:

Query rangeQuery = queryBuilder
  .range()
  .onField("memory")
  .from(64).to(256)
  .createQuery();

6.8. Больше подобных запросов

Наш последний тип запроса - это запрос «More Like This». Для этого мы предоставляем объект иHibernate Search returns a list with similar entities, каждому из которых присваивается оценка сходства.

Как упоминалось ранее, в этом случае требуется атрибутtermVector = TermVector.YES в нашем классе модели: он сообщает Lucene о необходимости сохранения частоты для каждого термина во время индексации.

Исходя из этого, сходство будет рассчитываться во время выполнения запроса:

Query moreLikeThisQuery = queryBuilder
  .moreLikeThis()
  .comparingField("productName").boostedTo(10f)
  .andField("description").boostedTo(1f)
  .toEntity(entity)
  .createQuery();
List results = (List) fullTextEntityManager
  .createFullTextQuery(moreLikeThisQuery, Product.class)
  .setProjection(ProjectionConstants.THIS, ProjectionConstants.SCORE)
  .getResultList();

6.9. Поиск более чем в одном поле

До сих пор мы создавали запросы только для поиска по одному атрибуту, используяonField().

В зависимости от варианта использованияwe can also search two or more attributes:

Query luceneQuery = queryBuilder
  .keyword()
  .onFields("productName", "description")
  .matching(text)
  .createQuery();

Кроме того,we can specify each attribute to be searched separately, e. g. если мы хотим определить повышение для одного атрибута:

Query moreLikeThisQuery = queryBuilder
  .moreLikeThis()
  .comparingField("productName").boostedTo(10f)
  .andField("description").boostedTo(1f)
  .toEntity(entity)
  .createQuery();

6.10. Объединение запросов

Наконец, Hibernate Search также поддерживает комбинирование запросов с использованием различных стратегий:

  • SHOULD: запрос должен содержать совпадающие элементы подзапроса

  • MUST: запрос должен содержать совпадающие элементы подзапроса

  • MUST NOT: запрос не должен содержать совпадающих элементов подзапроса

Агрегации:similar to the boolean ones AND, OR and NOT.. Однако имена отличаются, чтобы подчеркнуть, что они также влияют на релевантность.

Например,SHOULD между двумя запросами аналогично логическомуOR:, если один из двух запросов имеет совпадение, это совпадение будет возвращено.

Однако, если оба запроса совпадают, соответствие будет иметь более высокую релевантность по сравнению, если только один запрос соответствует:

Query combinedQuery = queryBuilder
  .bool()
  .must(queryBuilder.keyword()
    .onField("productName").matching("apple")
    .createQuery())
  .must(queryBuilder.range()
    .onField("memory").from(64).to(256)
    .createQuery())
  .should(queryBuilder.phrase()
    .onField("description").sentence("face id")
    .createQuery())
  .must(queryBuilder.keyword()
    .onField("productName").matching("samsung")
    .createQuery())
  .not()
  .createQuery();

7. Заключение

В этой статье мы обсудили основы Hibernate Search и показали, как реализовать наиболее важные типы запросов. Более сложные темы можно найти вofficial documentation.

Как всегда, доступен полный исходный код примеровover on GitHub.