本指南将引导您完成使用 Hibernate Search 索引和查询 Hibernate ORM 实体所需的初始步骤。
如果您的实体未在 Hibernate ORM 中定义,请参阅 本指南。
1. 假设条件
本入门指南旨在通用,不与任何特定框架绑定。如果您使用 Quarkus、WildFly 或 Spring Boot,请务必首先查看 框架支持 以获取提示并了解怪癖。
为简单起见,本指南假设您正在构建一个在单个节点上作为单个实例部署的应用程序。对于更高级的设置,建议您查看 架构示例。
2. 依赖项
可以在 Maven 的 中央存储库 中找到 Hibernate Search 工件。如果您从 Maven 获取 Hibernate Search,建议导入 Hibernate Search BOM,作为依赖关系管理的一部分,以保持其所有工件版本的统一性
<dependencyManagement>
<dependencies>
<!-- Import Hibernate Search BOM to get all of its artifact versions aligned: -->
<dependency>
<groupId>org.hibernate.search</groupId>
<artifactId>hibernate-search-bom</artifactId>
<version>7.2.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Any other dependency management entries -->
</dependencies>
</dependencyManagement>
如果您不想或无法从 Maven 存储库获取 JAR,则可以从 Sourceforge 上托管的分发包 获取。
为了使用 Hibernate Search,您至少需要两个直接依赖项
以下是快速上手最常见的设置和匹配依赖项;阅读 架构 以了解有关此的更多信息。
- Hibernate ORM + Lucene
-
允许多个应用程序节点中 ORM 实体的索引,将索引存储在本地文件系统上。
如果您从 Maven 获取 Hibernate Search,请使用以下依赖项
<dependencies> <!-- Add dependencies without specifying versions as that is already taken care by dependency management: --> <dependency> <groupId>org.hibernate.search</groupId> <artifactId>hibernate-search-mapper-orm</artifactId> </dependency> <dependency> <groupId>org.hibernate.search</groupId> <artifactId>hibernate-search-backend-lucene</artifactId> </dependency> <!-- Any other dependency entries --> </dependencies>
如果您从分发包中获取了 Hibernate Search,请从
dist/engine
、dist/mapper/orm
、dist/backend/lucene
以及它们各自的lib
子目录中复制 JAR。 - Hibernate ORM + Elasticsearch(或 OpenSearch)
-
允许在多个应用程序节点上索引 ORM 实体,并将索引存储在远程 Elasticsearch 或 OpenSearch 集群(需要单独配置)。
如果您从 Maven 获取 Hibernate Search,请使用以下依赖项
<dependencies> <!-- Add dependencies without specifying versions as that is already taken care by dependency management: --> <dependency> <groupId>org.hibernate.search</groupId> <artifactId>hibernate-search-mapper-orm</artifactId> </dependency> <dependency> <groupId>org.hibernate.search</groupId> <artifactId>hibernate-search-backend-elasticsearch</artifactId> </dependency> <!-- Any other dependency entries --> </dependencies>
如果您从分发包中获取了 Hibernate Search,请从
dist/engine
、dist/mapper/orm
、dist/backend/elasticsearch
以及它们各自的lib
子目录中复制 JAR。
3. 配置
将所有必需的依赖项添加到您的应用程序后,就该查看配置文件了。
如果您不熟悉 Hibernate ORM,我们建议您从 此处 开始,在应用程序中实施实体持久性,然后在此基础上添加 Hibernate Search 索引。 |
Hibernate Search 的配置属性源自 Hibernate ORM,因此可以将其添加到 Hibernate ORM 获取配置的任何文件
-
类路径中的
hibernate.properties
文件。 -
使用 Hibernate ORM 原生引导时,类路径中的
hibernate.cfg.xml
文件。 -
使用 Hibernate ORM JPA 引导时,类路径中的
persistence.xml
文件。
Hibernate Search 为所有配置属性提供合理的默认值,但根据您的设置,您可能希望设置以下内容
persistence.xml
中的 Hibernate Search 属性<property name="hibernate.search.backend.directory.root"
value="some/filesystem/path"/> (1)
1 | 设置文件系统中索引的位置。默认情况下,后端会将索引存储在当前工作目录中。 |
persistence.xml
中的 Hibernate Search 属性<property name="hibernate.search.backend.hosts"
value="elasticsearch.mycompany.com"/> (1)
<property name="hibernate.search.backend.protocol"
value="https"/> (2)
<property name="hibernate.search.backend.username"
value="ironman"/> (3)
<property name="hibernate.search.backend.password"
value="j@rV1s"/>
1 | 设置要连接的 Elasticsearch 主机。默认情况下,后端将尝试连接到 localhost:9200 。 |
2 | 设置协议。默认值是 http ,但您可能需要使用 https 。 |
3 | 设置基本 HTTP 认证的用户名和密码。您可能也有兴趣了解 AWS IAM 认证。 |
4. 映射
假设您的应用程序包含 Hibernate ORM 管理的 Book
和 Author
类,并且您想对它们编制索引,以便搜索数据库中包含的书籍。
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
@Entity
public class Book {
@Id
@GeneratedValue
private Integer id;
private String title;
private String isbn;
private int pageCount;
@ManyToMany
private Set<Author> authors = new HashSet<>();
public Book() {
}
// Getters and setters
// ...
}
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
@Entity
public class Author {
@Id
@GeneratedValue
private Integer id;
private String name;
@ManyToMany(mappedBy = "authors")
private Set<Book> books = new HashSet<>();
public Author() {
}
// Getters and setters
// ...
}
下面是一个示例,演示了如何映射上述模型。
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
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;
@Entity
@Indexed (1)
public class Book {
@Id (2)
@GeneratedValue
private Integer id;
@FullTextField (3)
private String title;
@KeywordField (4)
private String isbn;
@GenericField (5)
private int pageCount;
@ManyToMany
@IndexedEmbedded (6)
private Set<Author> authors = new HashSet<>();
public Book() {
}
// Getters and setters
// ...
}
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
@Entity (7)
public class Author {
@Id
@GeneratedValue
private Integer id;
@FullTextField (3)
private String name;
@ManyToMany(mappedBy = "authors")
private Set<Book> books = new HashSet<>();
public Author() {
}
// Getters and setters
// ...
}
1 | @Indexed 将 Book 实体类型标记为已编制索引,即,将为此实体类型创建一个索引,并且该索引将保持最新。 |
2 | 在默认情况下,JPA @Id 用于生成文档标识符。在一些更复杂的场景中,可能需要使用@DocumentId 明确地映射 ID。 |
3 | @FullTextField 将属性映射到一个同名同类型的全文索引字段。全文字段会分解为标记并标准化(小写,…)。在此处,我们依赖默认分析配置,但大多数应用程序需要对其进行自定义;这将在下面进行说明。 |
4 | @KeywordField 将属性映射到一个未经分析的索引字段。例如,可用于标识符。 |
5 | Hibernate Search 不仅用于全文搜索:您可以使用 @GenericField 注释对非 String 类型编制索引,开箱即用支持各种各样的属性类型,例如基本类型(int 、double 、…)及其装箱对应类型(Integer 、Double 、…)、枚举、日期/时间类型、BigInteger /BigDecimal 等。 |
6 | @IndexedEmbedded 会将关联对象的已编制索引表单(实体或可嵌入对象)“嵌入”到嵌入实体的已编制索引表单中。此处, |
7 | Author 在其他实体中是 @IndexedEmbedded ,但本身不需要可搜索,因此它不需要索引,也不需要用 @Indexed 进行注释。 |
这是一个非常简单的示例,但足以入门。只需记住 Hibernate Search 允许进行更复杂的映射
-
存在多个
@*Field
注释,其中一些允许全文搜索,另外一些允许对特定类型的字段进行更细粒度的配置。您可以在 使用@GenericField
、@FullTextField
等将属性映射到索引字段中详细了解@*Field
注释。 -
可以使用“桥接”对属性或甚至类型进行更细粒度的控制。这允许映射开箱即用不支持的类型。有关更多信息,请参阅 绑定和桥接。
5. 初始化
在第一次启动应用程序前,可能需要进行一些初始化
-
需要创建索引及其架构。
-
需要对数据库(如有)中已存在的数据进行索引。
5.1. 架构管理
在开始索引之前,需要创建索引及其架构,可以在磁盘(Lucene)上创建或通过 REST API 调用(Elasticsearch)创建。
幸运的是,默认情况下,Hibernate Search 将负责在第一次启动时创建索引:您不必执行任何操作。
下次启动应用程序时,将重新使用现有索引。
在两次重新启动应用程序之间,如果对映射进行任何更改(添加新字段、更改现有字段的类型,...),则需要更新索引架构。 虽然这需要一些特殊处理,但可以通过删除并重新创建索引轻松解决。有关更多信息,请参阅 更改现有应用程序的映射。 |
5.2. 初始索引
正如我们将在 后面看到的,Hibernate Search 会负责在应用程序中每次实体更改时触发索引。
但是,当您添加 Hibernate Search 集成时,数据库中已存在的数据对于 Hibernate Search 来说是未知的,因此必须通过批处理进行索引。为此,您可以使用大规模索引器 API,如下面的代码所示
SearchSession searchSession = Search.session( entityManager ); (1)
MassIndexer indexer = searchSession.massIndexer( Book.class ) (2)
.threadsToLoadObjects( 7 ); (3)
indexer.startAndWait(); (4)
1 | 通过 EntityManager 从名为 SearchSession 的 Hibernate Search 会话中获取数据。 |
2 | 创建“索引器”,传递您要编制的索引的实体类型。要为所有实体类型编制索引,请在不带任何参数的情况下调用 massIndexer() 。 |
3 | 可以设置要使用的线程数。有关选项的完整列表,请参阅 使用 MassIndexer 重新为海量数据建立索引。 |
4 | 调用批处理索引操作。 |
如果数据库中最初没有数据,则无需进行大规模索引。 |
6. 索引
Hibernate Search 将透明地索引每一个通过 Hibernate ORM 持久化、更新或删除的实体。因此,此代码将透明地填充您的索引
// Not shown: get the entity manager and open a transaction
Author author = new Author();
author.setName( "John Doe" );
Book book = new Book();
book.setTitle( "Refactoring: Improving the Design of Existing Code" );
book.setIsbn( "978-0-58-600835-5" );
book.setPageCount( 200 );
book.getAuthors().add( author );
author.getBooks().add( book );
entityManager.persist( author );
entityManager.persist( book );
// Not shown: commit the transaction and close the entity manager
尤其是使用 Elasticsearch 后端时,默认情况下,在提交事务后立即看不见更改。Elasticsearch 需要稍作延迟(默认一秒)才能处理这些更改。 因此,如果您在一个事务中修改了一些实体,然后在该事务之后立即执行搜索查询,搜索结果可能与您刚刚执行的更改不一致。 请参阅 与索引同步 了解更多有关此行为及其调整方法的信息。 |
7. 搜索
索引数据后,您可以执行搜索查询。
以下代码将准备一个针对 Book
实体索引的搜索查询,过滤结果,以便 title
和 authors.name
中至少有一个字段包含字符串 refactoring
。匹配隐式地出现在词语(“标记”)上,而不是出现在完整字符串中,并且不区分大小写:这是因为目标字段是 全文本 字段,应用了 默认分析器。
// Not shown: get the entity manager and open a transaction
SearchSession searchSession = Search.session( entityManager ); (1)
SearchResult<Book> result = searchSession.search( Book.class ) (2)
.where( f -> f.match() (3)
.fields( "title", "authors.name" )
.matching( "refactoring" ) )
.fetch( 20 ); (4)
long totalHitCount = result.total().hitCount(); (5)
List<Book> hits = result.hits(); (6)
List<Book> hits2 =
/* ... same DSL calls as above... */
.fetchHits( 20 ); (7)
// Not shown: commit the transaction and close the entity manager
1 | 通过 EntityManager 从名为 SearchSession 的 Hibernate Search 会话中获取数据。 |
2 | 对映射到 Book 实体的索引发起搜索查询。 |
3 | 定义仅返回与给定谓词匹配的文档。使用作为 lambda 表达式参数传递的工厂 f 创建谓词。 |
4 | 构建查询并提取结果,限制在最命中项 20 个。 |
5 | 检索匹配实体的总数。 |
6 | 检索匹配实体。 |
7 | 如果您不感兴趣整个结果,而只对命中项感兴趣,您还可以直接调用 fetchHits() 。 |
如果您出于某种原因不希望使用 lambda,可以使用一个替代的基于对象、但会稍微冗余一些的语法
// Not shown: get the entity manager and open a transaction
SearchSession searchSession = Search.session( entityManager ); (1)
SearchScope<Book> scope = searchSession.scope( Book.class ); (2)
SearchResult<Book> result = searchSession.search( scope ) (3)
.where( scope.predicate().match() (4)
.fields( "title", "authors.name" )
.matching( "refactoring" )
.toPredicate() )
.fetch( 20 ); (5)
long totalHitCount = result.total().hitCount(); (6)
List<Book> hits = result.hits(); (7)
List<Book> hits2 =
/* ... same DSL calls as above... */
.fetchHits( 20 ); (8)
// Not shown: commit the transaction and close the entity manager
1 | 通过 EntityManager 从名为 SearchSession 的 Hibernate Search 会话中获取数据。 |
2 | 创建一个“搜索范围”,表示要查询的已编制索引的类型。 |
3 | 对搜索范围发起搜索查询。 |
4 | 定义仅返回与给定谓词匹配的文档。使用与查询相同的搜索范围创建谓词。 |
5 | 构建查询并提取结果,限制在最命中项 20 个。 |
6 | 检索匹配实体的总数。 |
7 | 检索匹配实体。 |
8 | 如果您不感兴趣整个结果,而只对命中项感兴趣,您还可以直接调用 fetchHits() 。 |
可以使用 fetchTotalHitCount()
仅获得命中总数。
// Not shown: get the entity manager and open a transaction
SearchSession searchSession = Search.session( entityManager );
long totalHitCount = searchSession.search( Book.class )
.where( f -> f.match()
.fields( "title", "authors.name" )
.matching( "refactoring" ) )
.fetchTotalHitCount(); (1)
// Not shown: commit the transaction and close the entity manager
1 | 提取命中总数。 |
虽然上述示例将命中项作为受管实体检索,但这只是众多可能的命中项类型之一。请参阅 投影 DSL 了解更多信息。 |
8. 分析
全文搜索允许在不区分大小写的情况下快速匹配单词,这比关系数据库中的子字符串搜索更进一步。但它可以变得更好:如果我们想用“refactored”一词进行搜索以匹配标题包含“refactoring”的书呢?使用自定义分析即可实现。
分析定义了索引文本和搜索词的处理方式。这涉及到分析器,它由三种类型的组件组成,依次应用
-
零个或(很少见)更多的字符过滤器,用于清除输入文本:
A <strong>GREAT</strong> résume
⇒A GREAT résume
。 -
一个标记器,将输入文本拆分为单词,称为“标记”:
A GREAT résume
⇒[A, GREAT, résume]
。 -
零个或更多标记过滤器,用于规范标记并删除无意义的标记。
[A, GREAT, résume]
⇒[great, resume]
。
有一些内置分析器,特别是默认分析器,它将
-
根据Unicode 文本分段算法的单词断字规则对输入进行标记化(拆分);
-
通过将大写字母转换为小写字母来过滤(规范化)标记。
默认分析器非常适合大多数语言,但其并不太高级。为了充分利用分析,您需要通过选择最适合您特定需求的标记器和过滤器来定义一个自定义分析器。
以下段落将说明如何配置和使用一个简单但非常有用的分析器。有关分析及其配置方式的更多信息,请参考分析部分。
每个自定义分析器都需要在 Hibernate 搜索中指定一个名称。这是通过在每个后端定义的分析配置器完成的
-
首先,您需要实现一个分析配置器,一个实现特定于后端的接口的 Java 类:
LuceneAnalysisConfigurer
或ElasticsearchAnalysisConfigurer
。 -
其次,您需要更改后端的配置才能实际使用您的分析配置器。
作为一个示例,让我们假设您的一个已编入索引的Book
实体有标题“重构:改进现有代码的设计”,并且您希望获得以下任何搜索词的命中:“Refactor”、“refactors”、“refactored”和“refactoring”。实现此目标的一种方法是使用具有以下组件的分析器
-
“标准”标记器,它在空格、标点符号和连字符处拆分单词。这是一个通用的标记器。
-
“小写”过滤器,它将每个字符转换为小写。
-
“雪球词干”过滤器,它应用特定于语言的词干提取。
-
最后,一个“ascii 折叠”过滤器,它用其 ASCII 等价物(“e”、“a”等)替换带附加符号的字符(“é”、“à”等)。
下面的示例展示了如何根据所选的后端使用这些组件来定义分析器。
package org.hibernate.search.documentation.mapper.orm.gettingstarted.withhsearch.customanalysis;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurationContext;
import org.hibernate.search.backend.lucene.analysis.LuceneAnalysisConfigurer;
public class MyLuceneAnalysisConfigurer implements LuceneAnalysisConfigurer {
@Override
public void configure(LuceneAnalysisConfigurationContext context) {
context.analyzer( "english" ).custom() (1)
.tokenizer( "standard" ) (2)
.tokenFilter( "lowercase" ) (3)
.tokenFilter( "snowballPorter" ) (3)
.param( "language", "English" ) (4)
.tokenFilter( "asciiFolding" );
context.analyzer( "name" ).custom() (5)
.tokenizer( "standard" )
.tokenFilter( "lowercase" )
.tokenFilter( "asciiFolding" );
}
}
<property name="hibernate.search.backend.analysis.configurer"
value="class:org.hibernate.search.documentation.mapper.orm.gettingstarted.withhsearch.customanalysis.MyLuceneAnalysisConfigurer"/> (6)
1 | 定义一个名为 “english” 的自定义分析器,用于分析英文文本,例如书名。 |
2 | 将分词器设置为标准分词器。您需要传递特定于 Lucene 的名称才能引用分词器;有关可用分词器、它们的名称和参数的信息,请参阅 自定义分析器和标准化器。 |
3 | 设置标记过滤器。按给定的顺序应用标记过滤器。此处也需要特定的于 Lucene 的名称;有关可用标记过滤器、它们的名称和参数的信息,请参阅 使用自定义分析器和标准化器。 |
4 | 设置最近添加的字符过滤器/分词器/标记过滤器的参数值。 |
5 | 定义另一个称为 “name” 的自定义分析器,用于分析作者姓名。与第一个相反,不要启用词干提取(无 snowballPorter 标记过滤器),因为它不太可能对专有名词产生有用的结果。 |
6 | 在 Hibernate Search 配置(此处在 persistence.xml 中)中将配置器分配给后端。有关 bean 引用格式的详细信息,请参阅 解析 bean 引用。 |
package org.hibernate.search.documentation.mapper.orm.gettingstarted.withhsearch.customanalysis;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurationContext;
import org.hibernate.search.backend.elasticsearch.analysis.ElasticsearchAnalysisConfigurer;
public class MyElasticsearchAnalysisConfigurer implements ElasticsearchAnalysisConfigurer {
@Override
public void configure(ElasticsearchAnalysisConfigurationContext context) {
context.analyzer( "english" ).custom() (1)
.tokenizer( "standard" ) (2)
.tokenFilters( "lowercase", "snowball_english", "asciifolding" ); (3)
context.tokenFilter( "snowball_english" ) (4)
.type( "snowball" )
.param( "language", "English" ); (5)
context.analyzer( "name" ).custom() (6)
.tokenizer( "standard" )
.tokenFilters( "lowercase", "asciifolding" );
}
}
<property name="hibernate.search.backend.analysis.configurer"
value="class:org.hibernate.search.documentation.mapper.orm.gettingstarted.withhsearch.customanalysis.MyElasticsearchAnalysisConfigurer"/> (7)
1 | 定义一个名为 “english” 的自定义分析器,用于分析英文文本,例如书名。 |
2 | 将分词器设置为标准分词器。您需要传递特定的于 Elasticsearch 的名称才能引用分词器;有关可用分词器、它们的名称和参数的信息,请参阅 自定义分析器和标准化器。 |
3 | 设置标记过滤器。按给定的顺序应用标记过滤器。此处也需要特定的于 Elasticsearch 的名称;有关可用标记过滤器、它们的名称和参数的信息,请参阅 使用自定义分析器和标准化器。 |
4 | 请注意,对于 Elasticsearch,任何参数化的字符过滤器、分词器或标记过滤器都必须单独定义并分配一个新名称。 |
5 | 设置所定义的字符过滤器/分词器/标记过滤器的参数值。 |
6 | 定义另一个名为 “name” 的自定义分析器,用于分析作者姓名。与第一个相反,不要启用词干提取(无 snowball_english 标记过滤器),因为它不太可能对专有名词产生有用的结果。 |
7 | 在 Hibernate Search 配置(此处在 persistence.xml 中)中将配置器分配给后端。有关 bean 引用格式的详细信息,请参阅 解析 bean 引用。 |
配置分析后,必须调整映射,以便将相关的分析器分配给每个字段。
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
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;
@Entity
@Indexed
public class Book {
@Id
@GeneratedValue
private Integer id;
@FullTextField(analyzer = "english") (1)
private String title;
@KeywordField
private String isbn;
@GenericField
private int pageCount;
@ManyToMany
@IndexedEmbedded
private Set<Author> authors = new HashSet<>();
public Book() {
}
// Getters and setters
// ...
}
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
@Entity
public class Author {
@Id
@GeneratedValue
private Integer id;
@FullTextField(analyzer = "name") (1)
private String name;
@ManyToMany(mappedBy = "authors")
private Set<Book> books = new HashSet<>();
public Author() {
}
// Getters and setters
// ...
}
1 | 用 @FullTextField 替换 @GenericField 注解,并将 analyzer 参数设置为之前配置的自定义分析器的名称。 |
就是这样!现在,一旦实体重新索引,你就可以搜索术语“重构”、“重构”、“重构的”或“重构”,并且名为“重构:改进现有代码的设计”的书籍将会显示在结果中。
// Not shown: get the entity manager and open a transaction
SearchSession searchSession = Search.session( entityManager );
SearchResult<Book> result = searchSession.search( Book.class )
.where( f -> f.match()
.fields( "title", "authors.name" )
.matching( "refactored" ) )
.fetch( 20 );
// Not shown: commit the transaction and close the entity manager
9. 接下来
上述段落概述了 Hibernate Search。
如果参考文档还不够,你可以在延伸阅读部分中找到指向 Hibernate Search 外部资源的指针以及相关软件,包括使用 Hibernate Search 的应用程序示例。