Elasticsearch 系列 · 第五篇

能被搜到的字段,为什么不一定能被统计?

倒排索引(第一篇的话题)解决的是"哪些文档包含这个词"。但聚合要回答的是完全不同的问题:"这个字段的所有取值,分别有多少个文档"——这需要反过来,从文档快速取到字段值,倒排索引在这个方向上天生低效。Elasticsearch 为聚合专门准备了另一套结构:doc values,列式存储,和倒排索引并存,谁也不替代谁。

这篇继续用本机真实运行的 Elasticsearch,把"能搜到"和"能统计"这两件事到底差在哪,用真实报错和真实结果讲清楚。

524.0
真实聚合算出的平均价格((599+449)/2)
doc values
真实报错信息里,Elasticsearch 亲口指出的关键机制

两个方向,两套结构

"词 → 文档"和"文档 → 值"是两个相反的查找方向,各自需要一份为它优化过的物理结构。

倒排索引

词 → 文档列表

回答"包含'apple'这个词的文档有哪些"。这是搜索的方向,第一篇讲过的结构。

doc values

文档 → 字段值(列式存储)

回答"这份文档的 brand 字段是什么"。按列存,同一字段的所有值挨在一起,做统计/排序时不用满索引跳来跳去。

默认都开

大部分字段类型两份都建

keyword、数值、日期等字段默认同时拥有倒排索引(能被精确匹配)和 doc values(能被聚合/排序)——text 字段是唯一的例外,默认只有倒排索引。

行式 vs 列式:为什么方向不同,结构就得不同

倒排索引本质上是"按词分组"——这对统计"某个字段所有文档的取值分布"完全帮不上忙,你得反过来,一份一份翻文档去看它的字段值,这在几十万文档规模下极其低效。

倒排索引的组织方式(按词分组)

apple
doc1, doc2
samsung
doc3

查"哪些文档是 apple"很快;查"doc2 的品牌是什么"得反着扫。

doc1
brand: apple
price: 599
doc2
brand: apple
price: —
doc3
brand: samsung
price: 449

右边就是 doc values 的组织方式——按文档 ID 顺序把每个字段的值挨个排开,做聚合时按顺序扫一遍就能拿到所有值,不需要经过倒排索引那套"词 → 文档"的结构。

真实实验:同一个报错,两种触发方式

上一篇提到 text 字段做聚合会报错。这次反过来:手动关掉一个 keyword 字段的 doc values,看会不会报出同一类错误——如果是,说明真正起作用的是 doc values 这个机制本身,不是"keyword"这个类型名字的魔法。

真实实测 PUT /products2 { "mappings": { "properties": { "brand": { "type": "keyword", "doc_values": false }}}} POST /products2/_search { "aggs": { "by_brand": { "terms": { "field": "brand" }}}} → illegal_argument_exception: Can't load fielddata on [brand] because fielddata is unsupported on fields of type [keyword]. Use doc values instead.

报错措辞不完全一样(keyword 字段直接被禁止退化到 fielddata,连"手动打开"这条路都没有——这一点比 text 字段更严格),但指向的是同一件事:没有 doc values,就没法做聚合,不管字段类型叫 text 还是 keyword。keyword 默认自带 doc values,所以平时感觉不到这层机制的存在;上一篇 text 字段报错,本质也是同一个原因——只是 text 默认没开。

数值字段:天生就有,不用配置

数值、日期这类字段,doc values 默认就是开着的——聚合/排序开箱即用。

真实实测 POST /products/_search { "aggs": { "avg_price": { "avg": { "field": "price" }}, "by_brand": { "terms": { "field": "brand" }, "aggs": { "avg_price": { "avg": { "field": "price" }}}} }} → 整体均价: 524.0 按品牌分组: Apple: 1 件,均价 599.0 Apple Inc.: 1 件,均价 null(没有 price 字段) Samsung: 1 件,均价 449.0

这也是一次真实的嵌套聚合(bucket 聚合里再套 metric 聚合)——先按 brand 分组,组内再单独算均价,这是聚合最常见的用法,doc values 让它可以直接按列扫描完成,不用为每个分组重新翻一遍原始文档。

注意"Apple"和"Apple Inc."被分成了两个桶——这正好是上一篇 keyword 字段"精确匹配、大小写标点都算数"的自然延伸:聚合按的是字段的精确取值分组,写法不统一,统计结果就会被拆散。这也是为什么设计索引时,用于聚合/筛选的字段,数据源头的格式一致性比搜索字段更重要。

参考与说明

  • 本文所有聚合结果、报错信息均为本机真实运行的 Elasticsearch 8.15.0(单节点)真实执行得到,未做任何删改;两次报错分别来自 text 字段默认关闭 fielddata、以及手动对 keyword 字段关闭 doc_values 两种真实触发路径。
  • "行式 vs 列式"一节的示意图是对 doc values 概念的简化图解,用来直观对比两种组织方式的方向差异,不是对 Lucene 实际磁盘格式(基于列式压缩编码,比这里画的连续排列复杂得多)的还原。
  • 没有涉及:doc values 的具体磁盘编码格式(Lucene 的 Numeric/Sorted/SortedSet DocValues 格式)、聚合在多分片场景下"各分片先算再合并"的两阶段过程、cardinality 等近似聚合背后的 HyperLogLog 算法。
☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电