Elasticsearch 核心概念解析:从倒排索引到字段存储

Elasticsearch 核心概念深度解析:从倒排索引到字段存储

涵盖:倒排索引、正排索引、Doc Values、Fielddata、_sourcestorenull_value


前言

在使用 Elasticsearch 的过程中,很多开发者会遇到一些看似矛盾的设计:为什么 text 字段不能排序?为什么明明存了数据却搜不到 null?_sourcestore 到底什么关系?

本文将通过问答式 的讲解,结合具体数据示例,帮你彻底搞懂 ES 核心存储与索引机制。


一、倒排索引:搜索的基石

1.1 什么是倒排索引?

倒排索引是 ES 搜索快的根本原因。它的本质是:词项 → 文档ID 的映射表

传统方式(正排):文档 → 词

复制代码
文档1: "elasticsearch入门" → 包含词["elasticsearch","入门"]
文档2: "java入门" → 包含词["java","入门"]

要查"入门",需要扫描所有文档 → O(n)

倒排索引方式:词 → 文档

复制代码
"elasticsearch" → [文档1]
"入门" → [文档1, 文档2]
"java" → [文档2]

要查"入门",直接取列表 → O(1)

1.2 text 字段的倒排索引是怎么做的?

关键点:入库时就生成,不是查询时才做!

复制代码
写入文档时(indexing time):
原始文本:"elasticsearch 入门教程"
    ↓ 分词
词项列表:["elasticsearch", "入门", "教程"]
    ↓ 建索引
倒排索引:
"elasticsearch" → [文档1]
"入门" → [文档1]
"教程" → [文档1]

查询时(searching time):
查询词:"入门"
    ↓ 分词
词项:["入门"]
    ↓ 查索引
匹配到文档1 ✅

text vs keyword 倒排索引对比

维度 keyword text
索引时的输入 整个字符串 分词后的词项列表
词项数量 少(≈文档数) 多(文档数×平均词长)
存储位置信息 不需要 需要(支持短语查询)

二、正排索引(Doc Values):排序/聚合的支撑

2.1 什么是正排索引?

正排索引是文档ID → 字段值的映射,用于排序和聚合。

具体长什么样?

数值字段(price):

复制代码
文档ID:    1      2      3      4
price:   [2999,  5999,  3999,  1000]

存储方式:一个连续数组,下标即文档ID,不需要单独存ID列。

字符串字段(status):

复制代码
第1步:建立字典
0 → "published"
1 → "draft"

第2步:按文档ID顺序存编码
文档ID: 1       2       3
编码:  [0,      1,      0]

第3步:字典也存下来

2.2 正排索引怎么和文档ID关联?

数组下标就是文档ID(每个Segment内部独立编号):

复制代码
数组下标:    0      1      2
         ┌──────┬──────┬──────┐
price:   │ 2999 │ 5999 │ 3999 │
         └──────┴──────┴──────┘
           ↑      ↑      ↑
文档ID:     1      2      3

用户的 _id(如 "user_123")通过另一个倒排索引映射成这个下标。

2.3 排序时正排索引会变吗?

不会! 排序只是产生一个排好序的文档ID列表。

复制代码
原始正排索引(不动):
文档ID: 1     2     3     4
price: 2999  5999  3999  1000

排序结果(新列表):
位置:   第1名 第2名 第3名 第4名
文档ID:  4     1     3     2

正排索引从头到尾没变过,只是多了一个排序后的ID数组。

2.4 Doc Values vs Fielddata

维度 Doc Values Fielddata
构建时机 索引时(写入时建好) 查询时(第一次用时建)
存储位置 磁盘 JVM堆内存
内存占用 低(OS管理) 高(容易OOM)
适用场景 生产环境推荐 禁用,仅调试

Fielddata 内存中的样子

复制代码
文档ID 0 → ["elasticsearch", "入门", "教程"]
文档ID 1 → ["java", "入门", "精通"]
文档ID 2 → ["elasticsearch", "高级", "实战"]

2.5 为什么对 text 排序/聚合没有意义?

text 是一个整体一句话,对它排序没有业务含义:

  • 按第一个词的首字母?不直观
  • 按字符串字典序?"入门教程"和"实战"谁大谁小?

正确做法:需要排序/聚合的字段用 keyword,或用 multi-field:

json 复制代码
{
  "title": {
    "type": "text",
    "fields": {
      "raw": { "type": "keyword" }
    }
  }
}
// 搜索用 title,排序用 title.raw

三、_source:原始文档存储

3.1 _source 是什么?

_source 存储你写入的原始 JSON 文档

json 复制代码
PUT my_index/_doc/1
{
  "name": "张三",
  "age": 25
}

// ES 实际存储
{
  "_id": "1",
  "_source": {           // 👈 这就是 _source
    "name": "张三",
    "age": 25
  }
}

3.2 _source 的作用

作用 说明
返回原始文档 查询时,_source 里的内容就是你要的数据
重新索引(Reindex) 从一个索引复制到另一个索引
更新(Update) 部分更新需要读取旧值
高亮(Highlight) 需要从 _source 截取片段

3.3 能禁用 _source 吗?

可以,但后果严重

json 复制代码
PUT my_index
{
  "mappings": {
    "_source": { "enabled": false }
  }
}

禁用后:查询返回空、无法 reindex、无法 update、无法高亮。

唯一禁用的理由 :节省磁盘空间(约15-30%),但生产环境几乎永远不要禁用

3.4 安全的最佳实践:查询时过滤

与其禁用 _source,不如在查询时控制返回内容:

json 复制代码
// 只返回 name 和 age
GET my_index/_search
{
  "_source": ["name", "age"],
  "query": { "match_all": {} }
}

// 排除敏感字段
GET my_index/_search
{
  "_source": {
    "excludes": ["email", "phone"]
  }
}

// 完全不返回 _source
GET my_index/_search
{
  "_source": false
}

四、store:字段级单独存储

4.1 store 是什么?

store 是字段级别的属性,决定该字段是否单独存储一份 ,独立于 _source

json 复制代码
PUT my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "store": true    // 单独存储
      }
    }
  }
}

4.2 store vs _source 的关系

_source store: true 字段
存整个文档 存单个字段
默认开启 默认关闭
查询返回整个文档 查询只返回该字段
占用一份空间 额外占用空间

4.3 什么时候需要 store: true

场景1:禁用了 _source,但又想返回某些字段

json 复制代码
PUT my_index
{
  "mappings": {
    "_source": { "enabled": false },
    "properties": {
      "user_id": { "type": "keyword", "store": true }
    }
  }
}

GET my_index/_search
{
  "stored_fields": ["user_id"]
}

场景2:大文档只取小字段

文档很大,但只需要返回 titleauthor,开启 store 可以避免读取整个 _source

4.4 总结

问题 答案
默认值 false
什么时候用 禁用 _source 时;或大文档只取小字段
怎么查询 stored_fields 参数
最佳实践 99% 场景不需要,默认用 _source 过滤就够了

五、null_value:让 null 可以被搜索

5.1 问题背景

ES 默认行为:字段值为 null 时,不建立索引

json 复制代码
PUT my_index/_doc/1
{ "status": null }   // status 不会被索引
PUT my_index/_doc/2
{ "status": "active" }

倒排索引中只有 "active" → [2],没有文档1。

后果:无法搜索"status 为 null"的文档。

5.2 解决方案:null_value

设置 null_value,把 null 替换成占位值进行索引。

json 复制代码
PUT my_index
{
  "mappings": {
    "properties": {
      "status": {
        "type": "keyword",
        "null_value": "NULL"   // 👈 遇到 null 就存成 "NULL"
      }
    }
  }
}

写入后,倒排索引:

复制代码
"NULL"   → [文档1]
"active" → [文档2]

现在可以搜索了:

json 复制代码
GET my_index/_search
{
  "query": { "term": { "status": "NULL" } }
}
// 返回文档1 ✅

5.3 限制与注意事项

限制 说明
不改变 _source _source 里存的还是 null
类型必须匹配 数值字段用数值占位,不能用字符串
占位值可能冲突 建议用 __NULL__ 等特殊值
替代方案 must_not + exists 查询空值(不需要 null_value)
json 复制代码
// 不设 null_value 也能查空值
GET my_index/_search
{
  "query": {
    "bool": {
      "must_not": [{ "exists": { "field": "status" } }]
    }
  }
}

5.4 使用场景

场景:聚合中包含 null

json 复制代码
PUT orders
{
  "mappings": {
    "properties": {
      "status": {
        "type": "keyword",
        "null_value": "__NULL__"
      }
    }
  }
}

GET orders/_search
{
  "aggs": {
    "by_status": {
      "terms": { "field": "status" }
    }
  }
}
// 返回分组包括:__NULL__、active、pending、completed

六、概念对比总表

概念 存什么 用途 默认 查询方式
倒排索引 词项 → 文档ID 全文搜索 ✅ 自动 match / term
Doc Values 文档ID → 字段值 排序、聚合 keyword/数值 ✅ 自动使用
Fielddata 文档ID → 词项列表 text排序/聚合 ❌ 禁用 需手动开启
_source 原始 JSON 返回文档、reindex ✅ 开启 自动返回
store 单个字段值 绕过 _source 取字段 ❌ 关闭 stored_fields
null_value null 替换成占位值 让 null 可搜索 ❌ 不设置 term 查询

七、生产环境最佳实践

  1. 倒排索引:放心用,ES 自动管理
  2. 排序/聚合:用 keyword 字段,不要对 text 排序
  3. Fielddata永远不要在生产环境开启
  4. _source:保持开启,查询时用过滤控制返回内容
  5. store :99% 场景不需要,禁用 _source 时才考虑
  6. null_value :需要把 null 作为聚合分组时使用;仅查空值用 exists 查询

八、常见问题速查

问题 答案
text 字段为什么不能排序? 对一句话排序没有业务意义
怎么让 text 字段能排序? 用 multi-field 加 keyword 子字段
Fielddata 能开吗? 生产环境不要开,会 OOM
_source 能禁吗? 几乎不要,除非你接受无法 reindex/update
store_source 什么关系? store 是单独存一份,_source 是存整个文档
null 为什么搜不到? 默认不索引 null,用 null_value 解决
怎么查 null 值? must_not + exists 或设置 null_value

本文基于 Elasticsearch 7.x/8.x 版本编写

相关推荐
Elasticsearch2 小时前
Elasticsearch: 快速近似 ES|QL - 第一部分
elasticsearch
隐于花海,等待花开2 小时前
窗口函数之排序函数详细解读及示例
大数据·数据库·hive
武子康2 小时前
大数据-270 Spark MLib-机器学习库快速入门(分类/回归/聚类/推荐)
大数据·后端·spark
数字化顾问2 小时前
(87页PPT)数据战略规划(附下载方式)
大数据·数据仓库·数据挖掘
QYR_Jodie2 小时前
电子设备迭代与新能源扩张驱动,稳增前行:全球散热器2025年31.70亿,2032年锚定54.81亿,2026-2032年CAGR7.7%
大数据·人工智能·市场报告
The Open Group2 小时前
数据资产与TOGAF® |TOGAF如何重构数据治理体系
大数据·人工智能·重构
无忧智库2 小时前
智库级深度复盘:智慧养老解决方案——从“9073”养老格局到“6533”数字生态的重构(PPT)
大数据
AI先驱体验官2 小时前
臻灵:边缘AI与数字人融合,企业级实时互动的技术拐点
android·大数据·人工智能·microsoft·实时互动
X1A0RAN2 小时前
容器化部署elasticsearch教程+python操作es数据库示例
数据库·python·elasticsearch