Hibernate Search 中文操作指南
14. Indexing entities
14.1. Basics
有多种方法可以在 Hibernate Search 中索引实体。
如果您想了解最流行的方法,请直接跳到以下部分:
-
若要在 Hibernate ORM _Session_中更改实体时透明地保持索引同步,请参阅 listener-triggered indexing。
-
要编制大量数据索引(例如,当向现有应用程序添加 Hibernate Search 时使用整个数据库),请参阅 MassIndexer 。
或者,下表可以帮助您确定最适合您用例的方法。
表 6. 索引方法对比
Name and link |
Use case |
API |
Mapper |
处理应用程序事务中的增量变更 |
无:隐含起作用,无需 API 调用 |
||
MassIndexer |
批量重新编制大量数据 |
Specific to Hibernate Search |
7 或 8 |
9 |
Jakarta EE standard |
14.2. Indexing plans
14.2.1. Basics
对于 listener-triggered indexing和 some forms of explicit indexing,Hibernate Search 依赖于“索引计划”来聚合“实体变更”事件并推断出要执行的最终索引操作。
以下是索引计划在高层次上如何工作的: |
-
当应用程序执行实体更改时,将实体更改事件(已创建、更新、删除的实体)添加到计划。对于 listener-triggered indexing( Hibernate ORM integration仅此),这在执行更改时会隐式发生,但也可以 explicitly完成。
-
最终,应用程序判定变更已完成,然后计划进程处理迄今为止添加的变更事件,推断出哪些实体需要重新编制索引并构建相应文档 ( no coordination ) 或构建要发送至收件箱 ( outbox-polling coordination ) 的事件。对于 Hibernate ORM integration ,当 Hibernate ORM Session 刷新 (明确地或作为事务提交的一部分) 时发生这种情况,而对于 Standalone POJO Mapper ,当 SearchSession 关闭时发生这种情况。
-
最后执行该计划,触发编制索引,可能异步进行。对于 Hibernate ORM integration 而言,这在事务提交时发生,而对于 Standalone POJO Mapper 而言,这在关闭 _SearchSession_时发生。
以下是索引计划的关键特性的摘要,以及它们如何因配置的 coordination strategy而异。
表 7. 根据协调策略比较索引计划
Coordination strategy |
No coordination (default) |
10(仅限 11) |
Guarantee of indexes updates |
Non-transactional, after the database transaction / SearchSession.close() returns |
|
Visibility of index updates |
||
Overhead for application threads |
||
对数据库的开销(仅限 12) |
14.2.2. Synchronization with the indexes
Basics
有关在 Hibernate |
使用 outbox-polling coordination strategy 时,执行索引变更的实际索引计划会在后台线程中异步创建。因此,采用该协调策略时,设置非默认索引计划同步策略没有任何意义,这样做会在启动时导致异常。
当提交事务 ( Hibernate ORM integration) 或关闭 SearchSession( Standalone POJO Mapper) 时, with default coordination settings,即索引计划的执行 ( implicit (listener-triggered)或 explicit) 可能会阻塞应用程序线程,直至索引达到一定完成度。
阻塞线程有两个主要原因:
-
索引数据的安全性:如果在数据库事务完成后,必须将索引数据安全地存储到磁盘中,则需要 index commit 。没有它,索引变更可能在几秒后,当后台发生定期索引提交时,才会安全。
-
实时搜索查询:如果在数据库事务完成后 (对于 Hibernate ORM integration ) 或 SearchSession 's close() 方法返回 (对于 Standalone POJO Mapper ) 时,任何搜索查询必须立即考虑索引变更,则需要 index refresh 。没有它,索引变更可能在几秒后,当后台发生定期索引刷新时,才会可见。
这两个要求受 synchronization strategy 控制。默认策略由配置属性 hibernate.search.indexing.plan.synchronization.strategy 定义。以下是所有可用策略及其保证的参考。
Strategy |
Throughput |
当应用程序线程恢复时的保障 |
已应用的更改(使用或不使用 13) |
更改内容防止崩溃/断电( commit) |
搜索中可见的更改( refresh) |
async |
Best |
No guarantee |
No guarantee |
No guarantee |
write-sync (default) |
Medium |
Guaranteed |
Guaranteed |
No guarantee |
read-sync |
Medium to worst |
Guaranteed |
No guarantee |
Guaranteed |
sync |
Guaranteed |
根据后端及其配置的不同,sync 和 read-sync 策略可能会导致较低的索引吞吐量,因为后端可能不适用于频繁的按需索引刷新。
这就是仅在您确信后端适合进行索引刷新或进行集成测试时,才推荐此策略的原因。特别是, sync 策略将与 Lucene 后端的默认配置配合良好,但与 Elasticsearch 后端的配合效果不佳。
可能会根据所选策略以不同方式报告索引失败: |
无法从实体中提取数据:
无论采用何种策略,都会在应用程序线程中引发异常。
无法应用索引变更(即索引上的 I/O 操作):
对于立即应用变更的策略:在应用程序线程中引发异常。
对于不立即应用变更的策略:将失败转发至 failure handler ,默认情况下,它只会记录失败。
提交索引变更失败:
对于保证索引提交的策略:在应用程序线程中引发异常。
对于不能保证索引提交的策略:将失败转发至 failure handler ,默认情况下,它只会记录失败。
无论采用何种策略,都会在应用程序线程中引发异常。
对于立即应用变更的策略:在应用程序线程中引发异常。
对于不立即应用变更的策略:将失败转发至 failure handler ,默认情况下,它只会记录失败。
对于保证索引提交的策略:在应用程序线程中引发异常。
对于不能保证索引提交的策略:将失败转发至 failure handler ,默认情况下,它只会记录失败。
Per-session override
虽然上面提到的配置属性定义了一个默认值,但可以通过调用 SearchSession#indexingPlanSynchronizationStrategy(…) 并传递不同的策略来覆盖某个特定会话中的此默认值。
内置策略可以通过调用检索:
-
IndexingPlanSynchronizationStrategy.async()
-
IndexingPlanSynchronizationStrategy.writeSync()
-
IndexingPlanSynchronizationStrategy.readSync()
-
or IndexingPlanSynchronizationStrategy.sync()
SearchSession searchSession = /* ... */ (1)
searchSession.indexingPlanSynchronizationStrategy(
IndexingPlanSynchronizationStrategy.sync()
); (2)
entityManager.getTransaction().begin();
try {
Book book = entityManager.find( Book.class, 1 );
book.setTitle( book.getTitle() + " (2nd edition)" ); (3)
entityManager.getTransaction().commit(); (4)
}
catch (RuntimeException e) {
entityManager.getTransaction().rollback();
}
List<Book> result = searchSession.search( Book.class )
.where( f -> f.match().field( "title" ).matching( "2nd edition" ) )
.fetchHits( 20 ); (5)
Custom strategy
您还可以实现自定义策略。然后,可以像内置策略一样设置自定义策略:
-
通过将配置属性 hibernate.search.indexing.plan.synchronization.strategy 设置为指向自定义实现的 bean reference 来设置默认值,例如 class:com.mycompany.MySynchronizationStrategy。
-
通过将自定义实现的实例传递给 SearchSession#indexingPlanSynchronizationStrategy(…) 来在会话级别设置。
14.2.3. Indexing plan filter
以下列出的特性尚处于 incubating 阶段:它们仍在积极开发中。
通常 compatibility policy 不适用:孵化元素(例如类型、方法、配置属性等)的契约在后续版本中可能会以向后不兼容的方式更改,甚至可能被移除。
我们建议您使用孵化特性,以便开发团队可以收集反馈并对其进行改进,但在需要时您应做好更新依赖于这些特性的代码的准备。
在某些情况下,例如在导入大量数据时,按编程方式暂停 explicit and listener-triggered indexing可能会有帮助。Hibernate Search 允许配置应用程序范围和会话级别筛选器,以管理哪些类型被跟踪变更并编入索引。
SearchMapping searchMapping = /* ... */ (1)
searchMapping.indexingPlanFilter( (2)
ctx -> ctx.exclude( EntityA.class ) (3)
.include( EntityExtendsA2.class )
);
SearchSession session = /* ... */ (1)
session.indexingPlanFilter(
ctx -> ctx.exclude( EntityA.class ) (2)
.include( EntityExtendsA2.class )
);
可以通过提供索引和包含的类型以及它们的超类型来定义过滤器。不允许使用接口,并且将接口类传递给任何过滤器定义方法将导致异常。如果使用了由 Map 表示的动态类型,则必须使用它们的名称来配置过滤器。过滤器规则为:
-
如果过滤器明确包含类型 A,则将处理对类型 A 一致的对象的更改。
-
如果过滤器明确排除了类型 A,则将忽略对类型 A 一致的对象的更改。
-
如果过滤器明确包含类型 A,则将处理对类型 B 一致的对象的更改,而类型 B 是类型 A 的子类型,除非过滤器明确排除了类型 B 的更具体的超类型。
-
如果过滤器明确排除了类型 A,则将忽略对类型 B 一致的对象的更改,而类型 B 是类型 A 的子类型,除非过滤器明确包含类型 B 的更具体的超类型。
会话级过滤器优先于应用级过滤器。如果会话级过滤器配置未明确或通过继承包含/排除实体的确切类型,则决策将由应用程序范围的过滤器做出。如果应用程序范围的过滤器对某个类型也没有明确的配置,则认为包含此类型。
在某些情况下,我们可能需要完全禁用索引。逐个列举所有实体可能很繁琐,但由于过滤器配置是隐式应用于子类型的,因此 .exclude(Object.class) 可用于排除所有类型。相反,当应用程序范围的过滤器完全禁用索引时,可以使用 .include(Object.class) 启用会话过滤器中的索引。
SearchSession searchSession = /* ... */ (1)
searchSession.indexingPlanFilter(
ctx -> ctx.exclude( Object.class ) (2)
);
SearchMapping searchMapping = /* ... */ (1)
searchMapping.indexingPlanFilter(
ctx -> ctx.exclude( Object.class ) (2)
);
试图通过同一个过滤器在同一时间将相同类型配置为包含和排除将导致异常的引发。 |
只有在使用 outbox-polling coordination strategy 时才安全使用全应用程序过滤器。在使用此协调策略时,会在与更改它们的会话不同的会话中加载和索引实体。这可能会导致意外的结果,因为处理事件的会话不会应用由修改实体的会话配置的过滤器。如果此类过滤器已配置,除非该过滤器排除了所有类型以防止此协调策略配置的会话级过滤器产生任何意外后果,否则将引发异常。 |
14.3. Implicit, listener-triggered indexing
14.3.1. Basics
此功能仅可通过 Hibernate ORM integration 使用。 |
尤其不能与 Standalone POJO Mapper 一起使用。
默认情况下,每当通过 Hibernate ORM 会话更改实体时,如果 entity type是 mapped to an index,Hibernate Search 会透明地更新相关索引。
以下是监听器触发的索引在高级别的工作方式:
-
当 Hibernate ORM Session 被刷新(明确地或作为事务提交的一部分)时,Hibernate ORM 确定究竟发生了什么更改(创建、更新、删除实体),并将信息转发到 Hibernate Search。
-
Hibernate Search 将此信息添加到(会话范围) indexing plan 中,并且该计划处理到目前为止添加的更改事件,要么推断出哪些实体需要重新索引并构建相应的文档( no coordination ),要么构建事件以发送到出站队列( outbox-polling coordination )。
-
在数据库事务提交时,将执行计划,要么将文档索引/删除请求发送到后端( no coordination ),要么将事件发送到数据库( outbox-polling coordination )。
以下是监听器触发的索引的关键特性的摘要,以及它们如何因配置的 coordination strategy而异。
单击链接了解更多详细信息。
表 8. 根据协调策略比较监听器触发索引
Coordination strategy |
No coordination (default) |
|
检测在 ORM 会话中发生的更改 (session.persist(…), session.delete(…), 赋值器, …) |
检测由 JPQL 或 SQL 查询引起的更改 (insert/update/delete) |
|
关联必须在双方更新 |
||
Changes triggering reindexing |
Guarantee of indexes updates |
|
Non-transactional, after the database transaction / SearchSession.close() returns |
Visibility of index updates |
|
Overhead for application threads |
||
Overhead for the database |
14.3.2. Configuration
如果你的索引是只读的,或者你通过重新索引定期更新索引,则使用 MassIndexer 、 Jakarta Batch mass indexing job 或 explicitly ,则可能不需要侦听器触发的索引。
您可以通过将配置属性 hibernate.search.indexing.listeners.enabled 设置为 false 来禁用监听器触发索引。
由于监听器触发的索引在底层使用 indexing plans,因此影响索引计划的多个配置选项也将影响监听器触发的索引:
14.4. Indexing a large amount of data with the MassIndexer
14.4.1. Basics
在以下情况下, listener-triggered or explicit indexing可能不够用,因为已存在的数据必须编入索引:
-
在还原数据库备份时;
-
索引必须被清除时,例如因为 Hibernate Search mapping 或某些核心设置已更改;
-
由于性能原因,实体在更改时无法被编入索引(如 listener-triggered indexing),而优选定期重新索引(每晚,…)。
为了处理这些情况,Hibernate Search 提供了 MassIndexer:一个基于外部数据存储的内容完全重建索引的工具(对于 Hibernate ORM integration,该数据存储是数据库)。可以告知 _MassIndexer_重新索引一些选定的已编入索引类型或所有类型。
MassIndexer 采用以下方法来实现相当高的吞吐量:
-
当大规模索引开始时,索引将被完全清除。
-
大规模索引通过多个并行线程执行,每个线程都从数据库加载数据并向索引发送索引请求,从而不会触发任何 commit or refresh。
-
大规模索引完成后会执行隐式的 flush(提交)和 refresh,但 Amazon OpenSearch Serverless 除外,因为它不支持明确刷新或刷新。====== 由于初始索引清除,并且大规模索引是一项非常耗费资源的操作,因此建议在 MassIndexer 工作时让你的应用程序脱机。
在 MassIndexer 忙碌时查询索引可能比平时慢,并且可能会返回不完整的结果。
以下代码片段将重建所有已索引实体的索引,删除索引,然后从数据库重新加载所有实体。
SearchSession searchSession = /* ... */ (1)
searchSession.massIndexer() (2)
.startAndWait(); (3)
MassIndexer 会创建其自己的独立会话和(只读)事务,因此无需在 MassIndexer 启动前开始数据库事务或在完成该事务后提交事务。
请注意 MySQL 用户: MassIndexer 使用仅进的可滚动结果来迭代要加载的主键,但 MySQL 的 JDBC 驱动程序会预加载内存中的所有值。
要避免此“优化”,将 idFetchSize parameter 设置为 Integer.MIN_VALUE 。
尽管 MassIndexer 易于使用,但建议进行一些调整以加速该过程。有几个可选参数可用,可以在大规模索引器启动之前按如下所示设置。参见 MassIndexer parameters 以了解所有可用参数的参考,以及 Tuning the MassIndexer for best performance 以了解关键主题的详细信息。
searchSession.massIndexer() (1)
.idFetchSize( 150 ) (2)
.batchSizeToLoadObjects( 25 ) (3)
.threadsToLoadObjects( 12 ) (4)
.startAndWait(); (5)
用多个线程运行 MassIndexer 可能需要与数据库建立许多连接。如果您没有足够大的连接池,则 MassIndexer 本身和/或您的其他应用程序可能会匮乏资源并无法处理其他请求:请确保根据 Threads and connections 中说明调整连接池的大小,以适应大量索引参数。
==== 14.4.2. Selecting types to be indexed
当创建批量索引器时,你可以选择实体类型,以仅重新索引这些类型(以及它们的已索引子类型,如果存在):
searchSession.massIndexer( Book.class ) (1)
.startAndWait(); (2)
==== 14.4.3. Mass indexing multiple tenants
上述部分中的示例从给定的会话创建批量索引器,这将始终将批量索引限制为该会话针对的租户。
当使用 multi-tenancy 时,可以通过从 SearchScope 检索大规模索引器并将租户标识符集合传递来一次重新索引多个租户:
SearchMapping searchMapping = /* ... */ (1)
searchMapping.scope( Object.class ) (2)
.massIndexer( asSet( "tenant1", "tenant2" ) ) (3)
.startAndWait(); (4)
使用 Hibernate ORM mapper,如果 included the comprehensive list of tenants in Hibernate Search’s configuration,则只需调用 _scope.massIndexer()_而不带任何参数,生成的批量索引器会定位所有已配置租户:
SearchMapping searchMapping = /* ... */ (1)
searchMapping.scope( Object.class ) (2)
.massIndexer() (3)
.startAndWait(); (4)
==== 14.4.4. Running the mass indexer asynchronously
由于批量索引器不依赖于原始的 Hibernate ORM 会话,因此可以异步运行批量索引器。在异步使用时,批量索引器将返回完成阶段以跟踪批量索引的完成情况:
searchSession.massIndexer() (1)
.start() (2)
.thenRun( () -> { (3)
log.info( "Mass indexing succeeded!" );
} )
.exceptionally( throwable -> {
log.error( "Mass indexing failed!", throwable );
return null;
} );
// OR
Future<?> future = searchSession.massIndexer()
.start()
.toCompletableFuture(); (4)
==== 14.4.5. Conditional reindexing
此功能仅可通过 Hibernate ORM integration 使用。
尤其不能与 Standalone POJO Mapper 一起使用。
您可以通过将条件作为字符串传递给批量索引器来选择要重新索引的目标实体的子集。在查询数据库以查找要索引的实体时,将应用该条件。
条件字符串应遵循 Hibernate Query Language (HQL) 语法。可访问的实体属性为要重新索引的实体的属性(仅限这些属性)。
SearchSession searchSession = /* ... */ (1)
MassIndexer massIndexer = searchSession.massIndexer(); (2)
massIndexer.type( Book.class ).reindexOnly( "publicationYear < 1950" ); (3)
massIndexer.type( Author.class ).reindexOnly( "birthDate < :cutoff" ) (4)
.param( "cutoff", Year.of( 1950 ).atDay( 1 ) ); (5)
massIndexer.startAndWait(); (6)
即使重新索引应用于实体的子集,默认情况下所有实体也会在开始时被清除。清除 can be disabled completely ,但在启用时无法筛选将被清除的实体。
有关更多信息,参见 HSEARCH-3304 。
==== 14.4.6. MassIndexer parameters
表 9. MassIndexer 参数
Setter |
Default value |
Description |
typesToIndexInParallel(int) |
1 |
并行索引的类型数量。 |
threadsToLoadObjects(int) |
6 |
each type indexed in parallel 的实体加载线程数。换句话说,生成用于实体加载的线程数将是 typesToIndexInParallel * threadsToLoadObjects(+ 每种类型 1 个用于检索要加载的实体的 ID 的线程)。 |
idFetchSize(int) |
100 |
Only supported with the * Hibernate ORM integration.* 加载主键时要使用的获取大小。某些数据库接受特殊的值, 例如, MySQL 可能会受益于使用 Integer#MIN_VALUE, 否则它将尝试在内存中预加载所有内容。 |
batchSizeToLoadObjects(int) |
10 |
Only supported with the * Hibernate ORM integration.* 从数据库加载实体时要使用的获取大小。某些数据库接受特殊的值, 例如, MySQL 可能会受益于使用 Integer#MIN_VALUE, 否则它将尝试在内存中预加载所有内容。 |
dropAndCreateSchemaOnStart(boolean) |
false |
在索引之前删除索引及其模式(如果存在), 并重新创建它们。在删除和重新创建期间, 索引将不可用很短时间, 因此仅应在可接受并发操作失败时( listener-triggered indexing, …)使用此方法。当已知现有模式已过时时应使用此方法, 例如, 当 the Hibernate Search mapping changed 且某些字段现在具有不同类型、不同分析器、新功能(可投影的, …)等时。当模式是最新的时也可以使用这种方法, 因为它可以比在大型索引(尤其是使用 Elasticsearch 后端)上清除(purgeAllOnStart)更快。作为此参数的替代方法, 还可以使用模式管理器在您选择的时间手动管理模式: Manual schema management。 |
purgeAllOnStart(boolean) |
默认值取决于 dropAndCreateSchemaOnStart(boolean)。如果大规模索引器配置为启动时删除并创建架构,则默认为 false,否则默认为 true。 |
索引之前,从索引中删除所有实体。仅在你确定索引已为空时将此项设为 false;否则,你最终会在索引中留下副本。 |
mergeSegmentsAfterPurge(boolean) |
_true_通常, _false_在 Amazon OpenSearch Serverless上 |
强制在初始索引清除之后、索引之前的 each 索引合并为一个片段。如果 purgeAllOnStart 设置为 false,此设置无效。 |
mergeSegmentsOnFinish(boolean) |
false |
在索引之后, 强制将每个索引合并到一个段中。此操作并不总是会提高性能: 请参见 Merging segments and performance。 |
cacheMode(CacheMode) |
CacheMode.IGNORE |
Only supported with the * Hibernate ORM integration.* 加载实体时的 Hibernate CacheMode。默认值为 CacheMode.IGNORE, 在大多数情况下都将是最有效的选择, 但如果许多要编制索引的实体引用一小部分其他实体, 则使用 CacheMode.GET 等其他模式可能会更有效。 |
transactionTimeout |
- |
Only supported in JTA-enabled environments and with the * Hibernate ORM integration.* 超时传送以加载要重新编制索引的 id 和实体。超时时间必须足够长, 以便加载并对一种类型的实体编制索引。请注意, 这些交易是只读的, 因此选择较大的值(如 1800, 即 30 分钟)不会造成任何问题。 |
limitIndexedObjectsTo(long) |
- |
Only supported with the * Hibernate ORM integration.* 每种实体类型加载的最大结果数。此参数允许您定义阈值, 以避免意外加载太多实体。定义的值必须大于 0。默认情况下不使用此参数。它等同于 SQL 中的关键字 LIMIT。 |
monitor(MassIndexingMonitor) |
A logging monitor. |
负责监控大规模索引进度的组件。由于 MassIndexer 完成其工作可能需要一些时间,因此通常需要监控其进度。默认的内置监视器定期以 INFO 级别记录进度,但可以通过实现 MassIndexingMonitor 接口并在使用 monitor 方法传递实例来设置自定义监视器。MassIndexingMonitor 的实现必须是线程安全的。 |
failureHandler(MassIndexingFailureHandler) |
A failure handler. |
负责处理大规模索引期间发生的故障的组件。MassIndexer 并行执行多项操作, 其中某些操作可能会失败, 而不会停止整个大规模索引过程。因此, 可能需要跟踪单个失败。默认的内置故障处理程序只将故障转发到全局 background failure handler, 默认情况下, 它会在 ERROR 级别记录故障, 但可以通过实现 MassIndexingFailureHandler 接口并使用 @`` 方法传递实例来设置自定义处理程序。这可用于简单地将故障记录在特定于大规模索引器的上下文中, 例如请求大规模索引的维护控制台中的 Web 界面, 或者用于更高级的用例, 例如在首次故障时取消大规模索引。MassIndexingFailureHandler 的实现必须是线程安全的。 |
environment(MassIndexingEnvironment) |
一个空环境(没有线程局部变量,…)。 |
This feature is *incubating:仍处于积极开发中。* 后续版本中,孵化元素(例如类型、方法、配置属性等)的协定可能会以向后不兼容的方式改变,甚至删除。负责在大规模索引启动之前在进行大规模索引的线程上设置一个环境(线程局部变量,…),并在进行大规模索引之后拆除该环境的组件。除非在不进行进一步的大规模索引的情况下出现无法复原的情况,否则实现应处理它们的异常:由 MassIndexingEnvironment 引发的任何异常将中止大规模索引。 |
failureFloodingThreshold(long) |
100 采用默认故障处理程序(参见描述) |
This feature is *incubating:仍处于积极开发中。* 每个已编入索引的类型要处理的最大故障数量。任何超过此数量的故障将被忽略,且不会发送到 MassIndexingFailureHandler 进行处理。如果不应忽略任何故障,可以设置为 Long.MAX_VALUE。默认为由正在使用的故障处理程序定义的一个阈值,请参见 MassIndexingFailureHandler#failureFloodingThreshold,FailureHandler#failureFloodingThreshold。对于基于日志的默认故障处理程序,默认阈值为 100。 |
==== 14.4.7. Tuning the MassIndexer for best performance
===== Basics
MassIndexer 被设计为尽快完成重新索引任务,但没有一刀切的解决方案,因此需要进行一些配置以获得最佳效果。
性能优化会变得非常复杂,因此在你尝试配置 MassIndexer 时,请记住以下事项:
-
始终测试你的更改以评估其实际效果:本节中提供的建议总体上是正确的,但每个应用程序和环境都不同,而且一些选项组合在一起可能会产生意外的结果。
-
循序渐进:在为 40 个索引实体类型(每个实体类型有 2 百万个实例)调整大规模索引之前,尝试只使用一个实体类型来尝试更合理的情况,还可以选择限制要编入索引的实体数,以便更快地评估性能。
-
在尝试调节同时索引多个实体类型的批量索引操作之前,单独微调你的实体类型。
===== Threads and connections
增加并行性通常会有帮助,因为瓶颈通常是与数据库/数据存储的连接延迟:值得尝试一个远高于可用实际内核数的线程数。
但是,每个线程需要一个连接(例如,JDBC 连接),而连接通常是有限的。为了安全地增加线程数:
-
需要确保您的数据库/数据存储实际能够处理产生的连接数。
-
连接池应该配置为提供足够数量的连接。
-
上述操作应该考虑您的应用程序的其余部分(Web 应用程序中的请求线程):忽略这一部分可能会在操作正在运行时使其他进程停止。
有一个简单的公式可以了解应用于 MassIndexer 的不同选项如何影响所用工作线程和连接数:
以下是对影响并行性的参数的大致合理的调优起点的几个建议:
typesToIndexInParallel
它可能应该是一个很低的值,例如 1 或 2,具体取决于你的 CPU 有多少个空闲周期,以及数据库往返交互的速度有多慢。
threadsToLoadObjects
更高的值会增加数据库中选择的实体的预加载速率,但也会增加内存使用量和对处理后续索引的线程的压力。请注意,每个线程都会从实体中提取数据以重新索引,这取决于你的映射,可能需要访问惰性关联和加载关联的实体,从而对数据库/数据存储进行阻塞调用,因此可能需要大量线程并行工作。
所有内部线程组都有以“Hibernate Search”为前缀的有意义的名称,因此应该可以使用大多数诊断工具轻松识别它们,包括简单的线程转储。
=== 14.5. Indexing a large amount of data with the Jakarta Batch integration
==== 14.5.1. Basics
此功能仅可通过 Hibernate ORM integration 使用。
尤其不能与 Standalone POJO Mapper 一起使用。
Hibernate Search 提供了一个 Jakarta Batch 作业来执行批量索引。它不仅涵盖了上文描述的现有批量索引器功能,还受益于 Jakarta Batch 的一些强大的标准特性,例如使用检查点进行故障恢复、面向块的处理和并行执行。此批作业接受不同类型的实体作为输入,从数据库加载相关实体,然后从中重建全文索引。
执行此作业需要批处理运行时,而 Hibernate Search 并不提供该运行时。你可以自由选择一个符合你需求的运行时,例如嵌入到 Jakarta EE 容器中的默认批处理运行时。Hibernate Search 提供了对 JBeret 实现的完全集成(请参阅 how to configure it here)。至于其他实现,也可以使用,但需要 a bit more configuration on your side。
如果运行时是 JBeret,则需要添加以下依赖项:
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-mapper-orm-jakarta-batch-jberet</artifactId>
<version>7.2.0.Alpha2</version>
</dependency>
对于任何其他运行时,都需要添加以下依赖项:
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-mapper-orm-jakarta-batch-core</artifactId>
<version>7.2.0.Alpha2</version>
</dependency>
以下是如何运行批处理实例的示例:
Properties jobProps = MassIndexingJob.parameters() (1)
.forEntities( Book.class, Author.class ) (2)
.build();
JobOperator jobOperator = BatchRuntime.getJobOperator(); (3)
long executionId = jobOperator.start( MassIndexingJob.NAME, jobProps ); (4)
==== 14.5.2. Job Parameters
下表包含了所有可以用来自定义大规模索引作业的作业参数。
表 10.Jakarta Batch 集成中的作业参数
==== 14.5.3. Conditional indexing
你可以通过将条件作为字符串传递给大规模索引作业来选择要编制索引的目标实体的子集。该条件将在查询数据库以获取要编制索引的实体时应用。
条件字符串应遵循 Hibernate Query Language (HQL) 语法。可访问的实体属性为要重新索引的实体的属性(仅限这些属性)。
Properties jobProps = MassIndexingJob.parameters() (1)
.forEntities( Author.class ) (2)
.reindexOnly( "birthDate < :cutoff", (3)
Map.of( "cutoff", Year.of( 1950 ).atDay( 1 ) ) ) (4)
.build();
JobOperator jobOperator = BatchRuntime.getJobOperator(); (5)
long executionId = jobOperator.start( MassIndexingJob.NAME, jobProps ); (6)
即使重新索引应用于实体的子集,所有实体默认情况下也会在开始时被清除。清除 can be disabled completely ,但启用时无法筛选将被清除的实体。
有关更多信息,参见 HSEARCH-3304 。
==== 14.5.4. Parallel indexing
为了获得更好的性能,使用多线程以并行的方式执行索引创建。要编制索引的实体集被分割成多个分区。每个线程一次处理一个分区。
以下部分将解释如何调整并行执行。
线程数、获取大小、分区大小等的“黄金分割”高度依赖于你的整体架构、数据库设计甚至数据值。
你应该对这些设置进行尝试,找出针对你特定情况最适合的方法。
===== Threads
作业执行所使用的最大线程数通过方法 maxThreads() 定义。在给定的 N 个线程中,有 1 个线程保留给核心,因此只有 N - 1 个线程可用于不同的分区。如果 N = 1,则程序将正常工作,并且所有批元素都将在同一线程中运行。Hibernate Search 中使用的默认线程数为 10。您可以用您喜欢的数字覆盖它。
MassIndexingJob.parameters()
.maxThreads( 5 )
...
请注意,批处理运行时无法保证提供请求的线程数,它将尽可能使用请求的最大值(Jakarta Batch Specification v2.1 最终版本,第 29 页)。另外请注意,所有批处理作业共享同一个线程池,因此同时执行作业并非总是明智之举。
===== Rows per partition
每个分区都由一定数量的要索引的元素组成。您可以使用 rowsPerPartition 调整一个分区将包含多少个元素。
MassIndexingJob.parameters()
.rowsPerPartition( 5000 )
...
此属性与“块大小”无关,它是每次写入之间处理多少个元素的方式。处理的这一方面由块解决。
相反, rowsPerPartition 更关乎你的批量索引作业的并行程度。
请参见 Chunking section ,了解如何调整块。
当 rowsPerPartition 较低时,将有很多小的分区,因此处理线程不太可能出现饥饿(由于没有更多要处理的分区而保持空闲),但另一方面,您将只能利用较小的获取大小,这将增加数据库访问次数。此外,由于故障恢复机制,在启动新分区时会产生一些开销,因此,如果分区数量过大,这种开销将累积起来。
当 _rowsPerPartition_较高时,将会有几个较大的分区,因此你将能够利用较高的 chunk size,从而获得较高的提取大小,这将减少数据库访问次数,并且启动新分区的开销将不太明显,但另一方面可能无法使用所有可用线程。
每个分区处理一种根实体类型,因此不同实体类型永远不会在同一个分区下运行。
==== 14.5.5. Chunking and session clearing
大规模索引作业支持或多或少从暂停或失败的作业停止的地方重新启动。
通过在几个连续的 chunks 实体中分割每个分区,并在每个块的末尾保存在 checkpoint 中的处理信息,就可以做到这一点。当作业重新启动时,它将从上一个检查点恢复。
每个块的大小由 checkpointInterval 参数决定。
MassIndexingJob.parameters()
.checkpointInterval( 1000 )
...
但块的大小不仅仅是为了保存进度,还与性能有关:
-
为每个区块打开一个新的 Hibernate 会话;
-
为每个区块启动一个新的事务;
-
在一个区块内,定期根据 entityFetchSize 参数清除会话,此参数必须小于(或等于)区块大小;
-
文档在每个区块的末尾冲洗到索引中。
通常,检查点时间间隔应与每个分区中的行数相比很小。
事实上,由于故障恢复机制,每个分区的第一个检查点之前发生错误的元素的处理时间比其他元素都长,因此在一个 1000 个元素的分区中,设置 100 个元素的检查点时间间隔比设置 1000 个元素的检查点时间间隔更快。
另一方面,块在绝对意义上不应太小。执行检查点意味着你的 Jakarta Batch 运行时会将其持久性存储的作业执行进度信息写入到其持久性存储,这也会产生成本。此外,为每个块创建了一个新事务和会话,这不是免费的,这意味着将获取大小设置为高于块大小的值是毫无意义的。最后,在每个块的末尾执行的索引刷新是一个涉及全局锁的昂贵操作,基本而言,执行次数越少,索引速度就越快。因此,设置一个 1 个元素的检查点时间间隔绝对不是一个好主意。
==== 14.5.6. Selecting the persistence unit (EntityManagerFactory)
无论如何检索实体管理器工厂,都必须确保海量索引器使用的实体管理器工厂在整个海量索引过程中保持打开状态。
===== JBeret
如果您的 Jakarta Batch 运行时是 JBeret(特别是在 WildFly 中使用),则可以使用 CDI 检索 EntityManagerFactory。
如果你只使用一个持久性单元,大规模索引器将能够在没有任何特殊配置的情况下自动访问你的数据库。
如果你想使用多个持久性单元,则必须在 CDI 上下文中将 EntityManagerFactories 注册为 bean。请注意,实体管理器工厂可能不会被默认视为 bean,在这种情况下,你必须自己注册它们。你可以使用应用程序范围的 bean 来执行此操作:
@ApplicationScoped
public class EntityManagerFactoriesProducer {
@PersistenceUnit(unitName = "db1")
private EntityManagerFactory db1Factory;
@PersistenceUnit(unitName = "db2")
private EntityManagerFactory db2Factory;
@Produces
@Singleton
@Named("db1") // The name to use when referencing the bean
public EntityManagerFactory createEntityManagerFactoryForDb1() {
return db1Factory;
}
@Produces
@Singleton
@Named("db2") // The name to use when referencing the bean
public EntityManagerFactory createEntityManagerFactoryForDb2() {
return db2Factory;
}
}
一旦实体管理器工厂在 CDI 上下文中注册,你就可以通过使用 entityManagerReference 参数对其进行命名来指示大规模索引器使用特定的实体管理器工厂。
由于 CDI API 的限制,在将海量索引器与 CDI 一起使用时,不能通过持久性单元名称引用实体管理器工厂。
===== Other DI-enabled Jakarta Batch implementations
如果你想使用允许依赖项注入的其他 Jakarta Batch 实现:
-
必须将以下两个范围注解映射到依赖注入机制中的相关范围:org.hibernate.search.jakarta.batch.core.inject.scope.spi.HibernateSearchJobScoped__org.hibernate.search.jakarta.batch.core.inject.scope.spi.HibernateSearchPartitionScoped
-
org.hibernate.search.jakarta.batch.core.inject.scope.spi.HibernateSearchJobScoped
-
org.hibernate.search.jakarta.batch.core.inject.scope.spi.HibernateSearchPartitionScoped
-
必须确保依赖注入机制将在依赖注入上下文中注册来自 hibernate-search-mapper-orm-jakarta-batch-core 模块的所有注入注解类 (@Named,…)。例如,这可以在 Spring DI 中使用 @ComponentScan 注解来实现。
-
必须在依赖注入上下文中注册实现 EntityManagerFactoryRegistry 接口的单个 bean。
===== Plain Java environment (no dependency injection at all)
以下内容仅在你 jakarta Batch 运行时根本不支持依赖注入时才有效,即它忽略批处理工件中的 @Inject 注释。例如,对于 Java SE 模式中的 JBatch 就是这种情况。
如果您只使用一个持久性单元,群体索引器将能够在没有任何特殊配置情况下自动访问数据库:您只需要在启动群体索引器之前确保在应用程序中创建 EntityManagerFactory (或 SessionFactory)。
如果您想使用多个持久性单元,您将必须在启动群体索引器时添加两个参数:
-
entityManagerFactoryReference:这是将标识 EntityManagerFactory 的字符串。
-
entityManagerFactoryNamespace :这允许选择引用 EntityManagerFactory 的方式。可能的值为:
persistence-unit-name (默认):使用 persistence.xml 中定义的持久性单元名称。
session-factory-name:使用 Hibernate 配置中通过 hibernate.session_factory_name 配置属性定义的会话工厂名称。
-
persistence-unit-name (默认):使用 persistence.xml 中定义的持久性单元名称。
-
session-factory-name:使用 Hibernate 配置中通过 hibernate.session_factory_name 配置属性定义的会话工厂名称。====== 如果在 Hibernate 配置中设置属性并且没有使用 JNDI,则还必须将设置为。
14.6. Explicit indexing
14.6.1. Basics
虽然 listener-triggered indexing 和 MassIndexer 或 the mass indexing job 应该能满足大多数需求,但有时需要手动控制索引。
尤其是当 listener-triggered indexing 为 disabled 或根本不受支持(例如, with the Standalone POJO Mapper )时,或者当由侦听器触发的无法检测到实体更改 such as JPQL/SQL insert, update or delete queries 时,需要这样做。
为解决这些用例,Hibernate Search 提供了以下部分中解释的多个 API。
14.6.2. Configuration
由于显式索引在底层使用 indexing plans,因此影响索引计划的多个配置选项也将影响显式索引:
14.6.3. Using a SearchIndexingPlan manually
使用 SearchIndexingPlan 接口通过 SearchSession 的上下文来显式访问 indexing plan 。该接口表示在一个会话上下文中计划的一系列(可变)更改,并且在事务提交(对于 Hibernate ORM integration )或关闭 SearchSession (对于 Standalone POJO Mapper )时应用于索引。
基于 indexing plan 的显式索引的高层工作方式如下:
-
当应用程序需要索引更改时,它将调用当前 SearchSession 的索引计划中的 add / addOrUpdate / delete 方法之一。对于 Hibernate ORM integration ,目前的 SearchSession 是 bound to the Hibernate ORM Session ,而对于 Standalone POJO Mapper , SearchSession 为 is created explicitly by the application 。
-
最终,应用程序决定更改已完成,并且该计划处理迄今为止添加的更改事件,推断出哪些实体需要重新索引并构建对应的文档 ( no coordination ),或构建需要发送到出站收件箱的事件 ( outbox-polling coordination )。应用程序可以使用索引计划的 process 方法明确触发此操作,但通常不必执行此操作,因为它会自动发生:对于 Hibernate ORM integration ,当 Hibernate ORM Session 被刷新(明确或作为事务提交的一部分)时发生这种情况,而对于 Standalone POJO Mapper ,这种情况发生在 SearchSession 被关闭的时候。
-
最后,将执行计划,触发编制索引,这可能发生在异步状态下。应用程序可以通过使用编制索引计划的 execute 方法显式触发此计划,但一般不需要,因为它自动发生:对于 Hibernate ORM integration,这发生在事务提交后,而对于 Standalone POJO Mapper,这发生在 SearchSession 关闭时。
SearchIndexingPlan 界面提供以下方法:
add(Object entity)
(仅适用于 Standalone POJO Mapper 。)
如果实体类型映射到索引,则向索引中添加文档 (@Indexed)。
如果文档已经存在,这可能会在索引中创建重复项。除非你对自己非常确定并且需要(稍有)性能提升,否则优先使用 addOrUpdate 。
addOrUpdate(Object entity)
如果实体类型映射到索引( @Indexed ),则在索引中添加或更新文档,并重新索引嵌入此实体的文档(例如,通过 @IndexedEmbedded )。
delete(Object entity)
如果实体类型映射到索引( @Indexed ),则从索引中删除文档,并重新索引嵌入此实体的文档(例如,通过 @IndexedEmbedded )。
purge(Class<?> entityType, Object id)
从索引中删除实体,但不要尝试重新索引嵌入此实体的文档。
与相比,这主要在以下情况下有用:实体已从数据库中删除,并且即使在分离状态下也不在会话中可用。在这种情况下,重新索引关联实体将成为用户的责任,因为 Hibernate Search 无法知道哪些实体与不再存在的实体关联。
purge(String entityName, Object id)
与 purge(Class<?> entityType, Object id) 相同,但实体类型由其名称引用(请参阅 @javax.persistence.Entity#name )。
process()`
(仅适用于 Hibernate ORM integration 。)
处理迄今为止添加的更改事件,推断出哪些实体需要重新索引并构建对应的文档 ( no coordination ),或构建需要发送到出站收件箱的事件 ( outbox-polling coordination )。
此方法通常会自动执行(请参阅本节开头的概述),因此仅在处理大量项目时批处理时才需要明确调用它,如 Hibernate ORM and the periodic "flush-clear" pattern with SearchIndexingPlan 中所述。
execute()
(仅适用于 Hibernate ORM integration 。)
执行索引计划,触发索引,可能异步。
此方法通常会自动执行(请参阅本节开头的概述),因此仅在非常罕见的情况下才需要明确调用它,即在处理大量项目且事务不是选项时批处理,如 Hibernate ORM and the periodic "flush-clear" pattern with SearchIndexingPlan 中所述。
以下是使用 addOrUpdate 和 delete 的示例。
// Not shown: open a transaction if relevant
SearchSession searchSession = /* ... */ (1)
SearchIndexingPlan indexingPlan = searchSession.indexingPlan(); (2)
Book book = entityManager.getReference( Book.class, 5 ); (3)
indexingPlan.addOrUpdate( book ); (4)
// Not shown: commit the transaction or close the session if relevant
// Not shown: open a transaction if relevant
SearchSession searchSession = /* ... */ (1)
SearchIndexingPlan indexingPlan = searchSession.indexingPlan(); (2)
Book book = entityManager.getReference( Book.class, 5 ); (3)
indexingPlan.delete( book ); (4)
// Not shown: commit the transaction or close the session if relevant
在单个索引计划中可以执行多个操作。甚至可以多次更改同一个实体,例如添加然后移除:Hibernate Search 会按预期简化操作。 |
对于任何合理数量的实体,这都能很好地工作,但在单个会话中更改或简单的加载大量实体需要使用 Hibernate ORM 特别小心,然后使用 Hibernate Search 更加小心。有关更多信息,请参阅 Hibernate ORM and the periodic "flush-clear" pattern with SearchIndexingPlan 。
14.6.4. Hibernate ORM and the periodic "flush-clear" pattern with SearchIndexingPlan
此功能仅可通过 Hibernate ORM integration 使用。 |
尤其不能与 Standalone POJO Mapper 一起使用。
使用 JPA 操作大量数据集时一个相当常见的用例是 periodic "flush-clear" pattern,其中一个循环在每次迭代时读取或写入实体,并刷新然后清除每个 n 迭代的会话。此模式允许在将内存占用保持在合理较低水平的同时处理大量实体。
以下是不使用 Hibernate Search 时保留大量实体的模式示例。
entityManager.getTransaction().begin();
try {
for ( int i = 0; i < NUMBER_OF_BOOKS; ++i ) { (1)
Book book = newBook( i );
entityManager.persist( book ); (2)
if ( ( i + 1 ) % BATCH_SIZE == 0 ) {
entityManager.flush(); (3)
entityManager.clear(); (4)
}
}
entityManager.getTransaction().commit();
}
catch (RuntimeException e) {
entityManager.getTransaction().rollback();
throw e;
}
对于 Hibernate Search 6(与 Hibernate Search 5 及更早版本相反),此模式将按预期工作:
-
with coordination disabled (默认),文档将在冲洗时构建,并在提交事务后发送到索引。
-
with outbox-polling coordination,实体更改事件将在冲洗时被保存,并在提交事务后连同其余更改一起提交。
但是,每个 flush 调用都有可能将数据添加到内部缓冲区,对于大量数据,这可能导致出现 OutOfMemoryException,具体取决于 JVM 堆大小、 coordination strategy以及文档的复杂性和数量。
如果你遇到了内存问题,第一个解决方案是将批处理分解成多个事务,每个事务处理较少数量的元素:内部文档缓冲区将在每次事务后清除。
请参见下面的示例。
使用此模式时,如果某个事务失败,部分数据将已存在于数据库和索引中,且无法回滚更改。
但是,索引将与数据库一致,并且可以从失败的最后一个事务(手动)重新启动进程。
try {
int i = 0;
while ( i < NUMBER_OF_BOOKS ) { (1)
entityManager.getTransaction().begin(); (2)
int end = Math.min( i + BATCH_SIZE, NUMBER_OF_BOOKS ); (3)
for ( ; i < end; ++i ) {
Book book = newBook( i );
entityManager.persist( book ); (4)
}
entityManager.getTransaction().commit(); (5)
}
}
catch (RuntimeException e) {
entityManager.getTransaction().rollback();
throw e;
}
多事务解决方案和最初的 flush() / clear() 循环模式可以合并,将进程分解为多个中等大小的事务,并在每个事务中定期调用 flush / clear 。 |
此组合解决方案是最灵活的,因此如果要微调批处理,它是最合适的。
如果将批处理分解为多个事务不是一种选择,另一个解决方案是在调用 session.flush()/session.clear() 后直接写入索引,而无需等待数据库事务提交:在每次写入索引后,内部文档缓冲区将被清除。
这可以通过调用索引计划中的 execute() 方法来完成,如下例所示。
使用此模式,如果出现异常,部分数据将已在索引中,而无法回滚更改,而数据库更改将已回滚。因此,索引将与数据库不一致。
为从该情况中恢复,您必须手动执行完全相同的数据库更改(使数据库与索引重新同步),或手动执行受事务影响的 reindex the entities (使索引与数据库重新同步)。
当然,如果您能承受较长时间使索引脱机,则一个更简单的解决方案是擦除索引并 reindex everything 。
SearchSession searchSession = Search.session( entityManager ); (1)
SearchIndexingPlan indexingPlan = searchSession.indexingPlan(); (2)
entityManager.getTransaction().begin();
try {
for ( int i = 0; i < NUMBER_OF_BOOKS; ++i ) {
Book book = newBook( i );
entityManager.persist( book ); (3)
if ( ( i + 1 ) % BATCH_SIZE == 0 ) {
entityManager.flush();
entityManager.clear();
indexingPlan.execute(); (4)
}
}
entityManager.getTransaction().commit(); (5)
}
catch (RuntimeException e) {
entityManager.getTransaction().rollback();
throw e;
}