Перейти к основному содержимому

Введение в поиск в спящем режиме

· 7 мин. чтения

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 .