Use Hibernate Search with Hibernate ORM and Elasticsearch/OpenSearch

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

本指南中介绍的应用程序允许管理一个(简单的)库:您负责管理作者及其书籍。

这些实体存储在 PostgreSQL 数据库中,并编入 Elasticsearch 集群的索引中。

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

该解决方法位于 hibernate-search-orm-elasticsearch-quickstart directory 中。

提供的解决方法包含一些附加元素,例如测试和测试基础设施。

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

适用于 Windows 用户:

  • 如果使用 cmd,(不要使用反斜杠 \ ,并将所有内容放在同一行上)

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令生成一个 Maven 结构,该结构导入以下扩展:

  • Hibernate ORM with Panache,

  • the PostgreSQL JDBC driver,

  • Hibernate Search + Elasticsearch,

  • Quarkus REST(以前称为 RESTEasy Reactive)和 Jackson。

如果您已配置好 Quarkus 项目,则可以通过在项目基本目录中运行以下命令,将 hibernate-search-orm-elasticsearch 扩展添加到您的项目:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到您的 pom.xml

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-search-orm-elasticsearch</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-hibernate-search-orm-elasticsearch")

Creating the bare entities

首先,让我们在 model 子包中创建我们的 Hibernate ORM 实体 BookAuthor

package org.acme.hibernate.search.elasticsearch.model;

import java.util.List;
import java.util.Objects;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Author extends PanacheEntity { (1)

    public String firstName;

    public String lastName;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) (2)
    public List<Book> books;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Author)) {
            return false;
        }

        Author other = (Author) o;

        return Objects.equals(id, other.id);
    }

    @Override
    public int hashCode() {
        return 31;
    }
}
1 我们使用带 Panache 的 Hibernate ORM,它不是强制性的。
2 我们急切地加载这些元素,以便它们出现在 JSON 输出中。在实际应用程序中,您可能应使用 DTO 方法。
package org.acme.hibernate.search.elasticsearch.model;

import java.util.Objects;

import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;

import com.fasterxml.jackson.annotation.JsonIgnore;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
public class Book extends PanacheEntity {

    public String title;

    @ManyToOne
    @JsonIgnore 1
    public Author author;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Book)) {
            return false;
        }

        Book other = (Book) o;

        return Objects.equals(id, other.id);
    }

    @Override
    public int hashCode() {
        return 31;
    }
}
1 我们使用 @JsonIgnore 标记此属性,以在使用 Jackson 序列化时避免出现无限循环。

Initializing the REST service

虽然尚未为我们的 REST 服务设置所有内容,但我们可以使用我们需要的标准 CRUD 操作对其进行初始化。

创建 org.acme.hibernate.search.elasticsearch.LibraryResource 类:

package org.acme.hibernate.search.elasticsearch;

import java.util.List;
import java.util.Optional;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;

import org.acme.hibernate.search.elasticsearch.model.Author;
import org.acme.hibernate.search.elasticsearch.model.Book;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestQuery;

import io.quarkus.runtime.StartupEvent;

@Path("/library")
public class LibraryResource {

    @PUT
    @Path("book")
    @Transactional
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void addBook(@RestForm String title, @RestForm Long authorId) {
        Author author = Author.findById(authorId);
        if (author == null) {
            return;
        }

        Book book = new Book();
        book.title = title;
        book.author = author;
        book.persist();

        author.books.add(book);
        author.persist();
    }

    @DELETE
    @Path("book/{id}")
    @Transactional
    public void deleteBook(Long id) {
        Book book = Book.findById(id);
        if (book != null) {
            book.author.books.remove(book);
            book.delete();
        }
    }

    @PUT
    @Path("author")
    @Transactional
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void addAuthor(@RestForm String firstName, @RestForm String lastName) {
        Author author = new Author();
        author.firstName = firstName;
        author.lastName = lastName;
        author.persist();
    }

    @POST
    @Path("author/{id}")
    @Transactional
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void updateAuthor(Long id, @RestForm String firstName, @RestForm String lastName) {
        Author author = Author.findById(id);
        if (author == null) {
            return;
        }
        author.firstName = firstName;
        author.lastName = lastName;
        author.persist();
    }

    @DELETE
    @Path("author/{id}")
    @Transactional
    public void deleteAuthor(Long id) {
        Author author = Author.findById(id);
        if (author != null) {
            author.delete();
        }
    }
}

这里没有特别之处:它只是在 REST 服务中使用 Panache 操作的传统 Hibernate ORM。

事实上,有趣的部分是我们只需添加极少的元素,即可使我们的全文搜索应用程序正常工作。

Using Hibernate Search annotations

让我们回到我们的实体。

为它们启用全文搜索功能就像添加一些批注一样简单。

我们再次编辑 Book 实体,包含此内容:

package org.acme.hibernate.search.elasticsearch.model;

import java.util.Objects;

import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;

import com.fasterxml.jackson.annotation.JsonIgnore;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
@Indexed (1)
public class Book extends PanacheEntity {

    @FullTextField(analyzer = "english") (2)
    public String title;

    @ManyToOne
    @JsonIgnore
    public Author author;

    // Preexisting equals()/hashCode() methods
}
1 首先,让我们使用 @Indexed 批注将我们的 Book 实体注册为全文索引的一部分。
2 @FullTextField 批注声明索引中专门针对全文搜索定制的字段。特别是,我们必须定义一个分析器来拆分和分析标记(~ 词)——稍后对此进行更多介绍。

既然已经对我们的书进行了索引,我们可以对作者进行相同的操作。

打开 Author 类并包含以下内容。

这里的情况非常相似:我们使用的是 @Indexed@FullTextField@KeywordField 批注。

不过也有一些差异/添加。我们来检查一下。

package org.acme.hibernate.search.elasticsearch.model;

import java.util.List;
import java.util.Objects;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.OneToMany;

import org.hibernate.search.engine.backend.types.Sortable;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexedEmbedded;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;

import io.quarkus.hibernate.orm.panache.PanacheEntity;

@Entity
@Indexed
public class Author extends PanacheEntity {

    @FullTextField(analyzer = "name") (1)
    @KeywordField(name = "firstName_sort", sortable = Sortable.YES, normalizer = "sort") (2)
    public String firstName;

    @FullTextField(analyzer = "name")
    @KeywordField(name = "lastName_sort", sortable = Sortable.YES, normalizer = "sort")
    public String lastName;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @IndexedEmbedded (3)
    public List<Book> books;

    // Preexisting equals()/hashCode() methods
}
1 我们使用了一个类似于 Book@FullTextField,但你会注意到分析器有所不同——稍后对此进行更多介绍。
2 正如你所见,我们可以为同一个属性定义多个字段。此处,我们使用特定名称定义了一个 @KeywordField。主要区别在于,关键字字段没有标记化(字符串保持为单个标记),但可以将其标准化(即过滤)——稍后对此进行更多介绍。此字段标记为可排序,因为我们的目的是将其用于对作者进行排序。
3 @IndexedEmbedded 的目的是将 Book 字段包含到 Author 索引中。在此情况下,我们仅仅使用默认配置:关联的 Book 实体的所有字段都包含在索引中(即 title 字段)。关于 @IndexedEmbedded 的好处是,如果它的其中一个 Book`s has been updated thanks to the bidirectional relation. `@IndexedEmbedded 也支持嵌套文档(使用 structure = NESTED 特性),它能够自动地为一个 Author 重新建立索引,但我们在本文中不需要这样做。如果你不想使用所有字段,你还可以使用 includePaths/excludePaths 特性指定想要嵌入到父索引中的字段。

Analyzers and normalizers

Introduction

分析是全文搜索中的重要部分:它定义了在建立索引或搜索查询时文本的处理方式。

分析器的作用是将文本拆分为标记(~ 词)并过滤它们(例如,将它们全部添加为小写并移除重音)。

标准化器是一种特殊类型的分析器,它将输入保持为单个标记。它尤其适用于对关键字进行排序或编制索引。

有很多捆绑分析器,但你也可以针对你自己的特定目的开发自己的分析器。

你可以在 Analysis section of the Elasticsearch documentation 中了解有关 Elasticsearch 分析框架的更多信息。

Defining the analyzers used

在将 Hibernate Search 批注添加到我们的实体时,我们定义了使用的分析器和标准化器。通常情况下:

@FullTextField(analyzer = "english")
@FullTextField(analyzer = "name")
@KeywordField(name = "lastName_sort", sortable = Sortable.YES, normalizer = "sort")

我们使用:

  • 一个名为 name 的分析器用于人名,

  • 名为 english 的分析器,用于书籍标题

  • 名为 sort 的归一器,用于我们的排序字段

但我们尚未设置它们。

让我们看看你可以借助 Hibernate Search 如何实现它。

Setting up the analyzers

这是一项简单的任务,我们只需创建一个 ElasticsearchAnalysisConfigurer 的实现(稍后详细介绍,配置 Quarkus 以便使用它)。

为了满足我们的要求,我们来创建以下实现:

package org.acme.hibernate.search.elasticsearch.config;

import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurationContext;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer;

import io.quarkus.hibernate.search.orm.elasticsearch.SearchExtension;

@SearchExtension (1)
public class AnalysisConfigurer implements ElasticsearchAnalysisConfigurer {

    @Override
    public void configure(ElasticsearchAnalysisConfigurationContext context) {
        context.analyzer("name").custom() (2)
                .tokenizer("standard")
                .tokenFilters("asciifolding", "lowercase");

        context.analyzer("english").custom() (3)
                .tokenizer("standard")
                .tokenFilters("asciifolding", "lowercase", "porter_stem");

        context.normalizer("sort").custom() (4)
                .tokenFilters("asciifolding", "lowercase");
    }
}
1 使用 @SearchExtension 限定名对配置器实现进行注释,以告知 Quarkus 在所有 Elasticsearch 索引(默认情况下)中将其用于默认持久化单元。此注释还可以针对特定的持久化单元 (@SearchExtension(persistenceUnit = "nameOfYourPU"))、后端 (@SearchExtension(backend = "nameOfYourBackend"))、索引 (@SearchExtension(index = "nameOfYourIndex"))或它们的组合 (@SearchExtension(persistenceUnit = "nameOfYourPU", backend = "nameOfYourBackend", index = "nameOfYourIndex"))。
2 这是一个简单的分析器,它在空格上分隔单词,通过其 ASCII 对应项移除任何非 ASCII 字符(因此移除了重音符号),并将所有内容变为小写。我们在作者姓名中对此使用了这个示例。
3 我们对这个分析器采取更加激进的态度,而且我们包括了一些词干提取:我们可搜索 mystery,即使索引输入包含 mysteries,也能够获得结果。它绝对对人名而言太过于激进了,但对书籍标题而言很适合。
4 以下是用于排序的归一器。与我们的第一个分析器非常相似,差别在于我们不标记单词,因为我们只需要一个标记。

或者,如果出于某种原因你无法或不希望使用 @SearchExtension 为你的分析配置器添加注释,则只需使用 @Dependent @Named("myAnalysisConfigurer") 为其添加注释,然后从配置属性中引用它:

quarkus.hibernate-search-orm.elasticsearch.analysis.configurer=bean:myAnalysisConfigurer

有关配置分析器的更多信息,请参阅 this section of the reference documentation

Adding full text capabilities to our REST service

在我们现有的 LibraryResource 中,我们只需要注入 SearchSession

    @Inject
    SearchSession searchSession; (1)
1 注入一个 Hibernate Search 会话,它在后台依靠 EntityManager。具有多个持久化单元的应用程序可以使用 CDI 限定名 @io.quarkus.hibernate.orm.PersistenceUnit 来选择正确的单元:请参阅 CDI integration

然后神奇的事情就发生了。当我们为实体添加注释时,我们使用户能够进行全文搜索;现在,我们可以通过添加以下方法(和一些 import)来使用 Hibernate Search DSL 查询索引:

    @GET
    @Path("author/search")
    @Transactional (1)
    public List<Author> searchAuthors(@RestQuery String pattern, (2)
            @RestQuery Optional<Integer> size) {
        return searchSession.search(Author.class) (3)
                .where(f ->
                    pattern == null || pattern.trim().isEmpty() ?
                        f.matchAll() : (4)
                        f.simpleQueryString()
                                .fields("firstName", "lastName", "books.title").matching(pattern) (5)
                )
                .sort(f -> f.field("lastName_sort").then().field("firstName_sort")) (6)
                .fetchHits(size.orElse(20)); (7)
    }
1 重要提示:此方法需要一个事务上下文。
2 使用 org.jboss.resteasy.reactive.RestQuery 注释类型来避免重复参数名称。
3 我们表明我们要搜索 作者
4 我们创建一个谓词:如果模式为空,则使用 matchAll() 谓词。
5 如果我们有有效的模式,则在匹配我们模式的 firstNamelastNamebooks.title 字段上创建 ` simpleQueryString()` 谓词。
6 我们定义结果的排序顺序。此处按姓氏排序,然后按名字排序。请注意,我们使用为排序创建的特定字段。
7 获取 size 个热门命中,默认情况下为 20。显然,也支持分页。

Hibernate Search DSL 支持 Elasticsearch 谓词的大部分子集(匹配、范围、嵌套、短语、空间…​)。欢迎使用自动完成来探索 DSL。 如果这些还不够,则可以随时退回到 ` defining a predicate using JSON directly`。

Automatic data initialization

为了演示,我们导入初始数据集。

让我们创建一个具有以下内容的 src/main/resources/import.sql 文件(我们将在配置中引用它):

INSERT INTO author(id, firstname, lastname) VALUES (1, 'John', 'Irving');
INSERT INTO author(id, firstname, lastname) VALUES (2, 'Paul', 'Auster');
ALTER SEQUENCE author_seq RESTART WITH 3;

INSERT INTO book(id, title, author_id) VALUES (1, 'The World According to Garp', 1);
INSERT INTO book(id, title, author_id) VALUES (2, 'The Hotel New Hampshire', 1);
INSERT INTO book(id, title, author_id) VALUES (3, 'The Cider House Rules', 1);
INSERT INTO book(id, title, author_id) VALUES (4, 'A Prayer for Owen Meany', 1);
INSERT INTO book(id, title, author_id) VALUES (5, 'Last Night in Twisted River', 1);
INSERT INTO book(id, title, author_id) VALUES (6, 'In One Person', 1);
INSERT INTO book(id, title, author_id) VALUES (7, 'Avenue of Mysteries', 1);
INSERT INTO book(id, title, author_id) VALUES (8, 'The New York Trilogy', 2);
INSERT INTO book(id, title, author_id) VALUES (9, 'Mr. Vertigo', 2);
INSERT INTO book(id, title, author_id) VALUES (10, 'The Brooklyn Follies', 2);
INSERT INTO book(id, title, author_id) VALUES (11, 'Invisible', 2);
INSERT INTO book(id, title, author_id) VALUES (12, 'Sunset Park', 2);
INSERT INTO book(id, title, author_id) VALUES (13, '4 3 2 1', 2);
ALTER SEQUENCE book_seq RESTART WITH 14;

因为上述数据将在 Hibernate Search 不知情的情况下插入数据库中,所以它不会被索引,这与通过 Hibernate ORM 操作进行的即将到来的更新不同,即将到来的更新会自动同步到全文索引。

在我们现有的 LibraryResource 中,让我们添加以下内容(以及一些 import)来索引该初始数据:

如果你不手动导入数据库中的数据,则不需要这样做:只有在你更改索引配置(添加新字段、更改分析器的配置…​)且希望将新配置应用于现有数据时,才应该使用批量索引器。

    @Inject
    SearchMapping searchMapping; (1)

    void onStart(@Observes StartupEvent ev) throws InterruptedException { (2)
        // only reindex if we imported some content
        if (Book.count() > 0) {
            searchMapping.scope(Object.class) (3)
                    .massIndexer() (4)
                    .startAndWait(); (5)
        }
    }
1 注入 Hibernate Search SearchMapping,它依赖底层的 EntityManagerFactory。具有多个持久性单元的应用程序可以使用 CDI 限定符 @io.quarkus.hibernate.orm.PersistenceUnit 选择正确的持久性单元:请参阅 CDI integration
2 添加将在应用程序启动时执行的方法。
3 创建一个针对扩展 Object 的所有已索引实体类型的“搜索范围”,即每个已索引的实体类型(AuthorBook)。
4 创建一个 Hibernate Search 批量索引器的实例,该实例允许有效地索引大量数据(你可以对其进行微调以获得更好的性能)。
5 启动批量索引器并等待它完成。

Configuring the application

和往常一样,我们可以在 Quarkus 配置文件 application.properties 中配置所有内容。

编辑 src/main/resources/application.properties 并注入以下配置:

quarkus.ssl.native=false 1

quarkus.datasource.db-kind=postgresql 2

quarkus.hibernate-orm.sql-load-script=import.sql 3

quarkus.hibernate-search-orm.elasticsearch.version=8 4
quarkus.hibernate-search-orm.indexing.plan.synchronization.strategy=sync 5

%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost/quarkus_test 6
%prod.quarkus.datasource.username=quarkus_test
%prod.quarkus.datasource.password=quarkus_test
%prod.quarkus.hibernate-orm.database.generation=create
%prod.quarkus.hibernate-search-orm.elasticsearch.hosts=localhost:9200 6
1 我们不使用 SSL,因此禁用它以拥有更紧凑的本机可执行文件。
2 让我们创建一个 PostgreSQL 数据源。
3 我们在启动时加载了一些初始数据(请参见 Automatic data initialization)。
4 我们需要告诉 Hibernate Search 我们将使用的 Elasticsearch 版本。这很重要,因为 Elasticsearch 映射语法在不同版本之间存在显著差异。由于映射是在构建时创建的以缩短启动时间,所以 Hibernate Search 无法连接到集群以自动检测版本。请注意,对于 OpenSearch,您需要使用 opensearch: 为版本添加前缀;请参阅 OpenSearch compatibility
5 这意味着我们在认为写入完成之前,会等待实体可搜索。在生产环境设置中,write-sync 的默认设置将提供更好的性能。在测试时使用 sync 特别重要,因为您需要实体立即可搜索。
6 对于开发和测试,我们依赖 Dev Services,这意味着 Quarkus 将自动启动 PostgreSQL 数据库和 Elasticsearch 集群。然而,在生产模式下,我们希望手动启动 PostgreSQL 数据库和 Elasticsearch 集群,这就是我们向 Quarkus 提供 prod 个人资料(%prod. 前缀)中的此连接信息的原因。

因为我们依赖 Dev Services,所以在测试和开发模式下,在每次应用程序启动时,数据库和 Elasticsearch 模式将自动删除并重新创建(除非显式设置 <<`quarkus.hibernate-search-orm.schema-management.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-schema-management-strategy>>)。 如果由于某种原因无法使用 Dev Services,则必须设置以下属性以获取类似的行为:

%dev,test.quarkus.hibernate-orm.database.generation=drop-and-create
%dev,test.quarkus.hibernate-search-orm.schema-management.strategy=drop-and-create

另请参阅 <<`quarkus.hibernate-search-orm.schema-management.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-schema-management-strategy>>。

有关 Hibernate Search ORM 扩展配置的更多信息,请参阅 Configuration Reference

Creating a frontend

现在,让我们添加一个简单的网页以与我们的 LibraryResource 进行交互。Quarkus 自动为位于 META-INF/resources 目录下的静态资源提供服务。在 src/main/resources/META-INF/resources 目录中,使用来自此 $${quickstarts-base-url}/blob/main/hibernate-search-orm-elasticsearch-quickstart/src/main/resources/META-INF/resources/index.html[index.html] 文件的内容覆盖现有的 index.html 文件。

Time to play with your application

你现在可以与你的 REST 服务进行交互:

  • 使用以下命令启动 Quarkus 应用程序:include::./_includes/devtools/dev.adoc[]

  • 打开浏览器访问 http://localhost:8080/

  • 搜索作者或书籍标题(我们为你初始化了一些数据)

  • 创建新作者和书籍,并对它们进行搜索

如你所见,你的所有更新都自动同步到 Elasticsearch 集群。

Building a native executable

您可以使用以下常用命令构建一个本机可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

与本机可执行文件编译一样,此操作会消耗大量内存。 在构建本机可执行文件时,停止这两个容器可能更安全,完成后再重新启动它们。

运行本机可执行文件像执行 ./target/hibernate-search-orm-elasticsearch-quickstart-1.0.0-SNAPSHOT-runner 一样简单。

然后,您可以将浏览器指向 http://localhost:8080/ 并使用您的应用程序。

启动比平时慢一点:大部分是我们移除并重新创建数据库模式和 Elasticsearch 映射的缘故,每次都在启动时。我们还注入了一些数据并执行 mass 索引器。 在实际生活中,很显然您不会在每次启动时都执行此操作。

Dev Services (Configuration Free Datastores)

Quarkus 支持一项名为 Dev Services 的功能,允许您在没有任何配置的情况下启动各种容器。

Elasticsearch 的支持延伸到默认 Elasticsearch 连接。实际上,这意味着如果您尚未配置 quarkus.hibernate-search-orm.elasticsearch.hosts,Quarkus 会在执行测试或 dev mode 时自动启动 Elasticsearch 容器,并自动配置连接。

在运行应用程序的生产版本时,Elasticsearch 连接需要正常配置,因此如果您想在 application.properties 中包含生产数据库配置,并且希望继续使用 Dev 服务,我们建议您使用 %prod. 配置文件来定义您的 Elasticsearch 设置。

Elasticsearch 的 Dev 服务目前无法并发启动多个集群,因此它只适用于默认持久性单元的默认后端:命名持久性单元或命名后端无法利用 Elasticsearch 的 Dev 服务。

更多信息,你可以阅读 Dev Services for Elasticsearch guide

Programmatic mapping

如果无法添加 Hibernate Search 注解到实体中,则可以相反,以编程方式应用映射。编程方式映射通过 ProgrammaticMappingConfigurationContext 进行配置,可以通过映射配置器 (HibernateOrmSearchMappingConfigurer) 访问它。

映射配置器 (HibernateOrmSearchMappingConfigurer) 的功能不仅仅是编程方式映射功能。它还允许 configuring annotation mapping, bridges, and more

以下是对应用编程方式映射的映射配置器的一个示例:

package org.acme.hibernate.search.elasticsearch.config;

import org.hibernate.search.mapper.orm.mapping.HibernateOrmMappingConfigurationContext;
import org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.TypeMappingStep;

import io.quarkus.hibernate.search.orm.elasticsearch.SearchExtension;

@SearchExtension (1)
public class CustomMappingConfigurer implements HibernateOrmSearchMappingConfigurer {

	@Override
    public void configure(HibernateOrmMappingConfigurationContext context) {
        TypeMappingStep type = context.programmaticMapping()    (2)
            .type(SomeIndexedEntity.class);                     (3)
        type.indexed()                                          (4)
            .index(SomeIndexedEntity.INDEX_NAME);               (5)
        type.property("id").documentId();                       (6)
        type.property("text").fullTextField();                  (7)
    }
}
1 使用 @SearchExtension 限定符注释配置器实现,以告诉 Quarkus 它应该在默认持久性单元中用于 Hibernate Search。该注解还可以针对特定的持久性单元 (@SearchExtension(persistenceUnit = "nameOfYourPU"))。
2 访问编程方式映射上下文。
3 创建 SomeIndexedEntity 实体的映射步骤。
4 定义 SomeIndexedEntity 实体为索引。
5 提供要用于 SomeIndexedEntity 实体的索引名称。
6 定义文档 id 属性。
7 text 属性定义一个全文搜索字段。

或者,如果您出于某种原因无法或不想使用 @SearchExtension 为映射配置程序添加注释,则可以简单地使用 @Dependent @Named("myMappingConfigurer") 为其添加注释,然后在配置属性中引用它:

quarkus.hibernate-search-orm.mapping.configurer=bean:myMappingConfigurer

OpenSearch compatibility

Hibernate Search 与 ElasticsearchOpenSearch 都兼容,但它默认情况下会假定它正在与一个 Elasticsearch 集群工作。

要让 Hibernate Search 与一个 OpenSearch 集群一起工作,请执行以下操作: prefix the configured version with opensearch:

quarkus.hibernate-search-orm.elasticsearch.version=opensearch:2.16

所有其它配置选项和 API 与 Elasticsearch 中的配置选项和 API 完全相同。

您可以在 this section of Hibernate Search’s reference documentation 中找到有关 Elasticsearch 的兼容发行版和版本的更多信息。

Multiple persistence units

Configuring multiple persistence units

借助 Hibernate ORM 扩展,you can set up multiple persistence units,每个扩展都有自己的数据源和配置。

如果您确实声明了多个持久性单元,那么您还将为每个持久性单元单独配置 Hibernate Search。

quarkus.hibernate-search-orm. 命名空间根目录的属性定义了默认持久性单元。例如,以下代码片段定义了一个默认数据源和一个默认持久性单元,并为该持久性单元将 Elasticsearch 主机设置为 es1.mycompany.com:9200

quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1

quarkus.hibernate-search-orm.elasticsearch.hosts=es1.mycompany.com:9200
quarkus.hibernate-search-orm.elasticsearch.version=8

使用基于 map 的方法,也可以配置命名持久性单元:

quarkus.datasource."users".db-kind=h2 1
quarkus.datasource."users".jdbc.url=jdbc:h2:mem:users;DB_CLOSE_DELAY=-1

quarkus.datasource."inventory".db-kind=h2 2
quarkus.datasource."inventory".jdbc.url=jdbc:h2:mem:inventory;DB_CLOSE_DELAY=-1

quarkus.hibernate-orm."users".datasource=users 3
quarkus.hibernate-orm."users".packages=org.acme.model.user

quarkus.hibernate-orm."inventory".datasource=inventory 4
quarkus.hibernate-orm."inventory".packages=org.acme.model.inventory

quarkus.hibernate-search-orm."users".elasticsearch.hosts=es1.mycompany.com:9200 5
quarkus.hibernate-search-orm."users".elasticsearch.version=8

quarkus.hibernate-search-orm."inventory".elasticsearch.hosts=es2.mycompany.com:9200 6
quarkus.hibernate-search-orm."inventory".elasticsearch.version=8
1 定义一个名为 users 的数据源。
2 定义一个名为 inventory 的数据源。
3 定义一个名为 users 的持久性单元,该单元指向 users 数据源。
4 定义一个名为 inventory 的持久性单元,该单元指向 inventory 数据源。
5 users 持久性单元配置 Hibernate Search,将该持久性单元的 Elasticsearch 主机设置为 es1.mycompany.com:9200
6 inventory`持久性单元配置 Hibernate 搜索,将该持久性单元的 Elasticsearch 主机设置成 `es2.mycompany.com:9200

Attaching model classes to persistence units

对于每个持久性单元,Hibernate 搜索将仅考虑附加到该持久性单元的已索引实体。实体通过 configuring the Hibernate ORM extension附加到持久性单元。

CDI integration

Injecting entry points

您可以使用 CDI 注入 Hibernate 搜索的主入口点 SearchSession`和 `SearchMapping

@Inject
SearchSession searchSession;

这将注入默认持久性单元的 SearchSession

为了注入命名持久性单元(在本例中为 users)的 SearchSession,只需添加一个限定符:

@Inject
@PersistenceUnit("users") 1
SearchSession searchSession;
1 这是 `@io.quarkus.hibernate.orm.PersistenceUnit`注释。

您可以使用完全相同的机制注入命名持久性单元的 SearchMapping

@Inject
@PersistenceUnit("users")
SearchMapping searchMapping;

Plugging in custom components

具有 Hibernate ORM 的 Hibernate 搜索的 Quarkus 扩展将自动将带有 `@SearchExtension`注释的组件注入到 Hibernate 搜索中。

当有意义时,该注释可以选择针对特定的持久性单元 (@SearchExtension(persistenceUnit = "nameOfYourPU"))、后端 (@SearchExtension(backend = "nameOfYourBackend"))、索引 (@SearchExtension(index = "nameOfYourIndex")) 或它们的组合 (@SearchExtension(persistenceUnit = "nameOfYourPU", backend = "nameOfYourBackend", index = "nameOfYourIndex")) 进行。

以下组件类型可使用此功能:

org.hibernate.search.engine.reporting.FailureHandler

A component that should be notified of any failure occurring in a background process (mainly index operations).

范围:每个持久性单元一个。 请参阅 this section of the reference documentation获取更多信息。

org.hibernate.search.mapper.orm.mapping.HibernateOrmSearchMappingConfigurer

A component used to configure the Hibernate Search mapping, in particular programmatically.

范围:每个持久性单元一个或多个。 请参阅 this section of this guide获取更多信息。

org.hibernate.search.mapper.pojo.work.IndexingPlanSynchronizationStrategy

A component used to configure how to synchronize between application threads and indexing.

范围:每个持久性单元一个。 还可以通过 <<`quarkus.hibernate-search-orm.indexing.plan.synchronization.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-indexing-plan-synchronization-strategy>> 设置为内置实现。 请参阅 this section of the reference documentation获取更多信息。

org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer

A component used to configure full text analysis (e.g. analyzers, normalizers).

范围:每个后端一个或多个。 请参阅 this section of this guide获取更多信息。

org.hibernate.search.backend.elasticsearch.index.layout.IndexLayoutStrategy

A component used to configure the Elasticsearch layout: index names, index aliases, …​

范围:每个后端一个。 也可以通过 <<`quarkus.hibernate-search-orm.elasticsearch.layout.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-elasticsearch-layout-strategy>> 设置到内置实现。 更多信息,请参见 this section of the reference documentation

Offline startup

默认情况下,Hibernate Search 在启动时会向 Elasticsearch 集群发送一些请求。如果 Elasticsearch 集群在 Hibernate Search 启动时不必定启动并运行,则会导致启动失败。

为了解决这个问题,您可以将 Hibernate Search 配置为在开始时不发送任何请求:

  • 通过将配置属性 <<`quarkus.hibernate-search-orm.elasticsearch.version-check.enabled`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-elasticsearch-version-check-enabled>> 设置为 `false`来禁用 Elasticsearch 启动时的版本检查。

  • 通过将配置属性 <<`quarkus.hibernate-search-orm.schema-management.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-schema-management-strategy>> 设置为 `none`来禁用启动时的模式管理。

当然,即使进行了此项配置,Hibernate Search 仍然无法在 Elasticsearch 集群可访问前索引任何内容或运行搜索查询。

如果您通过将 quarkus.hibernate-search-orm.schema-management.strategy 设置为 `none`来禁用自动模式创建,则必须在应用程序开始持久化/更新实体并执行搜索请求之前,在某个时间点手动创建该模式。 更多信息,请参见 this section of the reference documentation

Coordination through outbox polling

使用收件箱轮询的协调被视为预览版。 在 preview 中,向后兼容性和生态环境中的交互性没有得到保证。具体的改进可能需要修改配置或 API,甚至存储格式,并且成为 stable 的计划正在进行中。欢迎在我们的 mailing list 中提供反馈,或作为我们在 GitHub issue tracker 中的问题。

尽管在分布式应用程序中使用 Hibernate Search 和 Elasticsearch 在技术上是可行的,但它们在默认情况下会受到 a few limitations 的影响。

这些限制是因为 Hibernate Search 默认情况下并未在各个线程或应用程序节点之间协调。

为了排除这些限制,您可以 use the outbox-polling coordination strategy。此策略在数据库中创建了一个收件箱表,用于将实体更改事件推送到该表,并依靠后台处理器来消耗这些事件并执行索引编制。

要启用 outbox-polling 协调策略,需要一个附加的扩展:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

一旦有了该扩展,您将需要通过将 <<`quarkus.hibernate-search-orm.coordination.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-coordination-strategy>> 设置为 outbox-polling 显式选择 outbox-polling 策略。

最后,您需要确保 Hibernate Search(用来表示收件箱和代理人)添加的 Hibernate ORM 实体,在您的数据库中具有对应的表/序列:

数据库模式 Hibernate Search 期望进行 outbox 轮询协调可以通过以下配置属性进行自定义:

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.catalog`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-agent-catalog>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.schema`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-agent-schema>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.table`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-agent-table>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-gen-strategy`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-agent-uuid-gen-strategy>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.agent.uuid-type`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-agent-uuid-type>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.catalog`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-outbox-event-catalog>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.schema`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-outbox-event-schema>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.table`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-outbox-event-table>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-gen-strategy`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-outbox-event-uuid-gen-strategy>>

  • <<`quarkus.hibernate-search-orm.coordination.entity-mapping.outbox-event.uuid-type`,quarkus-hibernate-search-orm-outbox-polling_quarkus-hibernate-search-orm-coordination-entity-mapping-outbox-event-uuid-type>>

完成上述操作后,您就可以使用 Hibernate Search 和出站信箱了。无需更改任何代码,只需启动应用程序即可:它将自动检测到多个应用程序是否已连接到同一数据库,并相应地协调索引更新。

使用 outbox-polling 协调策略时,Hibernate Search 的行为大多与不使用该策略时相同:应用程序代码(持久化实体、搜索等)无需进行任何更改。 但是,有一个关键区别:索引更新必然是异步的;可以保证索引更新必定会发生,但不是立即发生。 这意味着配置属性 <<`quarkus.hibernate-search-orm.indexing.plan.synchronization.strategy`,quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-indexing-plan-synchronization-strategy>>在使用 outbox-polling 协调策略时无法设置:Hibernate Search 始终行为得仿佛该属性已设置为 write-sync(默认值)。 此行为与 Elasticsearch 的 near-real-time search 一致,即使在禁用协调后也是使用 Hibernate Search 的推荐方式。

有关 Hibernate Search 中协调的更多信息,请参阅 this section of the reference documentation

有关与协调相关的配置选项的更多信息,请参阅 Configuration of coordination with outbox polling

[id="configuration-reference-aws"] AWS request signing

如果您需要使用 Amazon’s managed Elasticsearch service,您会发现它需要一种涉及请求签名的专有身份验证方法。

您可以通过向项目添加专用扩展并对其进行配置来启用 Hibernate Search 中的 AWS 请求签名。

Management endpoint

Hibernate Search 的管理端点被认为是预览版。 在 preview 中,向后兼容性和生态环境中的交互性没有得到保证。具体的改进可能需要修改配置或 API,甚至存储格式,并且成为 stable 的计划正在进行中。欢迎在我们的 mailing list 中提供反馈,或作为我们在 GitHub issue tracker 中的问题。

Hibernate Search 扩展通过 management interface 提供一个 HTTP 端点来重新索引您的数据。默认情况下,此端点不可用。可以通过如下所示的配置属性来启用它。

quarkus.management.enabled=true 1
quarkus.hibernate-search-orm.management.enabled=true 2
1 Enable the management interface.
2 启用 Hibernate Search 特定的管理端点。

启用管理端点之后,可以使用 /q/hibernate-search/reindex`重新索引数据,其中 `/q 是默认管理根路径,而 /hibernate-search 是默认 Hibernate Search 根管理路径。它 (/hibernate-search) 可以通过配置属性进行更改,如下所示。

quarkus.hibernate-search-orm.management.root-path=custom-root-path 1
1 使用自定义 custom-root-path 路径作为 Hibernate Search 的管理端点。如果使用默认管理根路径,则重新索引路径将变为 /q/custom-root-path/reindex

此端点仅接受具有 application/json 内容类型的 POST 请求。如果提交空请求正文,将重新索引所有索引实体。如果仅需要重新索引实体子集或需要对底层批量索引器进行自定义配置,则该信息可以通过请求正文传递,如下所示。

{
  "filter": {
    "types": ["EntityName1", "EntityName2", "EntityName3", ...], 1
  },
  "massIndexer":{
    "typesToIndexInParallel": 1, 2
  }
}
1 应重新索引的实体名称数组。如果未指定或空,则重新索引所有实体类型。
2 设置并行索引的实体类型数量。

以下示例中提供了可能的过滤器和可用批量索引器配置的完整列表。

{
  "filter": { 1
    "types": ["EntityName1", "EntityName2", "EntityName3", ...], 2
    "tenants": ["tenant1", "tenant2", ...] 3
  },
  "massIndexer":{ 4
    "typesToIndexInParallel": 1, 5
    "threadsToLoadObjects": 6,  6
    "batchSizeToLoadObjects": 10, 7
    "cacheMode": "IGNORE", 8
    "mergeSegmentsOnFinish": false, 9
    "mergeSegmentsAfterPurge": true, 10
    "dropAndCreateSchemaOnStart": false, 11
    "purgeAllOnStart": true, 12
    "idFetchSize": 100, 13
    "transactionTimeout": 100000, 14
  }
}
1 允许限制重新索引范围的过滤器对象。
2 应重新索引的实体名称数组。如果未指定或空,则重新索引所有实体类型。
3 在多租户的情况下,租户 ID 数组。如果未指定或空,则重新索引所有租户。
4 Mass indexer configuration object.
5 设置并行索引的实体类型数量。
6 设置用于加载根实体的线程数。
7 设置用于加载根实体的批处理大小。
8 设置数据加载任务的缓存交互模式。
9 每个索引是否在索引后合并到单个段中。
10 每个索引是否在最初的索引清除后在索引之前合并到单个段中。
11 在索引之前是否应该删除索引及其架构(如果存在)。
12 在索引之前是否从索引中删除所有实体。
13 指定在加载要索引的对象的主键时要使用的获取大小。
14 指定加载要重新索引的 ID 和实体的事务超时。请注意,JSON 中的所有属性都是可选的,并且仅应使用所需的属性。

有关批量索引器配置的更详细信息,请参阅 corresponding section of the Hibernate Search reference documentation

提交重新索引请求将触发后台的索引。大范围索引进度将显示在应用程序日志中。出于测试的目的,了解索引何时完成可能很有用。将 wait_for=finished 查询参数添加到 URL 中将导致管理端点返回一个分块响应,该响应将报告索引何时开始以及何时结束。

在使用多个持久性单元时,可通过 persistence_unit 查询参数提供要重新索引的持久性单元的名称:/q/hibernate-search/reindex?persistence_unit=non-default-persistence-unit

Further reading

如果您有兴趣了解更多关于 Hibernate Search 的内容,Hibernate 团队发布了 an extensive reference documentation,以及列出 other relevant resources 的页面。

FAQ

Why Elasticsearch only?

Hibernate Search 同时支持 Lucene 后台和 Elasticsearch 后台。

在 Quarkus 的背景下,为了构建可扩展的应用程序,我们认为后者更有意义。因此,我们集中精力于此。

我们目前还没有计划在 Quarkus 中支持 Lucene 后台,尽管有一个问题在跟踪 Quarkiverse 中此类实现的进度: quarkiverse/quarkus-hibernate-search-extras#179

Configuration Reference for Hibernate Search with Hibernate ORM

Main Configuration

Unresolved include directive in modules/ROOT/pages/hibernate-search-orm-elasticsearch.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-hibernate-search-orm-elasticsearch.adoc[]

About bean references

首先,请注意在配置属性中引用 bean 是可选的,事实上是不鼓励的:可以通过用 `@SearchExtension`注释 bean 来实现相同的结果。有关更多信息,请参见 this section。 如果您真的想要在配置属性中使用字符串值来引用 bean,请知道该字符串已被解析;这里是最常见的格式:

  • bean:`后跟 `@Named`CDI bean 的名称。例如 `bean:myBean

  • class:`后跟类的完全限定名称,如果它是 CDI bean,则通过 CDI 实例化它,否则通过其公共无参数构造函数实例化它。例如 `class:com.mycompany.MyClass

  • 引用内置实现的任意字符串。可用值在每个配置属性的文档中详细说明,例如 async/read-sync/write-sync/sync`用于 <<`quarkus.hibernate-search-orm.indexing.plan.synchronization.strategy, quarkus-hibernate-search-orm-elasticsearch_quarkus-hibernate-search-orm-indexing-plan-synchronization-strategy>>。

也接受其他格式,但仅适用于高级用例。有关更多信息,请参见 this section of Hibernate Search’s reference documentation

Configuration of coordination with outbox polling

这些配置属性需要附加扩展。请参见 Coordination through outbox polling

Unresolved include directive in modules/ROOT/pages/hibernate-search-orm-elasticsearch.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-hibernate-search-orm-outbox-polling.adoc[]