Hibernate Search 中文操作指南

4. Concepts

4.1. Full-text search

全文搜索是一组技术,用于在文本文档语料库中搜索与给定查询最匹配的文档。

与传统搜索(例如在 SQL 数据库中)的主要区别在于,存储的文本不被视为单个文本块,而是标记(单词)的集合。

Hibernate Search 依靠 #coordination-none[或 ]#coordination-none[来实施全文搜索功能。由于 Elasticsearch 在内部使用 Lucene,因此它们有很多特性和一般全文搜索方法是相同的。

简而言之,这些搜索引擎基于倒排索引的概念:一个字典,其中键是文档中找到的标记(单词),值是包含此标记的每个文档的标识符列表。

仍然简化一下,一旦所有文档编入索引,搜索文档就会涉及三个步骤:

  • 从查询中提取标记(单词);

  • 在索引中查找这些标记以查找匹配的文档;

  • 汇总查找结果以生成匹配文档的列表。

Lucene 和 Elasticsearch 不仅限于文本搜索:还支持数字数据,支持对整数、双精度数、长整数、日期等的文本搜索。这些类型使用略微不同的方法进行索引和查询,显然不涉及文本处理。

4.2. Entity types

当涉及到应用程序的领域模型时,Hibernate Search 会区分被视为实体的类型(Java 类)和非实体的类型。

Hibernate Search 中实体类型的定义特征在于它们的实例具有不同的生命周期:实体实例可以保存到数据存储中,或者从数据存储中检索,而无需保存或检索另一种类型的实例。出于此目的,假设每个实体实例都带有不可变的唯一标识符。

这些特性使 Hibernate Search 能够将实体类型 map 到索引,但只能索引实体类型。从实体中引用或包含在其中的“可嵌入”类型,但其生命周期完全绑定到该实体,不能映射到索引。

Hibernate Search 的多个方面涉及实体类型概念:

4.3. Mapping

Hibernate search 针对的应用程序使用基于 entity 的模型来表示数据。在此模型中,每个实体都是单个对象,具有几个原子类型属性(StringIntegerLocalDate、…​)。每个实体可以包含非根聚合(“可嵌入”类型),并且每个实体还可以与一个甚至多个其他实体有多个关联。

相比之下,Lucene 和 Elasticsearch 使用文档。每个文档都是一个“字段”集合,每个字段都被分配一个名称(一个唯一字符串)和一个值(可以是文本,也可以是整数或日期等数字数据)。字段还具有类型,该类型不仅确定值(文本/数字)的类型,更重要的是确定该值将被存储的方式:已编入索引、已存储、含 doc 值等。每个文档都可以包含嵌套聚合(“对象”/“嵌套文档”),但顶级文档之间实际上不能有关联。

这样:

  1. 实体被组织为一个图,其中每个节点是一个实体,每个关联是一条边。

  2. 文档充其量可以组织为一组树,其中每棵树是一个文档,可以选择性地包含嵌套文档。

实体模型和文档模型之间存在多个不匹配:简单属性类型与更复杂的字段类型、关联与无关联、图表与树集合。

mapping 在 Hibernate search 中的目标是通过定义如何将一个或多个实体转换为文档,以及如何将搜索结果解析回原始实体,来解决这些不匹配的情况。这是 Hibernate Search 的主要附加值,也是从 indexing 到各种搜索 DSL 的一切事物的基础。

映射通常使用实体模型中的注释进行配置,但这也可以使用编程 API 来实现。要了解有关如何配置映射的更多信息,请参见 Mapping entities to indexes

要了解如何为生成的文档编制索引,请参见 Indexing entities(提示:对于 Hibernate ORM integration,为 automatic)。

要了解如何使用利用映射来更接近实体模型的 API 进行搜索(特别是将结果作为实体而不是仅仅作为文档标识符进行返回),请参见 Searching

4.4. Binding

尽管 mapping 定义是声明式的,但需要解释这些声明并将其实际应用于领域模型。

这就是 Hibernate Search 所谓的“绑定”:在启动期间,给定的映射指令(例如 @GenericField)将导致实例化并调用“绑定程序”,使它有机会检查其应用于该部分的域模型并“绑定”(分配)一个组件到模型的那一部分(例如“桥”,负责在索引期间从实体中提取数据)。

Hibernate Search 附带绑定程序和桥接程序,用于许多常见用例,还提供了插入自定义绑定程序和桥接程序的能力。

有关更多信息,特别是有关如何插入自定义绑定器和桥接器的信息,请参见 Binding and bridges

4.5. Analysis

Full-text search 中所述,全文引擎处理标记,这意味着必须在索引(文档处理,构建标记 → 文档索引)和搜索(查询处理,生成要查找的标记列表)时处理文本。

但是,处理不仅仅是“标记化”。索引查找是精确查找,这意味着查找 Great (大写)不会返回仅包含 great (全小写)的文档。在处理文本时执行额外的步骤来解决此警告:标记过滤器,这可以规范标记。由于这种“规范化”, Great 将被编入索引为 great ,以便查询的索引查找 great 将按预期匹配。

在 Lucene 世界(Lucene、Elasticsearch、Solr,…​),在索引和搜索阶段期间的文本处理称为“分析”,并由“分析器”执行。

分析器由三类组件组成,这些组件将按照以下顺序连续处理文本:

  • 字符筛选器:转换输入字符。替换、添加或删除字符。

  • 分词器:将文本分割成几个单词,称为“标记”。

  • 标记筛选器:变换标记。在一个标记中替换、添加或移除字符,从现有标记中派生新标记,基于某个条件移除标记,…​

分词器通常在空格处分隔(尽管还有其他选项)。令牌筛选器通常是在其中进行自定义的地方。它们可以删除重音字符,删除无意义的后缀(-ing-s、…​)或令牌(athe、…​),用所选拼写替换令牌(wi-fiwifi),等等。

字符过滤器虽然有用,但很少使用,因为它们不了解标记边界。

除非您知道自己在做什么,否则通常应该支持标记过滤器。

在某些情况下,有必要在不分词的情况下以一个块的方式对文本进行索引:

  1. 对于某些类型的文本,例如 SKU 或其他业务代码,标记化完全没有意义:文本是一个单独的“关键词”。

  2. 对于按字段值排序,标记化是没有必要的。在 Hibernate Search 中也禁止这样做,因为会引发性能问题;仅可以对非标记化字段进行排序。

为了解决这些用例,可以使用一种称为“规范化器”的特殊分析器类型。规范化器只是保证不使用分词器的分析器:它们只能使用字符筛选器和令牌筛选器。

在 Hibernate Search 中,分析器和规范化器由其名称引用,例如 when defining a full-text field。分析器和规范化器具有两个单独的命名空间。

某些名称已分配给内置分析器(特别是在 Elasticsearch 中),但可以(并且建议)为自定义分析器和规范化器分配名称,使用内置组件(分词器、筛选器)组装这些名称以满足您的特定需求。

每个后端都公开其自己的 API 来定义分析器和规范化器,并通常来配置分析。有关更多信息,请参阅每个后端的文档:

4.6. Commit and refresh

为了在索引和搜索时获得最佳吞吐量,Elasticsearch 和 Lucene 在写入和从索引读取时都依赖于“缓冲区”:

  1. 在写入时,更改不会 directly 写入索引,而是一个“索引写入器”,它在内存中或临时文件中缓冲更改。

在写入程序为 committed 时,会将更改“推”到实际索引。在提交发生之前,未提交的更改处于“不安全”状态:如果应用程序崩溃或服务器断电,未提交的更改将丢失。

  1. 在读取时,例如执行搜索查询时,数据不会 directly 从索引中读取,而是从“索引读取器”中读取,该读取器显示索引在过去某个时间点的视图。

在阅读程序为 refreshed 时,视图会更新。在刷新发生之前,搜索查询的结果可能略有过期:自上次刷新以来添加的文档将丢失,自上次刷新以来删除的文档仍会存在,等等。

显然,不安全的更改和不同步索引不可取,但它们是提高性能的权衡之计。

以下不同因素会影响刷新和提交发生的时间:

  1. 默认情况下 Listener-triggered indexingexplicit indexing 将要求在每次更改之后执行一次索引写入器的提交,这意味着在 Hibernate ORM 事务提交返回(对于 Hibernate ORM integration)或 SearchSessionclose() 方法返回(对于 Standalone POJO Mapper)之后,这些更改便是安全的。不过,默认情况下不会请求刷新,这意味着这些更改可能仅在以后才可见,即后端决定刷新索引读取器时。可以通过设置不同的 synchronization strategy 来定制此行为。

  2. mass indexer 在大量索引的最后才会需要进行提交或刷新,以最大化索引吞吐量。

  3. 只要没有特别的提交或刷新要求,将适用后端默认值:

请参阅 here for Elasticsearch

请参阅 here for Lucene

  1. See here for Elasticsearch.

  2. See here for Lucene.

  3. 可以通过 flush() API 显式强制执行提交。

  4. 可以通过 refresh() API 显式强制执行刷新。

即使我们使用“提交”一词,但它与关系数据库事务中的提交概念不同:没有事务且不可能“回滚”。

也没有隔离的概念。在刷新之后,将考虑对索引的所有更改:那些提交给索引的更改,以及仍缓冲在索引写入器中的更改。

出于此原因,提交和刷新可以被视为完全正交的概念:某些设置偶尔会导致提交的更改在搜索查询中不可见,而其他设置则允许即使未提交的更改在搜索查询中可见。

4.7. Sharding and routing

分片将索引数据拆分为多个称为分片的“较小索引”,以提升在处理大量数据时性能。

在 Hibernate Search 中,与 Elasticsearch 类似,另外一个概念与分片密切相关:路由。路由包含将文档标识符(或一般称为“路由密钥”的任意字符串)解析到对应分片。

在索引时:

  1. 文档标识符和可选的路由密钥是从索引实体生成的。

  2. 文档连同它的标识符以及可选的路由密钥将传递到后端。

  3. 后端将文档“路由”到正确的分片,并将路由密钥(如果有的话)添加到文档中的一个特殊字段(以便对其建立索引)。

  4. 文档会在此分片中建立索引。

在搜索时:

  1. 搜索查询可以有选择地传递一个或多个路由密钥。

  2. 如果未传递任何路由密钥,那么该查询将在所有分片上执行。

  3. 如果传递了一个或多个路由键:

后端将这些路由键解析为一组分片,并将仅在所有分片上执行查询,忽略其他分片。

添加一个过滤器到查询中,以便仅匹配用给定的一个路由键建立索引的文档。

  1. 后端将这些路由键解析为一组分片,并将仅在所有分片上执行查询,忽略其他分片。

  2. 添加一个过滤器到查询中,以便仅匹配用给定的一个路由键建立索引的文档。

因此,分片可用于通过以下两种方式提升性能:

  1. 在建立索引时:分片索引可以将“压力”分散到多个分片上,这些分片可以位于不同的磁盘(Lucene)或不同的服务器(Elasticsearch)上。

  2. 在搜索时:如果经常使用一个属性(我们称之为 category )来选择文档的子集,则此属性可以是 defined as a routing key in the mapping ,这样就可以用它来路由文档而不是文档 ID。这样一来, category 值相同的文档将被编入索引到同一个分片。然后在搜索时,如果查询已经过滤了文档,已知命中结果将全部具有相同的 category 值,则可以手动 routed to the shards containing documents with this value 查询并忽略其他分片。

要启用分片,需要一些配置:

  1. 后端需要明确配置:请参见 here for Lucenehere for Elasticsearch

  2. 大多数情况下,文档 ID 用于默认路由文档到分片。这不允许在搜索中利用路由的优势,而搜索需要多个文档共享相同的路由键。在这种情况下,对搜索查询应用路由最多只会返回一个结果。要明确定义要分配给每个文档的路由键,请将 routing bridges 分配给你的实体。====== 分片本质上是静态的:预计每个索引具有相同的碎片,具有相同的标识符,从一个引导到另一个引导。更改分片数或其标识符将需要完全重新索引。