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
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 算法。
下一篇
写入一致性:主分片和副本分片怎么保证不丢数据
→