Введение в поиск Hibernate

1. Обзор

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

2. Основы поиска в гибернации

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

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

Hibernate Search интегрирует Apache Lucene, высокопроизводительную и расширяемую библиотеку полнотекстового поискового движка, написанную на Java . Это объединяет мощь Lucene с простотой Hibernate и JPA.

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

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

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

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

Перед тем, как начать, нам сначала нужно добавить необходимые зависимости в наш 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-master / filesystem-slave и infinispan заслуживают внимания для кластерных приложений, где индекс должен быть синхронизирован между узлами.

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

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

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

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

Поверх аннотаций JPA @Entity и @Table мы должны добавить аннотацию @Indexed . Он сообщает Hibernate Search, что продукт сущности должен быть проиндексирован.

После этого мы должны определить необходимые атрибуты как доступные для поиска, добавив аннотацию @Field :

@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 атрибут будет необходим для «More Like This» запрос позже.

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

Перед запуском фактических запросов мы должны запустить Lucene для первоначального построения индекса :

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

После этой начальной сборки Hibernate Search позаботится о поддержании индекса в актуальном состоянии . I. e. мы можем создавать, управлять и удалять объекты через EntityManager как обычно.

Примечание: мы должны убедиться, что объекты полностью привязаны к базе данных, прежде чем они могут быть обнаружены и проиндексированы Lucene (кстати, это также причина, по которой первоначальный импорт тестовых данных в наших тестовых примерах кода осуществляется в выделенном JUnit тестовый пример, помеченный @Commit ).

6. Создание и выполнение запросов

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

В следующем разделе мы покажем общий рабочий процесс для подготовки и выполнения запроса.

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

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

В целом подготовка и выполнение запроса состоит из четырех шагов :

На шаге 1 мы должны получить JPA FullTextEntityManager и из него 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();

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

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

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

6.2. Ключевые слова запросы

Самый простой вариант использования - это поиск определенного слова .

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

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

Here, keyword() specifies that we are looking for one specific word, onField() tells Lucene where to look and matching() what to look for.

6.3. Fuzzy Queries

Fuzzy queries are working like keyword queries, except that we can define a limit of “fuzziness”, above which Lucene shall accept the two terms as matching.

By withEditDistanceUpTo(), we can define how much a term may deviate from the other. It can be set to 0, 1, and 2, whereby the default value is 2 (note: this limitation is coming from the Lucene's implementation).

By withPrefixLength(), we can define the length of the prefix which shall be ignored by the fuzziness:

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

6.4. Wildcard Queries

Hibernate Search also enables us to execute wildcard queries, i. e. queries for which a part of a word is unknown.

For this, we can use “?” for a single character, and “*” for any character sequence:

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

6.5. Phrase Queries

If we want to search for more than one word, we can use phrase queries. We can either look for exact or for approximate sentences, using phrase() and withSlop(), if necessary. The slop factor defines the number of other words permitted in the sentence:

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

6.6. Simple Query String Queries

With the previous query types, we had to specify the query type explicitly.

If we want to give some more power to the user, we can use simple query string queries: by that, he can define his own queries at runtime.

The following query types are supported:

  • boolean (AND using “+”, OR using “|”, NOT using “-“)
  • prefix (prefix*)
  • phrase (“some phrase”)
  • precedence (using parentheses)
  • fuzzy (fuzy~2)
  • near operator for phrase queries (“some phrase”~3)

The following example would combine fuzzy, phrase and boolean queries:

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

6.7. Range Queries

Range queries search for avalue in between given boundaries. This can be applied to numbers, dates, timestamps, and strings:

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

6.8. More Like This Queries

Our last query type is the “More Like This” – query. For this, we provide an entity, and Hibernate Search returns a list with similar entities, each with a similarity score.

As mentioned before, the termVector = TermVector.YES attribute in our model class is required for this case: it tells Lucene to store the frequency for each term during indexing.

Based on this, the similarity will be calculated at query execution time:

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. Searching More Than One Field

Until now, we only created queries for searching one attribute, using onField().

Depending on the use case, we can also search two or more attributes:

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

Moreover, we can specify each attribute to be searched separately, e. g. if we want to define a boost for one attribute:

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

6.10. Combining Queries

Finally, Hibernate Search also supports combining queries using various strategies:

  • SHOULD: the query should contain the matching elements of the subquery
  • MUST: the query must contain the matching elements of the subquery
  • MUST NOT: the query must not contain the matching elements of the subquery

The aggregations are similar to the boolean ones AND, OR and NOT. However, the names are different to emphasize that they also have an impact on the relevance.

Например, СЛЕДУЕТ между двумя запросами аналогично логическому ИЛИ: если один из двух запросов имеет совпадение, это совпадение будет возвращено.

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

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

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