Quarkus 和 Hibernate Search 的索引滚动

这是本系列的第一篇文章,深入探讨了 quarkus.io 引导搜索的后端应用程序的实现细节。

您的应用程序是否需要全文搜索功能?您是否需要在不中断服务的情况下让您的应用程序保持运行并生成搜索结果,即使在重新索引所有数据时也是如此?别再寻找了。在本文中,我们将介绍如何处理这个问题,并通过一些低级 API 在实践中解决它,前提是您使用 Hibernate Search,无论是 基于 Hibernate ORM 还是 独立模式

本文提出的方法基于这样一个事实:Hibernate Search 使用 别名索引,并通过读/写别名与实际索引进行通信,具体取决于它需要执行的操作。例如,搜索操作将被路由到读取索引别名,而索引操作将被发送到写入索引别名。

initial app
此方法已在我们的 Quarkus 应用程序中实现并成功使用,该应用程序支持 quarkus.io/guides/ 的引导搜索。您可以在此处查看完整的实现:切换实现切换使用

使用 Hibernate Search 的应用程序可以通过逐步更新索引来保持其搜索索引的最新状态,因为用于索引文档的基础数据已得到修改,从而实现了近乎实时地同步索引。

另一方面,如果搜索要求允许同步延迟,或者数据仅在一天中的特定时间更新,则批量索引选项可以有效地使索引保持最新。Hibernate Search 文档提供了有关这些方法和其他 Hibernate Search 功能的更多信息。

本文讨论的应用程序正在使用批量索引方法。这意味着在某些事件(例如,应用程序新版本部署或到达预定时间)时,应用程序必须处理文档指南并从中创建搜索索引文档。

现在,由于我们希望我们的应用程序在我们添加/更新索引中的文档时继续为任何搜索请求提供结果,因此我们不能使用 批量索引器 或 Quarkus 中新添加的 管理端点 执行简单的重新索引操作,因为这些操作会在索引之前删除索引中的所有现有文档:搜索操作将无法再匹配它们,直到重新索引完成。

相反,我们可以创建一个具有相同架构的新索引,并将任何写入操作路由到它。

write app

由于 Hibernate Search 尚未提供开箱即用的索引切换功能(尚待),我们需要诉诸于使用低级 API 来访问 Elasticsearch 客户端并自行执行所需的操作。为此,我们需要遵循几个简单的步骤

  1. 使用模式管理器获取我们想要重新索引的索引的映射信息。

    @Inject
    SearchMapping searchMapping; (1)
    // ...
    
    searchMapping.scope(MyIndexedEntity.class).schemaManager() (2)
        .exportExpectedSchema((backendName, indexName, export) -> { (3)
            var createIndexRequestBody = export.extension(ElasticsearchExtension.get())
                    .bodyParts().get(0); (4)
            var mappings = createIndexRequestBody.getAsJsonObject("mappings"); (5)
            var settings =createIndexRequestBody.getAsJsonObject("settings"); (6)
        });
    1. SearchMapping 注入到您应用程序的某个地方,以便我们可以使用它来访问模式管理器。

    2. 获取我们感兴趣的被索引实体(MyIndexedEntity)的模式管理器。如果目标是所有实体,则可以使用 Object.class 来创建范围。

    3. 使用导出模式 API 来访问映射信息。

    4. 使用扩展来访问 Elasticsearch 特定的 .bodyParts() 方法,该方法返回一个 JSON,表示创建索引所需的 JSON HTTP 请求体。

    5. 获取特定索引的映射信息。

    6. 获取特定索引的设置。

  2. 获取 Elasticsearch 客户端的引用,以便我们可以向搜索后端集群执行 API 调用

    @Inject
    SearchMapping searchMapping; (1)
    // ...
    RestClient client = searchMapping.backend() (2)
        .unwrap(ElasticsearchBackend.class) (3)
        .client(RestClient.class); (4)
    1. SearchMapping 注入到您应用程序的某个地方,以便我们可以使用它来访问模式管理器。

    2. 从搜索映射实例访问后端。

    3. 将后端解包到 ElasticsearchBackend,以便我们可以访问后端特定的 API。

    4. 获取 Elasticsearch 的 REST 客户端的引用。

  3. 使用 OpenSearch/Elasticsearch 的切换 API 创建一个新索引,这将使我们能够继续使用现有索引进行读操作,同时将写操作发送到新索引

    @Inject
    SearchMapping searchMapping; (1)
    // ...
    
    SearchIndexedEntity<?> entity = searchMapping.indexedEntity(MyIndexedEntity.class);
    var index = entity.indexManager().unwrap(ElasticsearchIndexManager.class).descriptor(); (2)
    
    var request = new Request("POST", "/" + index.writeName() + "/_rollover"); (3)
    var body = new JsonObject();
    body.add("mappings", mappings);
    body.add("settings", settings);
    body.add("aliases", new JsonObject()); (4)
    request.setEntity(new StringEntity(gson.toJson(body), ContentType.APPLICATION_JSON));
    
    var response = client.performRequest(request); (5)
    //...
    1. SearchMapping 注入到您应用程序的某个地方,以便我们可以使用它来访问模式管理器。

    2. 获取索引描述符以从中获取别名。

    3. 使用索引描述符中的写入索引别名开始构建切换请求体。

    4. 请注意,我们包含了一个空的“aliases”,这样除了写入别名(由于切换请求直接针对它而隐式更新)之外,别名不会被复制到新索引。我们不希望读取别名立即指向新索引。

    5. 使用上一步中获得的 Elasticsearch REST 客户端执行切换 API 请求。

成功完成后,索引将处于我们期望的状态

write app

我们可以开始填充我们的写入索引,而不会影响搜索请求。

完成索引后,我们可以根据结果提交或回滚

after indexing

提交索引切换意味着我们对结果感到满意,并准备将新索引用于读写操作,同时删除旧索引。为此,我们需要向集群发送一个请求

var client = ... (1)

var request = new Request("POST", "_aliases"); (2)
request.setEntity(new StringEntity("""
        {
            "actions": [
                {
                    "add": {  (3)
                        "index": "%s",
                        "alias": "%s",
                        "is_write_index": false
                    },
                    "remove_index": {  (4)
                        "index": "%s"
                    }
                }
            ]
        }
        """.formatted( newIndexName, readAliasName, oldIndexName ) (5)
    , ContentType.APPLICATION_JSON));

var response = client.performRequest(request); (5)
//...
  1. 如上所述,获取 Elasticsearch REST 客户端的访问权限。

  2. 开始创建 _aliases API 请求。

  3. 添加一个操作来更新索引别名,以便将新索引用于读写操作。在这里,我们必须将读取别名指向新索引。

  4. 添加一个操作来删除旧索引。

  5. 新/旧索引的名称可以从初始 _rollover API 请求的响应中检索,而别名可以从索引描述符中检索。

否则,如果我们遇到错误或因其他任何原因决定停止切换,我们可以回滚到使用初始索引

var client = ... (1)

var request = new Request("POST", "_aliases"); (2)
request.setEntity(new StringEntity("""
        {
            "actions": [
                {
                    "add": {  (3)
                        "index": "%s",
                        "alias": "%s",
                        "is_write_index": true
                    },
                    "remove_index": {  (4)
                        "index": "%s"
                    }
                }
            ]
        }
        """.formatted( oldIndexName, writeAliasName, newIndexName ) (5)
    , ContentType.APPLICATION_JSON));

var response = client.performRequest(request); (5)
//...
  1. 如上所述,获取 Elasticsearch REST 客户端的访问权限。

  2. 开始创建 _aliases API 请求。

  3. 添加一个操作来更新索引别名,以便将旧索引用于读写操作。在这里,我们必须将写入别名指向旧索引。

  4. 添加一个操作来删除新索引。

  5. 新/旧索引的名称可以从初始 _rollover API 请求的响应中检索,而别名可以从索引描述符中检索。

请记住,在回滚的情况下,如果写入别名指向新索引时执行了任何写入操作,则您的初始索引可能不同步。

有了这些知识,我们可以将切换过程组织如下

try (Rollover rollover = Rollover.start(searchMapping)) {
    // Perform the indexing operations ...
    rollover.commit();
}

Rollover 类将如下所示

class Rollover implements Closeable {
    public static Rollover start(SearchMapping searchMapping) {
        // initiate the rollover process by sending the _rollover request ...
        // ...
        return new Rollover( client, rolloverResponse );  (1)
    }

    @Override
    public void close() {
        if ( !done ) { (2)
            rollback();
        }
    }

    public void commit() {
        // send the `_aliases` request to switch to the *new* index
        // ...
        done = true;
    }

    public void rollback() {
        // send the `_aliases` request to switch to the *old* index
        // ...
        done = true;
    }
}
  1. 保留 Elasticsearch REST 客户端的引用以执行 API 调用。

  2. 如果我们没有成功提交切换,将在关闭时回滚。

再次,有关此切换实现的完整工作示例,请查看 GitHub 上的 search.quarkus.io

如果您发现此功能有用,并希望将其内置到您的 Hibernate Search 和 Quarkus 应用程序中,请随时在 待处理功能请求 上与我们联系,讨论您的想法和建议。

敬请关注,我们将在未来几周内发布更多博客文章,深入探讨此应用程序的其他有趣实现方面。祝您搜索愉快,切换顺利!