1. Обзор
В этой статье мы обсудим основы Hibernate Search, способы его настройки и реализуем несколько простых запросов.
2. Основы поиска в спящем режиме
Всякий раз, когда нам нужно реализовать функцию полнотекстового поиска, использование инструментов, с которыми мы уже хорошо знакомы, всегда является плюсом.
Если мы уже используем Hibernate и JPA для ORM, мы всего в одном шаге от Hibernate Search.
Hibernate Search интегрирует Apache Lucene, высокопроизводительную и расширяемую библиотеку полнотекстового поиска, написанную на Java . Это сочетает в себе мощь Lucene с простотой Hibernate и JPA.
Проще говоря, нам просто нужно добавить некоторые дополнительные аннотации к нашим классам предметной области, и инструмент позаботится о таких вещах, как синхронизация базы данных/индекса.
Hibernate Search также обеспечивает интеграцию с Elasticsearch; однако, поскольку он все еще находится на экспериментальной стадии, мы сосредоточимся здесь на Lucene.
3. Конфигурации
3.1. Зависимости Maven
Прежде чем начать, нам сначала нужно добавить необходимые зависимости в наш pom.xml
:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-orm</artifactId>
<version>5.8.2.Final</version>
</dependency>
Для простоты мы будем использовать H2 в качестве нашей базы данных:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>
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, что объект Product
должен быть проиндексирован.
После этого мы должны определить необходимые атрибуты как доступные для поиска, добавив аннотацию @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
потребуется позже для запроса «Еще похоже на это».
5. Создание индекса Lucene
Перед запуском реальных запросов мы должны запустить Lucene для первоначального построения индекса :
FullTextEntityManager fullTextEntityManager
= Search.getFullTextEntityManager(entityManager);
fullTextEntityManager.createIndexer().startAndWait();
После этой начальной сборки Hibernate Search позаботится о поддержании индекса в актуальном состоянии . т. е. мы можем создавать, манипулировать и удалять объекты через 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<Product> results = jpaQuery.getResultList();
Примечание
. По умолчанию Lucene сортирует результаты по релевантности.
Шаги 1, 3 и 4 одинаковы для всех типов запросов.
Далее мы сосредоточимся на шаге 2, т. е. на том, как создавать различные типы запросов.
6.2. Запросы ключевых слов
Самый простой вариант использования — поиск определенного слова .
Это то, что мы на самом деле уже сделали в предыдущем разделе:
Query keywordQuery = queryBuilder
.keyword()
.onField("productName")
.matching("iphone")
.createQuery();
Здесь ключевое слово ()
указывает, что мы ищем одно конкретное слово, onField ()
сообщает Lucene, где искать, а Matching ( )
, что искать.
6.3. Нечеткие запросы
Нечеткие запросы работают так же, как запросы по ключевым словам, за исключением того, что мы можем определить предел «нечеткости» , выше которого Lucene будет принимать два термина как совпадающие.
С помощью withEditDistanceUpTo()
мы можем определить, насколько один термин может отличаться от другого . Его можно установить на 0, 1 и 2, при этом значение по умолчанию равно 2 ( примечание
: это ограничение связано с реализацией Lucene).
С помощью withPrefixLength()
мы можем определить длину префикса, который будет игнорироваться нечеткостью:
Query fuzzyQuery = queryBuilder
.keyword()
.fuzzy()
.withEditDistanceUpTo(2)
.withPrefixLength(0)
.onField("productName")
.matching("iPhaen")
.createQuery();
6.4. Запросы с подстановочными знаками
Hibernate Search также позволяет выполнять запросы с подстановочными знаками, то есть запросы, в которых часть слова неизвестна.
Для этого мы можем использовать « ?»
для одного символа и « *»
для любой последовательности символов:
Query wildcardQuery = queryBuilder
.keyword()
.wildcard()
.onField("productName")
.matching("Z*")
.createQuery();
6.5. фразовые запросы
Если мы хотим найти более одного слова, мы можем использовать фразовые запросы. Мы можем искать точные или приблизительные предложения , используя, при необходимости, фразу()
и функцию withSlop()
. Фактор небрежности определяет количество других слов, разрешенных в предложении:
Query phraseQuery = queryBuilder
.phrase()
.withSlop(1)
.onField("description")
.sentence("with wireless charging")
.createQuery();
6.6. Простые запросы строки запроса
В предыдущих типах запросов нам приходилось явно указывать тип запроса.
Если мы хотим дать пользователю больше возможностей, мы можем использовать простые запросы строки запроса: таким образом, он может определять свои собственные запросы во время выполнения .
Поддерживаются следующие типы запросов:
- логическое значение (И с использованием «+», ИЛИ с использованием «|», НЕ с использованием «-»)
- префикс (префикс*)
- фраза («какая-то фраза»)
- приоритет (используя круглые скобки)
- нечеткий (нечеткий ~ 2)
- оператор Near для фразовых запросов («некоторая фраза»~3)
В следующем примере будут объединены нечеткие, фразовые и логические запросы:
Query simpleQueryStringQuery = queryBuilder
.simpleQueryString()
.onFields("productName", "description")
.matching("Aple~2 + \"iPhone X\" + (256 | 128)")
.createQuery();
6.7. Запросы диапазона
Запросы диапазона ищут значение между заданными границами . Это можно применить к числам, датам, временным меткам и строкам:
Query rangeQuery = queryBuilder
.range()
.onField("memory")
.from(64).to(256)
.createQuery();
6.8. Больше подобных запросов
Наш последний тип запроса — это « More Like This
» — запрос. Для этого мы предоставляем сущность, и Hibernate Search возвращает список с похожими сущностями , каждая из которых имеет оценку сходства.
Как упоминалось ранее, для этого случая требуется атрибут termVector = TermVector.YES
в нашем классе модели: он указывает Lucene сохранять частоту для каждого термина во время индексации.
Исходя из этого, сходство будет рассчитываться во время выполнения запроса:
Query moreLikeThisQuery = queryBuilder
.moreLikeThis()
.comparingField("productName").boostedTo(10f)
.andField("description").boostedTo(1f)
.toEntity(entity)
.createQuery();
List<Object[]> results = (List<Object[]>) fullTextEntityManager
.createFullTextQuery(moreLikeThisQuery, Product.class)
.setProjection(ProjectionConstants.THIS, ProjectionConstants.SCORE)
.getResultList();
6.9. Поиск более чем в одном поле
До сих пор мы создавали запросы только для поиска одного атрибута, используя onField()
.
В зависимости от варианта использования мы также можем искать два или более атрибута :
Query luceneQuery = queryBuilder
.keyword()
.onFields("productName", "description")
.matching(text)
.createQuery();
Более того, мы можем указать каждый атрибут для поиска отдельно , например, если мы хотим определить усиление для одного атрибута:
Query moreLikeThisQuery = queryBuilder
.moreLikeThis()
.comparingField("productName").boostedTo(10f)
.andField("description").boostedTo(1f)
.toEntity(entity)
.createQuery();
6.10. Объединение запросов
Наконец, Hibernate Search также поддерживает объединение запросов с использованием различных стратегий:
СЛЕДУЕТ:
запрос должен содержать совпадающие элементы подзапросаДОЛЖЕН:
запрос должен содержать совпадающие элементы подзапросаНЕ ДОЛЖЕН
: запрос не должен содержать совпадающие элементы подзапроса.
Агрегации аналогичны логическим И, ИЛИ
и НЕ
.
Однако названия разные, чтобы подчеркнуть, что они тоже влияют на релевантность.
Например, SHOULD
между двумя запросами похож на логическое ИЛИ:
если один из двух запросов имеет совпадение, это совпадение будет возвращено.
Однако, если оба запроса совпадают, совпадение будет иметь более высокую релевантность по сравнению с совпадением только одного запроса:
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 и показали, как реализовать наиболее важные типы запросов. Более сложные темы можно найти в официальной документации .
Как всегда, полный исходный код примеров доступен на GitHub .