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 的多个方面涉及实体类型概念:
-
每个实体类型都有一个 entity name,区别于类型名称。例如,对于名为 com.acme.Book_的类,实体名称可以是 _Book(默认值),或任何任意选择的字符串。
-
指向实体类型(称为 associations)的属性具有特定的机制;特别是,为了处理 reindexing,Hibernate Search 需要 know about the inverse side of associations。
-
为了在 reindexing时(例如在 indexing plans中)进行更改跟踪,实体类型表示 Hibernate Search 考虑的最小作用域。这意味着代表 Hibernate Search 中“已更改属性”的路径始终以一个实体作为它们的起始点,并且这些路径中的组件永远不会触及到另一个实体(但可能会指向一个实体,当一个 _association_更改时)。
-
Hibernate Search 可能需要其他配置才能启用从外部数据存储加载实体类型,无论是为了 load entities matching a query from an external source还是为了 load all entity instances from an external source for full reindexing。
4.3. Mapping
Hibernate search 针对的应用程序使用基于 entity 的模型来表示数据。在此模型中,每个实体都是单个对象,具有几个原子类型属性(String、Integer、LocalDate、…)。每个实体可以包含非根聚合(“可嵌入”类型),并且每个实体还可以与一个甚至多个其他实体有多个关联。
相比之下,Lucene 和 Elasticsearch 使用文档。每个文档都是一个“字段”集合,每个字段都被分配一个名称(一个唯一字符串)和一个值(可以是文本,也可以是整数或日期等数字数据)。字段还具有类型,该类型不仅确定值(文本/数字)的类型,更重要的是确定该值将被存储的方式:已编入索引、已存储、含 doc 值等。每个文档都可以包含嵌套聚合(“对象”/“嵌套文档”),但顶级文档之间实际上不能有关联。
这样:
-
实体被组织为一个图,其中每个节点是一个实体,每个关联是一条边。
-
文档充其量可以组织为一组树,其中每棵树是一个文档,可以选择性地包含嵌套文档。
实体模型和文档模型之间存在多个不匹配:简单属性类型与更复杂的字段类型、关联与无关联、图表与树集合。
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、…)或令牌(a、 the、…),用所选拼写替换令牌(wi-fi ⇒ wifi),等等。
字符过滤器虽然有用,但很少使用,因为它们不了解标记边界。 |
除非您知道自己在做什么,否则通常应该支持标记过滤器。
在某些情况下,有必要在不分词的情况下以一个块的方式对文本进行索引:
-
对于某些类型的文本,例如 SKU 或其他业务代码,标记化完全没有意义:文本是一个单独的“关键词”。
-
对于按字段值排序,标记化是没有必要的。在 Hibernate Search 中也禁止这样做,因为会引发性能问题;仅可以对非标记化字段进行排序。
为了解决这些用例,可以使用一种称为“规范化器”的特殊分析器类型。规范化器只是保证不使用分词器的分析器:它们只能使用字符筛选器和令牌筛选器。
在 Hibernate Search 中,分析器和规范化器由其名称引用,例如 when defining a full-text field。分析器和规范化器具有两个单独的命名空间。
某些名称已分配给内置分析器(特别是在 Elasticsearch 中),但可以(并且建议)为自定义分析器和规范化器分配名称,使用内置组件(分词器、筛选器)组装这些名称以满足您的特定需求。
每个后端都公开其自己的 API 来定义分析器和规范化器,并通常来配置分析。有关更多信息,请参阅每个后端的文档:
4.6. Commit and refresh
为了在索引和搜索时获得最佳吞吐量,Elasticsearch 和 Lucene 在写入和从索引读取时都依赖于“缓冲区”:
-
在写入时,更改不会 directly 写入索引,而是一个“索引写入器”,它在内存中或临时文件中缓冲更改。
在写入程序为 committed 时,会将更改“推”到实际索引。在提交发生之前,未提交的更改处于“不安全”状态:如果应用程序崩溃或服务器断电,未提交的更改将丢失。
-
在读取时,例如执行搜索查询时,数据不会 directly 从索引中读取,而是从“索引读取器”中读取,该读取器显示索引在过去某个时间点的视图。
在阅读程序为 refreshed 时,视图会更新。在刷新发生之前,搜索查询的结果可能略有过期:自上次刷新以来添加的文档将丢失,自上次刷新以来删除的文档仍会存在,等等。
显然,不安全的更改和不同步索引不可取,但它们是提高性能的权衡之计。
以下不同因素会影响刷新和提交发生的时间:
-
默认情况下 Listener-triggered indexing 和 explicit indexing 将要求在每次更改之后执行一次索引写入器的提交,这意味着在 Hibernate ORM 事务提交返回(对于 Hibernate ORM integration)或 SearchSession 的 close() 方法返回(对于 Standalone POJO Mapper)之后,这些更改便是安全的。不过,默认情况下不会请求刷新,这意味着这些更改可能仅在以后才可见,即后端决定刷新索引读取器时。可以通过设置不同的 synchronization strategy 来定制此行为。
-
mass indexer 在大量索引的最后才会需要进行提交或刷新,以最大化索引吞吐量。
-
只要没有特别的提交或刷新要求,将适用后端默认值:
请参阅 here for Elasticsearch 。
请参阅 here for Lucene 。
-
See here for Lucene.
-
可以通过 flush() API 显式强制执行提交。
-
可以通过 refresh() API 显式强制执行刷新。
即使我们使用“提交”一词,但它与关系数据库事务中的提交概念不同:没有事务且不可能“回滚”。
也没有隔离的概念。在刷新之后,将考虑对索引的所有更改:那些提交给索引的更改,以及仍缓冲在索引写入器中的更改。
出于此原因,提交和刷新可以被视为完全正交的概念:某些设置偶尔会导致提交的更改在搜索查询中不可见,而其他设置则允许即使未提交的更改在搜索查询中可见。
4.7. Sharding and routing
分片将索引数据拆分为多个称为分片的“较小索引”,以提升在处理大量数据时性能。
在 Hibernate Search 中,与 Elasticsearch 类似,另外一个概念与分片密切相关:路由。路由包含将文档标识符(或一般称为“路由密钥”的任意字符串)解析到对应分片。
在索引时:
-
文档标识符和可选的路由密钥是从索引实体生成的。
-
文档连同它的标识符以及可选的路由密钥将传递到后端。
-
后端将文档“路由”到正确的分片,并将路由密钥(如果有的话)添加到文档中的一个特殊字段(以便对其建立索引)。
-
文档会在此分片中建立索引。
在搜索时:
-
搜索查询可以有选择地传递一个或多个路由密钥。
-
如果未传递任何路由密钥,那么该查询将在所有分片上执行。
-
如果传递了一个或多个路由键:
后端将这些路由键解析为一组分片,并将仅在所有分片上执行查询,忽略其他分片。
添加一个过滤器到查询中,以便仅匹配用给定的一个路由键建立索引的文档。
-
后端将这些路由键解析为一组分片,并将仅在所有分片上执行查询,忽略其他分片。
-
添加一个过滤器到查询中,以便仅匹配用给定的一个路由键建立索引的文档。
因此,分片可用于通过以下两种方式提升性能:
-
在建立索引时:分片索引可以将“压力”分散到多个分片上,这些分片可以位于不同的磁盘(Lucene)或不同的服务器(Elasticsearch)上。
-
在搜索时:如果经常使用一个属性(我们称之为 category )来选择文档的子集,则此属性可以是 defined as a routing key in the mapping ,这样就可以用它来路由文档而不是文档 ID。这样一来, category 值相同的文档将被编入索引到同一个分片。然后在搜索时,如果查询已经过滤了文档,已知命中结果将全部具有相同的 category 值,则可以手动 routed to the shards containing documents with this value 查询并忽略其他分片。
要启用分片,需要一些配置:
-
后端需要明确配置:请参见 here for Lucene 和 here for Elasticsearch。
-
大多数情况下,文档 ID 用于默认路由文档到分片。这不允许在搜索中利用路由的优势,而搜索需要多个文档共享相同的路由键。在这种情况下,对搜索查询应用路由最多只会返回一个结果。要明确定义要分配给每个文档的路由键,请将 routing bridges 分配给你的实体。====== 分片本质上是静态的:预计每个索引具有相同的碎片,具有相同的标识符,从一个引导到另一个引导。更改分片数或其标识符将需要完全重新索引。