Use Hibernate Search in Standalone mode with 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

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

实体存储并索引在 Elasticsearch 集群中。

Solution

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

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

解决办法位于 hibernate-search-standalone-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 Search 独立 + Elasticsearch,

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

如果你已经配置了 Quarkus 项目,你可以通过在项目基本目录中运行以下命令将 hibernate-search-standalone-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-standalone-elasticsearch</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-hibernate-search-standalone-elasticsearch")

Creating the bare classes

首先,让我们在 BookAuthor 子包中创建我们的 model 类。

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

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

public class Author {

    public UUID id; (1)

    public String firstName;

    public String lastName;

    public List<Book> books;

    public Author(UUID id, String firstName, String lastName, List<Book> books) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.books = books;
    }
}
1 我们这里使用公共字段,因为它更短,并且对于本质上是数据类的内容,它不会封装任何内容。但是,如果你更喜欢使用带有 getter/setter 的私有字段,那完全没问题,只要 getter/etter 遵循 JavaBeans 命名规范 (getSomething()/isSomething()/setSomething(…​)),它们就会完美地工作。
package org.acme.hibernate.search.elasticsearch.model;

import java.util.Objects;

public class Book {

    public UUID id;

    public String title;

    public Book(UUID id, String title) {
        this.id = id;
        this.title = title;
    }
}

Using Hibernate Search annotations

为我们的类启用全文本搜索功能仅需添加一些注释即可。

让我们编辑 Author 实体来包括此内容:

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

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import org.hibernate.search.engine.backend.types.Sortable;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IdProjection;
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 org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.SearchEntity;

@SearchEntity (1)
@Indexed (2)
public class Author {

    @DocumentId (3)
    public UUID id;

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

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

    @IndexedEmbedded (6)
    public List<Book> books = new ArrayList<>();

    public Author(UUID id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @ProjectionConstructor (7)
    public Author(@IdProjection UUID id, String firstName, String lastName, List<Book> books) {
        this( id, firstName, lastName );
        this.books = books;
    }
}
1 首先,让我们将 Author 类型标记为 entity type。简而言之,这表示 Author 类型有自己独特的生命周期(与其他类型无关),并且每个 `BookAuthor 实例都携带一个不可变的唯一标识符。
2 然后,让我们使用 @Indexed 注释将我们的 Author 实体注册为全文本索引的一部分。
3 最后,让我们通过定义文档标识符来结束强制配置。
4 @FullTextField 批注声明索引中专门针对全文搜索定制的字段。特别是,我们必须定义一个分析器来拆分和分析标记(~ 词)——稍后对此进行更多介绍。
5 正如你所见,我们可以为同一个属性定义多个字段。此处,我们使用特定名称定义了一个 @KeywordField。主要区别在于,关键字字段没有标记化(字符串保持为单个标记),但可以将其标准化(即过滤)——稍后对此进行更多介绍。此字段标记为可排序,因为我们的目的是将其用于对作者进行排序。
6 @IndexedEmbedded 的目的是将 Book 字段包含到 Author 索引中。在这种情况下,我们只需使用默认配置:关联的 Book 实例的所有字段都会包含在索引中(即 title 字段)。@IndexedEmbedded 还支持嵌套文档(使用 structure = NESTED 属性),但我们此处不需要它。如果你不想包含所有字段,还可以使用 includePaths/excludePaths 属性指定要嵌入到父索引中的字段。
7 我们标记一个(单个)构造函数为 @ProjectionConstructor,这样就可以从索引内容重建 Author 实例。

现在我们的作者已经编入索引,我们将需要映射书籍,这样这个 @IndexedEmbedded 注释实际上就会嵌入 something

打开 Book 类,并包含下面的内容。

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

import java.util.Objects;
import java.util.UUID;

import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ProjectionConstructor;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.SearchEntity;

@SearchEntity (1)
public class Book {

    @KeywordField (2)
    public UUID id;

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

    @ProjectionConstructor (4)
    public Book(UUID id, String title) {
        this.id = id;
        this.title = title;
    }
}
1 我们还将 Book 类型标记为 entity type,但我们不使用 @Indexed,因为我们认为不需要专门为书籍制定索引。
2 我们对图书的 ID 编制索引,以便投影(见下文)。
3 我们使用一个 @FullTextField,类似于我们对 Author 所做的事情,但是你会注意到分析器不同 - 稍后将对此进行详细说明。
4 Author 一样,我们标记一个构造函数为 @ProjectionConstructor,以便可以从索引内容重建 Book 实例。

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

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

Implementing the REST service

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

package org.acme.hibernate.search.elasticsearch;

import java.util.ArrayList;
import java.util.UUID;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
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.pojo.standalone.mapping.SearchMapping;
import org.hibernate.search.mapper.pojo.standalone.session.SearchSession;

import org.jboss.resteasy.reactive.RestForm;
import org.jboss.resteasy.reactive.RestPath;

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

    @Inject
    SearchMapping searchMapping; (1)

    @PUT
    @Path("author")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void addAuthor(@RestForm String firstName, @RestForm String lastName) {
        try (var searchSession = searchMapping.createSession()) { (2)
            Author author = new Author(UUID.randomUUID(), firstName, lastName, new ArrayList<>());
            searchSession.indexingPlan().add(author); (3)
        }
    }

    @GET
    @Path("author/{id}")
    public Author getAuthor(@RestPath UUID id) {
        try (var searchSession = searchMapping.createSession()) {
            return getAuthor(searchSession, id);
        }
    }

    private Author getAuthor(SearchSession searchSession, UUID id) {
        return searchSession.search(Author.class) (4)
                .where(f -> f.id().matching(id))
                .fetchSingleHit()
                .orElseThrow(NotFoundException::new);
    }

    @POST
    @Path("author/{id}")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void updateAuthor(@RestPath UUID id, @RestForm String firstName, @RestForm String lastName) {
        try (var searchSession = searchMapping.createSession()) {
            Author author = getAuthor(searchSession, id); (5)
            author.firstName = firstName;
            author.lastName = lastName;
            searchSession.indexingPlan().addOrUpdate(author); (5)
        }
    }

    @DELETE
    @Path("author/{id}")
    public void deleteAuthor(@RestPath UUID id) {
        try (var searchSession = searchMapping.createSession()) {
            searchSession.indexingPlan().purge(Author.class, id, null); (6)
        }
    }

    @PUT
    @Path("author/{authorId}/book/")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void addBook(@RestPath UUID authorId, @RestForm String title) {
        try (var searchSession = searchMapping.createSession()) {
            Author author = getAuthor(searchSession, authorId); (7)
            author.books.add(new Book(authorId, title));
            searchSession.indexingPlan().addOrUpdate(author);
        }
    }

    @DELETE
    @Path("author/{authorId}/book/{bookId}")
    public void deleteBook(@RestPath UUID authorId, @RestPath UUID bookId) {
        try (var searchSession = searchMapping.createSession()) {
            Author author = getAuthor(searchSession, authorId); (7)
            author.books.removeIf(book -> book.id.equals(bookId));
            searchSession.indexingPlan().addOrUpdate(author);
        }
    }
}
1 注入 Hibernate Search 映射,它是 Hibernate Search API 的主入口点。
2 创建 Hibernate Search 会话,允许对索引执行操作。
3 若要索引新的作者,则检索会话的索引计划,然后调用 add,传递参数中的作者实例。
4 若要从索引中检索作者,则执行简单的搜索(稍后会详细介绍搜索)按标识符进行。
5 若要更新作者,则从索引中检索作者,应用更改,检索会话的索引计划,然后调用 addOrUpdate,传递参数中的作者实例。
6 若要按标识符删除作者,则检索会话的索引计划并调用 purge,传递参数中的作者类和标识符。
7 由于作者“拥有”书籍(它们会为每个作者复制并其生命周期与作者的生命周期绑定),因此添加/删除书籍仅仅是作者的一次更新。

这里没有什么开创性:仅仅是 REST 服务中几个 CRUD 操作,使用 Hibernate Search API。

有趣的部分涉及添加搜索端点。在我们的 LibraryResource 中,我们只需要添加以下方法(和一些 import):

    @GET
    @Path("author/search")
    public List<Author> searchAuthors(@RestQuery String pattern, (1)
            @RestQuery Optional<Integer> size) {
        try (var searchSession = searchMapping.createSession()) { (2)
            return searchSession.search(Author.class) (3)
                    .where(f -> pattern == null || pattern.isBlank()
                            ? 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 使用 org.jboss.resteasy.reactive.RestQuery 注释类型来避免重复参数名称。
2 创建 Hibernate Search 会话,允许对索引执行操作。
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

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

让我们在 LibraryResource 中添加一些方法:

    void onStart(@Observes StartupEvent ev) { (1)
        // Index some test data if nothing exists
        try (var searchSession = searchMapping.createSession()) {
            if (0 < searchSession.search(Author.class) (2)
                    .where(f -> f.matchAll())
                    .fetchTotalHitCount()) {
                return;
            }
            for (Author author : initialDataSet()) { (3)
                searchSession.indexingPlan().add(author); (4)
            }
        }
    }

    private List<Author> initialDataSet() {
        return List.of(
                new Author(UUID.randomUUID(), "John", "Irving",
                        List.of(
                                new Book(UUID.randomUUID(), "The World According to Garp"),
                                new Book(UUID.randomUUID(), "The Hotel New Hampshire"),
                                new Book(UUID.randomUUID(), "The Cider House Rules"),
                                new Book(UUID.randomUUID(), "A Prayer for Owen Meany"),
                                new Book(UUID.randomUUID(), "Last Night in Twisted River"),
                                new Book(UUID.randomUUID(), "In One Person"),
                                new Book(UUID.randomUUID(), "Avenue of Mysteries"))),
                new Author(UUID.randomUUID(), "Paul", "Auster",
                        List.of(
                                new Book(UUID.randomUUID(), "The New York Trilogy"),
                                new Book(UUID.randomUUID(), "Mr. Vertigo"),
                                new Book(UUID.randomUUID(), "The Brooklyn Follies"),
                                new Book(UUID.randomUUID(), "Invisible"),
                                new Book(UUID.randomUUID(), "Sunset Park"),
                                new Book(UUID.randomUUID(), "4 3 2 1"))));
    }
1 添加将在应用程序启动时执行的方法。
2 检查索引中是否已经存在数据,如果没有,则退出。
3 Generate the initial dataset.
4 为每个作者,将其添加到索引。

Configuring the application

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

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

quarkus.ssl.native=false 1

quarkus.hibernate-search-standalone.mapping.structure=document 2
quarkus.hibernate-search-standalone.elasticsearch.version=8 3
quarkus.hibernate-search-standalone.indexing.plan.synchronization.strategy=sync 4

%prod.quarkus.hibernate-search-standalone.elasticsearch.hosts=localhost:9200 5
1 我们不使用 SSL,因此禁用它以拥有更紧凑的本机可执行文件。
2 我们需要告诉 Hibernate Search 我们实体的结构。在此应用程序中,我们将索引实体(作者)视为“文档”的根:作者“拥有”它通过关联引用的书籍,cannot 可以独立地更新作者。 另请参阅 <<`quarkus.hibernate-search-standalone.mapping.structure`,quarkus-hibernate-search-standalone-elasticsearch_quarkus-hibernate-search-standalone-mapping-structure>>,了解其他选项和更多详细信息。
3 我们需要告诉 Hibernate Search 我们将使用的 Elasticsearch 版本。这很重要,因为不同版本之间的 Elasticsearch 映射语法之间存在很大差异。由于映射是在构建时创建的以减少启动时间,因此 Hibernate Search 无法连接到该集群以自动检测该版本。请注意,对于 OpenSearch,您需要用 opensearch: 为版本加前缀;请参见 OpenSearch compatibility
4 这意味着我们在认为写入完成之前,会等待实体可搜索。在生产环境设置中,write-sync 的默认设置将提供更好的性能。在测试时使用 sync 特别重要,因为您需要实体立即可搜索。
5 对于开发和测试,我们依赖于 Dev Services,这意味着 Quarkus 将自动启动 Elasticsearch 集群。然而,在生产模式中,我们需要手动启动 Elasticsearch 集群,这就是为什么我们在 prod 配置(%prod. 前缀)中向 Quarkus 提供此连接信息的原因。

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

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

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

如需了解有关 Hibernate Search Standalone 扩展配置的更多信息,请参阅 Configuration Reference

Creating a frontend

现在,让我们添加一个简单的网页与我们的 LibraryResource 进行交互。Quarkus 会自动处理位于 META-INF/resources 目录下的静态资源。在 src/main/resources/META-INF/resources 目录中,使用此 index.html 文件中的内容覆盖现有的 index.html 文件:$${quickstarts-base-url}/blob/main/hibernate-search-standalone-elasticsearch-quickstart/src/main/resources/META-INF/resources/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-standalone-elasticsearch-quickstart-1.0.0-SNAPSHOT-runner 一样简单。

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

启动比平时慢一些:这主要是由于我们每次在启动时删除和重新创建 Elasticsearch 映射所致。我们还索引一些初始数据。 在实际生活中,很显然您不会在每次启动时都执行此操作。

Dev Services (Configuration Free Datastores)

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

对于 Elasticsearch,这种支持扩展到了默认 Elasticsearch 连接。实际上这意味着,如果您尚未配置 quarkus.hibernate-search-standalone.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) 访问它。

映射配置器 (StandalonePojoMappingConfigurer) 允许的功能远不止是按程序设置映射。它还允许 configuring annotation mapping, bridges, and more

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

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

import org.hibernate.search.mapper.pojo.standalone.mapping.StandalonePojoMappingConfigurationContext;
import org.hibernate.search.mapper.pojo.standalone.mapping.StandalonePojoMappingConfigurer;
import org.hibernate.search.mapper.pojo.mapping.definition.programmatic.TypeMappingStep;

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

@SearchExtension (1)
public class CustomMappingConfigurer implements StandalonePojoMappingConfigurer {

	@Override
    public void configure(StandalonePojoMappingConfigurationContext context) {
        TypeMappingStep type = context.programmaticMapping()    (2)
                .type(SomeIndexedEntity.class);                 (3)
        type.searchEntity();                                    (4)
        type.indexed()                                          (5)
                .index(SomeIndexedEntity.INDEX_NAME);           (6)
        type.property("id").documentId();                       (7)
        type.property("text").fullTextField();                  (8)
    }
}
1 使用 @SearchExtension 限定符对配置器实现进行注释,以告知 Quarkus 它应由 Hibernate Search Standalone 使用。
2 访问编程方式映射上下文。
3 SomeIndexedEntity 类型创建映射步骤。
4 SomeIndexedEntity 定义为 Hibernate Search 的实体类型。
5 定义 SomeIndexedEntity 实体为索引。
6 提供要用于 SomeIndexedEntity 实体的索引名称。
7 定义文档 id 属性。
8 text 属性定义一个全文搜索字段。

OpenSearch compatibility

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

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

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

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

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

CDI integration

Injecting entry points

您可以使用 CDI 注入 Hibernate Search 的主要入口点 SearchMapping

@Inject
SearchMapping searchMapping;

Plugging in custom components

适用于 Hibernate Search Standalone 的 Quarkus 扩展会将用 @SearchExtension 注释的组件自动注入 Hibernate Search。

当针对特定后端 (@SearchExtension(backend = "nameOfYourBackend"))、索引 (@SearchExtension(index = "nameOfYourIndex")) 或这些元素的组合 (@SearchExtension(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.pojo.standalone.mapping.StandalonePojoMappingConfigurer

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-standalone.indexing.plan.synchronization.strategy`,quarkus-hibernate-search-standalone-elasticsearch_quarkus-hibernate-search-standalone-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-standalone.elasticsearch.layout.strategy`,quarkus-hibernate-search-standalone-elasticsearch_quarkus-hibernate-search-standalone-elasticsearch-layout-strategy>> 设置为内置实现。 更多信息,请参见 this section of the reference documentation

Offline startup

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

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

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

  • 通过将配置属性 <<`quarkus.hibernate-search-standalone.schema-management.strategy`,quarkus-hibernate-search-standalone-elasticsearch_quarkus-hibernate-search-standalone-schema-management-strategy>> 设置为 none,在启动时禁用架构管理。

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

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

Loading

作为使用 Elasticsearch 作为主要数据存储的替代方案,此扩展还可以用于索引来自其他数据存储的实体。

在这样的情况下,您需要设置 <<`quarkus.hibernate-search-standalone.mapping.structure`,quarkus-hibernate-search-standalone-elasticsearch_quarkus-hibernate-search-standalone-mapping-structure>> 为与主数据存储的结构匹配的值。

为了做到这一点,需要从另一个数据存储加载实体,并且加载必须明确实现。

您可以参考 Hibernate Search 的参考文档,以了解有关配置加载的更多信息:

在 Quarkus 中,Hibernate Search 的参考文档中提到的实体加载器可以定义为 CDI bean,但仍需要使用 @SearchEntity(loadingBinder = …​) 附加到特定实体。

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-standalone.management.enabled=true 2
1 Enable the management interface.
2 启用 Hibernate Search Standalone 特定的管理端点。

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

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

此端点仅接受带有 application/json 内容类型的 POST 请求。如果提交空请求正文,则所有已编入索引的实体都将重新编入索引。

要重新为实体类型编制索引,它需要 configured for loading from an external source。 如果没有该配置,将通过管理端点(或通过任何其他 API)重新编制索引会失败。

如果只需要重新索引一部分实体或需要对底层质量索引器进行自定义配置,那么可以通过请求正文(如下所示)传递此信息。

{
  "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 中将导致管理端点返回一个分块响应,该响应将报告索引何时开始以及何时结束。

Limitations

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#180

Configuration Reference for Hibernate Search Standalone

Unresolved include directive in modules/ROOT/pages/hibernate-search-standalone-elasticsearch.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-hibernate-search-standalone-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

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

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