Elasticsearch 核心概念对比:store、docValues、index
概述
1、store:控制列是否单独存储原始值
2、docValue:控制字段是否排序聚合
3、index:控制字段是否建 倒排索引,能否被检索到
--
图解对比
假设有 3 个文档:
Doc1: {shopname: "肯德基", address: "人民广场", cityid: 1}
Doc2: {shopname: "麦当劳", address: "南京路", cityid: 1}
Doc3: {shopname: "星巴克", address: "淮海路", cityid: 2}
_source 字段 (默认开启)
用于返回完整文档:
┌──────────────────────────────────────────┐
│ Doc1 完整 JSON │
│ {"shopname":"肯德基","address":"人民广场","cityid":1} │
├──────────────────────────────────────────┤
│ Doc2 完整 JSON │
│ {"shopname":"麦当劳","address":"南京路","cityid":1} │
├──────────────────────────────────────────┤
│ Doc3 完整 JSON │
│ {"shopname":"星巴克","address":"淮海路","cityid":2} │
└──────────────────────────────────────────┘
特点: 存储完整文档,包含所有字段
index=true (默认):
词项 → 文档ID列表
─────────────────────
肯德基 → [1, 3, 5]
麦当劳 → [2, 4]
人民 → [1, 2]
广场 → [1, 2]
Store 存储结构 (行式):
只存储 store=true 的字段,按 doc 维度组织在一块
Lucene 文件: .fdt (stored fields data)
物理布局:
┌─────────────────────────────────────┐
│ 文档1的所有 store 字段 (连续存储) │
│ shopname: "肯德基" (压缩) │
│ address: "人民广场" (压缩) │
├─────────────────────────────────────┤
│ 文档2的所有 store 字段 (连续存储) │
│ shopname: "麦当劳" (压缩) │
│ address: "南京路" (压缩) │
├─────────────────────────────────────┤
│ 文档3的所有 store 字段 (连续存储) │
│ shopname: "肯德基" (压缩) │
│ address: "淮海路" (压缩) │
└─────────────────────────────────────┘
问题:
1. 需要跳转 3 次 (随机 IO)
2. 读取了不需要的 address 字段
DocValues 存储结构 (列式):
单列完全拎出来
Lucene 文件: .dvd (doc values data)
物理布局:
┌─────────────────────────────────────┐
│ shopname.keyword 列 (所有文档) │
│ [肯德基, 麦当劳, 肯德基, ...] │
│ (连续存储,未压缩或轻量压缩) │
├─────────────────────────────────────┤
│ cityid 列 (所有文档) │
│ [1, 1, 2, 1, 2, ...] │
│ (连续存储,数值编码) │
├─────────────────────────────────────┤
│ address.keyword 列 (所有文档) │
│ [人民广场, 南京路, 淮海路, ...] │
│ (连续存储) │
└─────────────────────────────────────┘
优势:
1. 只需要 1 次顺序 IO
2. 只读取需要的字段
1. store - 字段独立存储
概念:将某个字段的数据单独存储,类似于 MySQL 的列存储,用空间换时间。
eg:查询文档快速返回标题,不需要解析完整文档。
ES 配置示例
{
"properties": {
"title": {
"type": "text",
"store": true // 原始数据会单独存储
}
}
}
MySQL 对比
-- 传统行存储(类似 ES 的 _source)
CREATE TABLE articles (
id INT,
title VARCHAR(100),
content TEXT, -- 假设很大,10KB
author VARCHAR(50)
);
-- 数据在磁盘上按行存储:
-- Row1: [1, "ES教程", "很长的内容...", "张三"]
-- Row2: [2, "MySQL教程", "很长的内容...", "李四"]
-- 如果 MySQL 支持列存储(类似 ES 的 store: true)
-- title 列单独存储:
-- title_store: ["ES教程", "MySQL教程", ...]
-- 主表仍然存储完整行数据
特点
- store: true = 类似 MySQL 表中存储完整的列数据
- store: false(默认)= 数据存在
_source中,类似 MySQL 的行存储
2. docValues - 聚合排序优化
概念:将某个字段单独存储并建立索引,专门用于排序、聚合、脚本计算。
eg:利用 price 的 docValues 快速排序。
ES 配置示例
{
"properties": {
"price": {
"type": "integer",
"doc_values": true // 支持排序、聚合
}
}
}
MySQL 对比
-- MySQL 中类似于列存储引擎或聚合优化
SELECT AVG(price), MAX(price) FROM products; -- 聚合查询
SELECT * FROM products ORDER BY price; -- 排序查询
3. index - 搜索索引
概念:相当于 MySQL 的 B+树索引,控制字段是否可被搜索。
ES 配置示例
{
"properties": {
"email": {
"type": "keyword",
"index": true // 可以被搜索
},
"internal_id": {
"type": "keyword",
"index": false // 不能被搜索,类似不建索引的列
}
}
}
MySQL 对比
-- MySQL 索引
CREATE TABLE users (
id INT,
email VARCHAR(100),
internal_id VARCHAR(50)
);
CREATE INDEX idx_email ON users(email); -- email 可以快速查询
-- internal_id 没有索引,查询会全表扫描
数据存储方式对比
原始数据(行式存储)
Doc1: {"title": "ES教程", "price": 99, "category": "技术"}
Doc2: {"title": "MySQL教程", "price": 199, "category": "技术"}
Doc3: {"title": "小说", "price": 29, "category": "文学"}
列式存储结果
# store: true - 为了快速获取字段值
title_store: ["ES教程", "MySQL教程", "小说"]
# doc_values: true - 为了聚合排序
price_docvalues: [99, 199, 29]
category_docvalues: ["技术", "技术", "文学"]
关键区别:数据组织方式
store 存储(面向查询)
// 按文档组织,便于快速返回
{
"doc1_stored_fields": {"title": "ES教程"},
"doc2_stored_fields": {"title": "MySQL教程"},
"doc3_stored_fields": {"title": "小说"}
}
使用场景:获取搜索结果
GET /books/_search
{
"_source": false,
"stored_fields": ["title"]
}
docValues 存储(面向计算)
# 按列组织,便于批量计算
price_column: [99, 199, 29] # 可以快速计算平均价格
category_column: ["技术", "技术", "文学"] # 可以快速分组统计
# 还会建立排序索引
price_sorted_index: [doc3→29, doc1→99, doc2→199]
实际应用场景
场景1:获取搜索结果的标题
// 使用 store
GET /books/_search
{
"query": {"match": {"content": "elasticsearch"}},
"_source": false,
"stored_fields": ["title"] // 快速返回标题,不需要解析完整文档
}
场景2:按价格排序
// 使用 docValues
GET /books/_search
{
"query": {"match_all": {}},
"sort": [{"price": "asc"}] // 利用 price 的 docValues 快速排序
}
场景3:价格统计分析
// 使用 docValues
GET /books/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": {"field": "price"} // 利用 price 的 docValues 计算统计信息
},
"by_category": {
"terms": {"field": "category"} // 利用 category 的 docValues 分组
}
}
}
存储格式差异
# store 存储格式(类似 JSON)
doc1_store: {"title": "ES教程"}
doc2_store: {"title": "MySQL教程"}
# docValues 存储格式(列式 + 索引)
price_docvalues:
- values: [99, 199, 29] # 原始值数组
- sorted_index: [2,0,1] # 排序索引:29,99,199
- ordinals: [1,2,0] # 序数映射(用于分组)
性能对比
// 查询:获取1000本书的标题
// store 方式:直接读取 title 的独立存储 → 50KB
// _source 方式:读取1000个完整文档后解析 → 10MB
// 聚合:计算各类别的平均价格
// docValues 方式:顺序扫描 category 和 price 列 → 100KB
// _source 方式:读取所有文档,逐个解析字段 → 10MB
组合使用示例
{
"mappings": {
"properties": {
"title": {
"type": "text",
"index": true, // 需要全文搜索
"store": true, // 经常在结果中返回
"doc_values": false // text 类型不支持聚合
},
"price": {
"type": "double",
"index": true, // 需要范围查询
"store": false, // 不常单独返回
"doc_values": true // 需要排序和聚合
},
"description": {
"type": "text",
"index": false, // 不需要搜索
"store": false, // 不需要单独返回
"doc_values": false // 不需要聚合
},
"summary": {
"type": "text",
"index": true, // 需要搜索
"store": true, // 搜索结果中显示摘要
"fields": {
"length": {
"type": "integer",
"store": false, // 长度值不需要单独返回
"doc_values": true // 但需要按长度排序/统计
}
}
}
}
}
}
内存使用差异
# store 只在查询时加载到内存
GET /books/_search → 临时加载需要的 stored_fields
# docValues 可能会被缓存在内存中(用于频繁的聚合排序)
# 但也可以直接从磁盘读取,内存压力更小
总结对比表
| ES 概念 | MySQL 对应 | 主要作用 | 适用场景 |
|---|---|---|---|
store | 单列数据存储 | 单独存储字段原始值 | 频繁返回的字段 |
docValues | 列存储 + 聚合索引 | 支持排序、聚合、脚本 | 需要统计分析的字段 |
index | B+树索引 | 支持搜索查询 | 需要搜索的字段 |
通过合理配置这三个参数,可以在存储空间、查询性能和内存使用之间找到最佳平衡点。
存储结构详解 (物理层面简化表示)
-
_source存储区 (行式存储 - 存原始 JSON) :Doc1: {"id":1,"name":"Running Shoes","price":99.99,"description":"..."} Doc2: {"id":2,"name":"Yoga Mat","price":29.99,"description":"..."} Doc3: {"id":3,"name":"Water Bottle","price":15.99,"description":"..."} -
store=true的独立存储区 (行式存储 - 按文档存储字段) :Doc1-Stored: [id:1, name:"Running Shoes"] Doc2-Stored: [id:2, name:"Yoga Mat"] Doc3-Stored: [id:3, name:"Water Bottle"] -
doc_values存储区 (列式存储 - 按字段存储所有文档值) :price 列 (编码后): Doc1: 99.99 Doc2: 29.99 Doc3: 15.99
index VS store
核心概念对比
| 特性 | index | store |
|---|---|---|
| 作用 | 控制是否建立搜索索引 | 控制是否单独存储原始值 |
| 用途 | 用于查询和过滤 | 用于直接获取字段值 |
| 存储位置 | 倒排索引结构 | 独立的存储区域 |
| 默认值 | true | false |
详细说明
index 参数
{
"mappings": {
"properties": {
"city_id": {
"type": "keyword",
"index": true // 建立倒排索引,可搜索
},
"description": {
"type": "text",
"index": false // 不建立索引,不可搜索
}
}
}
}
index: true 的效果:
- ✅ 可以用于 term/match 查询
- ✅ 可以用于聚合 (aggregations)
- ✅ 可以用于排序 (需要doc_values)
- ✅ 建立倒排索引结构
index: false 的效果:
- ❌ 无法搜索该字段
- ❌ 无法聚合该字段
- ❌ 无法排序该字段
- ✅ 节省索引空间
- ✅ 提高写入性能
store 参数
{
"mappings": {
"properties": {
"title": {
"type": "text",
"store": true // 单独存储原始值
},
"content": {
"type": "text",
"store": false // 不单独存储(默认)
}
}
}
}
store: true 的效果:
- ✅ 原始字段值单独存储
- ✅ 可通过 stored_fields 快速获取
- ✅ 不依赖 _source 字段
- ❌ 增加存储空间
store: false 的效果:
- ✅ 节省存储空间
- ✅ 字段值仍在 _source 中可获取
- ❌ 获取字段需解析整个 _source
存储结构示例
假设有如下文档:
{
"_id": "1",
"title": "北京烤鸭",
"city_id": "110100",
"description": "正宗北京烤鸭,皮脆肉嫩"
}
配置1: 默认配置
{
"title": {"type": "text"}, // index:true, store:false
"city_id": {"type": "keyword"}, // index:true, store:false
"description": {"type": "text"} // index:true, store:false
}
存储结构:
# 倒排索引
title: "北京" -> [doc1], "烤鸭" -> [doc1]
city_id: "110100" -> [doc1]
description: "正宗" -> [doc1], "北京" -> [doc1], ...
# _source 存储
{"title": "北京烤鸭", "city_id": "110100", "description": "正宗北京烤鸭,皮脆肉嫩"}
# stored_fields 存储
(无单独存储)
配置2: 部分字段单独存储
{
"title": {"type": "text", "store": true}, // index:true, store:true
"city_id": {"type": "keyword", "index": false}, // index:false, store:false
"description": {"type": "text", "index": false} // index:false, store:false
}
存储结构:
# 倒排索引
title: "北京" -> [doc1], "烤鸭" -> [doc1]
(city_id 和 description 无索引)
# _source 存储
{"title": "北京烤鸭", "city_id": "110100", "description": "正宗北京烤鸭,皮脆肉嫩"}
# stored_fields 存储
title: "北京烤鸭"
查询行为差异
搜索查询
// ✅ 可以搜索 (index: true)
GET /products/_search
{
"query": {
"term": {"city_id": "110100"}
}
}
// ❌ 无法搜索 (index: false)
GET /products/_search
{
"query": {
"term": {"description": "正宗"} // 报错!
}
}
字段获取
// 方式1: 从 _source 获取 (默认)
GET /products/_search
{
"_source": ["title", "city_id"]
}
// 方式2: 从 stored_fields 获取 (store: true)
GET /products/_search
{
"_source": false,
"stored_fields": ["title"] // 只有 store:true 的字段可用
}
性能对比
查询性能
| 配置 | 查询速度 | 聚合速度 | 写入速度 | 存储空间 |
|---|---|---|---|---|
| index: true | ✅ 极快 (O(1)倒排索引) | ✅ 快 (DocValues) | ❌ 较慢 (需建索引) | ❌ 较大 (索引开销) |
| index: false | ❌ 无法查询 | ❌ 无法聚合 | ✅ 快 (无索引开销) | ✅ 小 (仅存储原始数据) |
字段获取性能
| 配置 | 字段获取 | 部分字段 | 存储空间 |
|---|---|---|---|
| store: true | ✅ 极快 (直接读取) | ✅ 高效 (无需解析_source) | ❌ 大 (数据冗余) |
| store: false | ❌ 较慢 (需解析_source) | ❌ 低效 (仍需解析完整_source) | ✅ 小 (无冗余) |
最佳实践建议
index 使用建议
{
// ✅ 需要搜索的字段
"city_id": {"type": "keyword", "index": true},
"status": {"type": "keyword", "index": true},
// ❌ 不需要搜索的字段
"description": {"type": "text", "index": false},
"internal_notes": {"type": "text", "index": false}
}
store 使用建议
{
// ✅ 大文档中的小字段,经常单独获取
"title": {"type": "text", "store": true},
// ✅ 高频访问的字段
"price": {"type": "float", "store": true},
// ❌ 一般情况下保持默认 (store: false)
"content": {"type": "text", "store": false}
}
典型配置模式
{
"mappings": {
"properties": {
// 搜索字段:建索引,不单独存储
"category": {"type": "keyword", "index": true, "store": false},
// 显示字段:建索引,单独存储
"title": {"type": "text", "index": true, "store": true},
// 存储字段:不建索引,不单独存储
"metadata": {"type": "object", "index": false, "store": false},
// 快速获取:不建索引,单独存储
"thumbnail": {"type": "keyword", "index": false, "store": true}
}
}
}
总结
ES官方里提供的获取字段的几种方式.
-
_source: 需要把doc的整个原始文档解开, 然后取需要的字段.
-
fields: 类似于_source, 只是取出field后会按照mapping来解析格式化.
-
docvalue_fields: 可以用来取支持docvalue的字段(不需要显示指定, 支持的数据类型默认会存docvalue), 避免读取整个_source.
- 支持keyword, 数值类型, date等字段类型, 不支持text.
- 不支持嵌套对象.
-
stored_fields: 可以用来取支持store的字段(需要显式指定store=true), 一般不推荐使用, 比起这个方式更推荐用_source.
- store配置默认为false,只支持显式配置了store=true的字段, 很不方便.
- 不支持嵌套对象
- 可以完全禁用store*fields:
"stored_fields": "_none*", 禁用的话_source也不能访问了, 因为_source本质也是一个store field.