Store vs DocValues vs Index

150 阅读10分钟

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列存储 + 聚合索引支持排序、聚合、脚本需要统计分析的字段
indexB+树索引支持搜索查询需要搜索的字段

通过合理配置这三个参数,可以在存储空间、查询性能和内存使用之间找到最佳平衡点。

存储结构详解 (物理层面简化表示)

  1. _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":"..."}
    
  2. store=true 的独立存储区 (行式存储 - 按文档存储字段) :

    Doc1-Stored: [id:1, name:"Running Shoes"]
    Doc2-Stored: [id:2, name:"Yoga Mat"]
    Doc3-Stored: [id:3, name:"Water Bottle"]
    
  3. doc_values 存储区 (列式存储 - 按字段存储所有文档值) :

    price  (编码后):
      Doc1: 99.99
      Doc2: 29.99
      Doc3: 15.99
    

index VS store

核心概念对比

特性indexstore
作用控制是否建立搜索索引控制是否单独存储原始值
用途用于查询和过滤用于直接获取字段值
存储位置倒排索引结构独立的存储区域
默认值truefalse

详细说明

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官方里提供的获取字段的几种方式.

  1. _source: 需要把doc的整个原始文档解开, 然后取需要的字段.

  2. fields: 类似于_source, 只是取出field后会按照mapping来解析格式化.

  3. docvalue_fields: 可以用来取支持docvalue的字段(不需要显示指定, 支持的数据类型默认会存docvalue), 避免读取整个_source.

    1. 支持keyword, 数值类型, date等字段类型, 不支持text.
    2. 不支持嵌套对象.
  4. stored_fields: 可以用来取支持store的字段(需要显式指定store=true), 一般不推荐使用, 比起这个方式更推荐用_source.

    1. store配置默认为false,只支持显式配置了store=true的字段, 很不方便.
    2. 不支持嵌套对象
    3. 可以完全禁用store*fields: "stored_fields": "_none*", 禁用的话_source也不能访问了, 因为_source本质也是一个store field.