在这篇文章中,我想从性能的角度探讨ElasticSearch 为我们存储了哪些字段,以及在查询检索时这些字段如何工作。实际上,ElasticSearch和Solr的底层库Lucene提供了两种存储和检索字段的方式:store_fields
和doc_values
。此外,ElasticSearch默认提供了 _source
字段,这是在索引时由文档的所有字段构造的一个大json。
为什么 ElasticSearch使用 _source
字段作为默认值,所有这些可用的字段从性能的角度来看有什么区别?让我们一探究竟!
Lucene中的store_fields和doc_values
当我们在 Lucene 中索引一个文档时,已经被索引的原始字段的信息丢失了。字段根据schema配置进行分词、转换然后索引形成倒排索引。没有任何额外的数据结构,当我们搜索一个文档时,我们得到的是这个文档的 docId 而不是原始字段。为了获得这些原始信息,我们需要额外的数据结构。Lucene为此提供了两种可用的方式:store_fields
和doc_values
。
store_fields
store_fields
的目的是存储字段的原始值(没被分词),以便在查询时检索它们。正如前面所说Lucene的倒排索引查询出来的是一个个docId,为了得到原始值就得把原始值存储起来。
doc_values
引入了doc_values
是为了对排序、聚合、分组等操作进行加速。doc_values
也可用于在查询时返回字段值。唯一的限制是我们不能在text字段上使用doc_values
。
store_fields
和doc_values
是在 Lucene 库中实现的,在 Solr 和 ElasticSearch 中都可以使用。
这里有一篇文章,比较了 Solr 中store_fields
和doc_values
检索性能:
DocValues VS Stored Fields : Apache Solr Features and Performance SmackDown.
可以找到关于store_fields
和doc_values
的更详细的使用方法以及各自局限性。
ElasticSearch中的字段检索
如果我们在映射中明确定义store_fields
和doc_values
,则可以在 elasticsearch 中使用它们:
1 | "properties" : { |
默认情况下,每个字段的
store
都设置为 false。相反,所有支持doc_values
的字段都会默认开启doc_values
。
根据store_fields
和doc_values
的默认配置,在查询时仍然会返回查询命中的文档中的每个字段值。发生这种情况是因为 ElasticSearch 使用另一种工具进行字段检索:Elasticsearch 提供的_source
字段。
ElasticSearch _source字段
_source
字段是在索引时传递给 ElasticSearch 的 json。此字段在 ElasticSearch 中默认设置为 true,可以通过以下方式使用mappings
禁用_source
:
1 | "mappings": { |
有两种方式检索_source
字段的内容:
1、查询时用field
选项可以提取在mappings
中已经定义的字段
1 | POST my-index-000001/_search |
还可以用format
选项对一些特殊的字段进行格式化处理,比如可以将时间戳转成字符串。
这种方式命中的结果也会在的hits
对象下有对应的fields
字段作为响应。
1 | { |
2、通过_source
选项提取原始的文档内容。前面的例子中,查询时_source
都置成了false。
默认情况下_source
为true,也就是默认返回_source
原始内容的所有字段。
也可以指定要在响应中返回的_source
中的一部分字段,这应该是为了提高网络传输的响应速度。
1 | GET /_search |
可以通过适当的配置将_source
的某些字段在索引的时候就排除掉:
1 | PUT logs |
索引时,从_source
中排除字段将减少磁盘空间使用,但被排除的字段将永远不会在响应中返回。
如果禁用 elasticsearch _source
字段,更新文档时需要从头开始重新索引。实际上,为了更新文档,我们需要从旧文档中获取字段的值。从逻辑上讲,使用store_fields和doc_values从旧文档中获取字段的值应该是可行的(这就是 Solr 中原子更新的工作方式)。但是,由于设计决定,这在 ElasticSearch 中是不允许的,如果您需要更新文档,则必须在 elasticsearch 索引配置中启用_source
字段。
检索字段
在 elasticsearch 中,您可以启用或禁用_source
字段并使Stored Field或doc_values
。但是如何在查询时检索字段?
默认情况下,如果启用了_source
,则返回包含整个文档的_source
。您可以避免它并仅返回源的一个子集,如下所示:
1 | POST my-index-000001/_search |
但是,如果您没有启用_source
字段,并且想要从Stored Field和doc_values
返回字段,则必须以另一种方式告诉它给 ElasticSearch。对于您使用的每个源,您必须以不同的方式指定字段列表:
1 | ... |
例如,如果您有一个字段既存储了store_fields
也存储了doc_values
,您可以选择是从store_fields
还是doc_values
中检索它。从功能的角度来看,这完全相同,但您的选择可能会影响查询的执行时间。
store_fields字段, doc_values和ElasticSearch _source内部结构
在本节中,我只想简要概述store_fields
、_source
字段和 doc_values
的内部结构,以便来了解使用这些方法进行字段检索时对性能的期望。
store_fields内部结构
store_fields以行方式放置在磁盘中:对于每个文档,都有一行连续包含所有需要存储的字段。
以上图为例。为了访问文档 x 的 field3,我们必须先访问文档 x 的行起始位置,并跳过存储在 field3 之前的所有字段。跳过字段需要获取其长度。跳过字段虽然不像读取那么繁琐,但此操作并非不耗时。
doc_values内部结构
doc_values以列方式存储。多个文档的同一个字段的值一起连续存储在一起,因为同一个字段的格式基本是一致的,所以可以“几乎”直接访问某个文档的某个字段。计算一个想要的值的地址不是一个简单的操作,它有一个计算成本,但我们可以想象,如果我们只想要一个字段,使用这种访问会更有效率。但是对于磁盘来说,这种随机访问会非常影响性能,所以一般只有在排序和聚合这种需要大批量提取一个字段的情况下会使用doc_values
。
ElasticSearch _source内部结构
_source
呢?好吧,如上所述,_source
是一个包含 json 的大字段,其中包含在索引时提供给 ElasticSearch 的所有输入。但是,这个字段实际上是如何存储的?毫不奇怪,ElasticSearch 利用了一种已经由 Lucene 现成的机制:store_fields
。而且,_source
字段是行中第一个存储的字段。
正因为它是包含整个文档内容的json,所以必须读取整个_source
才能使用它包含的信息。如果我们要返回一个文档的所有字段,这个过程直观上是最快的。另一方面,如果我们只需要返回它包含的信息的一小部分,读取这个巨大的字段可能会浪费计算能力。
性能测试
为了对 3 种类型的字段进行基准测试,我在 ElasticSearch 中创建了 3 个不同的索引。我索引了来自维基百科的 100 万个文档,对于每个文档,我用三种不同的方法索引了 100 个包含 15 个字符的字符串字段:在第一个索引中,我将字段设置为store_fields
,在第二个索引中设置为doc_values
。在这两个索引中,我都禁用了_source
字段。相反,在第三个索引中,我只是启用了_source
字段。
文档和查询集合来自 https://github.com/tantivy-search/search-benchmark-game。 我使用真实的集合来模拟真实的场景。
执行细节:
- CPU: AMD锐龙3600
- RAM: 32 GB
对于每个查询,我请求了最好的 200 个文档,并重复测试——将返回的字段数量(在我创建的 100 个随机字符串字段中)从 1 逐步提升到 100。
这是基准测试的结果:
结果正好显示了我们期望看到的结果。
1、**如果我们需要每个文档的字段很少,建议使用 doc_values
**。
2、当我们想要返回整个文档_source
字段是最好的
3、而store_field
是其他两者之间的完美折中。
在我执行的基准测试场景中,如果我们只需要一个字段,doc_values
的速度几乎是 _source
字段的两倍 ,而在相反的极端情况下,如果我们想返回所有字段,使用_source
字段代替doc_values
,图表显示速度几乎提高了 2倍。
总之,性能不是我们必须考虑的唯一参数。正如我们在这篇文章中简要解释的那样,使用一种或另一种方法存在一些限制。由于您的用例的一些限制,您可能被迫使用这三个中的一个。而且即使从表现来看,我们也没有明显的赢家。
如果磁盘空间不是问题,**甚至可以混合不同的方法并将字段设置为store_field
和doc_values
,并保持开启_source
**。在查询时,elasticsearch 使您可以选择所需的字段列表,以及是否希望从 _source
、_store_field
或 doc_values
返回它们。
当然三个都存储,也会导致索引阶段速度很慢,容易出现EsReject异常。所以软件工程没有银弹。根据场景合适选择吧!
参考:
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html
https://sease.io/2021/02/field-retrieval-performance-in-elasticsearch.html