一、正排索引基础概念
在 Elasticsearch 中,正排索引用于存储完整的文档内容,以便通过文档ID 快速定位文档的字段值。正排索引通过 Doc Values 和 Store Fields 两种形式,为聚合、排序、脚本计算等场景提供高效支持。Doc Values 的列式存储设计显著优化了分析性能,而 Store Fields 提供了灵活的直接字段访问能力。
与倒排索引的对比:
- 倒排索引:词项 → 文档列表(用于搜索)。
- 正排索引:文档ID → 字段内容(用于聚合、排序、返回原始数据)。
二、正排索引基本结构
Elasticsearch 中的正排索引(正向索引)主要通过两种机制实现:Doc Values 和 Stored Fields。以下面两个文档为例:
- 文档内容
-
文档id为 1:
bash{ "title": "Elasticsearch Guide", "author": "John Doe", "year": 2023, "tags": ["search", "database"] }
-
文档id为 2:
bash{ "title": "Introduction to Elasticsearch", "author": "Jane Smith", "year": 2022, "tags": ["tutorial", "search"] }
-
-
Doc Values 结构(列式存储)
Doc Values 是 Elasticsearch 默认的正排索引实现方式,采用列式存储结构。
-
设计目标:支持高效的列式存储(Column-oriented),用于聚合(Aggregations)、排序(Sorting)、脚本计算等。
-
核心特点:
- 列式存储:按字段垂直存储,而非按文档水平存储。
- 默认启用:所有不支持text类型的字段默认开启。
- 磁盘存储:存储在磁盘,但会被OS缓存到文件系统缓存。
-
适用场景:
- 数值、日期、关键字(Keyword)等非文本字段。
- 默认启用(可通过 mapping 的 doc_values: false 关闭)。
-
存储结构:
- 每个字段单独存储为一列,所有文档的该字段值按DocID顺序排列。
- 列式存储优势:
- 高效聚合:列式存储适合聚合计算。
- 内存友好:可以只加载需要的列。
- 压缩存储:使用多种压缩技术减少空间占用。
- 缓存友好:CPU缓存命中率高。
文档 ID(DocID) 字段名 1 value1 2 value2 以year字段为例,Doc Values 结构如下:
文档 ID(DocID) year 1 2023 2 2022
-
-
Stored Fields 结构(行式存储)
Stored Fields 存储原始文档的完整字段值,用于_source和显式标记为store的字段。
-
设计目标:存储字段的原始值(如文本内容),用于直接返回特定字段(而非整个 _source)。
-
核心特点:
- 行式存储:按文档存储完整数据。
- 按需启用:需要通过"store": true显式配置。
- 原始格式:保留字段原始值。
-
适用场景:
- 需要频繁返回少量字段(避免解析整个 _source)。
- 默认不启用(需在 mapping 中显式设置 "store": true)。
-
存储结构:
- 按字段存储原始值,类似传统数据库的行存储。
- 通过 stored_fields 参数指定需要返回的字段。
文档 ID(DocID) 字段名 字段值 1 title "Elasticsearch Guide" 1 author "John Doe" 1 year 2023 1 tags ["search", "database"] 2 title "Introduction to Elasticsearch" 2 author "Jane Smith" 2 year 2022 2 tags ["tutorial", "search"]
-
-
正排索引的 JSON 表示
bash{ "documents": [ { "doc_id": 1, "fields": { "title": "Elasticsearch Guide", "author": "John Doe", "year": 2023, "tags": ["search", "database"] } }, { "doc_id": 2, "fields": { "title": "Introduction to Elasticsearch", "author": "Jane Smith", "year": 2022, "tags": ["tutorial", "search"] } } ], "doc_values": { "year": [ {"doc_id": 1, "value": 2023}, {"doc_id": 2, "value": 2022} ], "tags": [ {"doc_id": 1, "value": "search"}, {"doc_id": 1, "value": "database"}, {"doc_id": 2, "value": "tutorial"}, {"doc_id": 2, "value": "search"} ] } }
三、正排索引的构建过程
Elasticsearch 的正排索引主要通过 Doc Values 和 Stored Fields 两种机制实现,它们的构建过程有所不同。以下是完整的构建流程:
-
Doc Values 构建过程
-
阶段一:内存缓冲
-
文档解析:
- 根据字段映射定义解析文档各字段值。
- 对非text类型字段自动准备构建Doc Values。
-
内存缓冲区:
bash// 伪数据结构示例 Map<FieldName, List<DocValueEntry>> buffer = { "price": [(doc1, 100), (doc2, 200), ...], "city": [(doc1, "北京"), (doc2, "上海"), ...] }
-
-
阶段二:列式存储转换
-
字典编码:
-
对字符串等离散值创建唯一值字典。
bash// city字段示例 Dictionary: ["北京", "上海", "广州"]
-
-
值映射:
-
将原始值替换为字典序数。
bash// 原始值 → 编码值 "北京" → 0 "上海" → 1 "北京" → 0 "广州" → 2
-
-
列式存储:
-
按字段组织数据,与文档分离。
bashprice列: [100, 200, 150, ...] city列: [0, 1, 0, 2, ...] // 使用字典编码后的值
-
-
-
阶段三:磁盘持久化
- 文件生成:
- 生成.dvd(数据值)和.dvm(元数据)文件。
- 使用紧凑的二进制格式。
- 压缩处理:
- 数值类型:增量编码 + 位压缩
- 字符串类型:前缀压缩
- 索引构建:
- 创建字段值到文档的快速访问索引。
- 对排序字段构建B-tree类结构加速范围查询。
- 文件生成:
-
-
Stored Fields 构建过程
-
阶段一:原始文档处理
-
字段筛选:
- 包含_source字段
- 包含显式设置"store": true的字段
-
内存缓冲:
bash// 伪数据结构示例 List<StoredDocument> buffer = [ {doc1, {"title": "ES指南", "content": "..."}}, {doc2, {"title": "大数据", "content": "..."}} ]
-
-
阶段二:文档块组织
- 分块处理:
- 将多个文档打包为一个块(通常4-32KB)
- 块内文档连续存储
- 压缩处理:
- 使用LZ4算法压缩每个块
- 字段级压缩优化
- 分块处理:
-
阶段三:磁盘存储
- 文件生成:
- 生成.fdt(字段数据)和.fdm(字段元数据)文件
- 存储文档原始JSON结构
- 指针构建:
-
创建文档ID到磁盘位置的映射表
bash// 伪数据结构 Map<DocID, (fileOffset, compressedSize)> = { 1 → (0x1000, 1024), 2 → (0x1400, 768) }
-
- 文件生成:
-
-
构建过程关键优化
- 内存控制
- 缓冲限制:
- 默认使用JVM堆外内存
- 通过indices.memory.index_buffer_size配置(默认10%)
- 分段策略:
- 内存缓冲满后生成新的段(segment)
- 每个段包含独立的Doc Values
- 缓冲限制:
- 并行构建
- 多线程处理:
- 不同字段的Doc Values并行构建
- 大型字段使用单独线程
- 异步持久化:
- 内存数据异步刷盘
- 通过refresh_interval控制(默认1秒)
- 多线程处理:
- 内存控制
-
构建过程示例
假设索引以下文档:
bash[ {"id":1, "title":"ES基础", "price":100, "city":"北京"}, {"id":2, "title":"高级教程", "price":200, "city":"上海"} ]
-
Doc Values构建结果:
bashprice字段: - 列数据: [100, 200] - 字典: 无(数值类型直接存储) city字段: - 字典: ["北京", "上海"] - 列数据: [0, 1] (字典序数)
-
Stored Fields构建结果:
bashSegment文件: - 文档1原始JSON + 文档2原始JSON - 压缩存储为连续数据块
-
四、正排索引的优势
正排索引作为Elasticsearch的关键组成部分,在特定场景下展现出显著优势,与倒排索引形成互补。以下是其主要优势的详细分析:
- 列式存储带来的性能优势
- 高效聚合计算
- 相同字段的值连续存储,减少磁盘I/O
- 直接对整列数据进行统计运算(如sum/avg/max/min)
- 示例:计算1亿条销售记录的总金额,只需顺序读取price列
- 更好的压缩率
- 同列数据相似度高,压缩率可达60-70%
- 支持多种压缩算法:LZ4、DEFLATE等
- 显著减少磁盘占用和内存压力
- CPU缓存友好
- 现代CPU的缓存预取机制能更好预测列式数据访问模式
- 相比行式存储,缓存命中率提升3-5倍
- 高效聚合计算
- 特定操作性能优势
- 排序(Sorting)效率极高
- 直接访问有序存储的列数据
- 避免倒排索引需要"收集-排序"的两阶段操作
- 测试显示比基于fielddata的排序快2-3倍
- 聚合(Aggregation)加速
- terms聚合直接扫描列值
- 基数聚合(cardinality)使用列式统计
- 比传统数据库的GROUP BY快10-100倍
- 脚本访问优化
- 脚本中访问doc values比_source解析快5-10倍
- 示例:
doc['price'].value * params.tax_rate
- 排序(Sorting)效率极高
- 内存与资源管理优势
- 堆外内存管理
- 默认使用文件系统缓存而非JVM堆内存
- 避免GC压力,稳定性提升
- 可通过indices.queries.cache.size控制内存使用
- 按需加载机制
- 仅加载查询涉及的列
- 支持内存映射(mmap)访问方式
- 对比fielddata的全量加载更节省资源
- 冷数据处理能力
- 数据持久化在磁盘,适合不频繁访问的历史数据
- 仍能保持较好性能(约为内存性能的60-70%)
- 堆外内存管理
- 特殊场景优化
-
高基数字段处理
-
全局序数(Global Ordinals)优化
bash{ "mappings": { "properties": { "user_id": { "type": "keyword", "eager_global_ordinals": true } } } }
-
-
地理空间数据
- 地理距离聚合依赖doc values
- 比传统GIS数据库快3-5倍
-
二进制数据
- 支持binary类型的快速读取
- 适合存储加密数据或序列化对象
-
五、正排索引的局限性
尽管正排索引(Doc Values)在Elasticsearch中提供了诸多优势,但在实际应用中仍存在一些重要的局限性。
1. 存储开销限制
- 冗余存储
- Doc Values 的默认启用:ES 默认对所有非文本字段(如 keyword、numeric、date)启用 Doc Values,即使某些字段不参与聚合或排序,仍会占用额外存储空间。
- Store Fields 与 _source 的重复:若字段同时开启 store: true,则同一份数据会存储在 _source 和 Store Fields 中,导致存储冗余。
- 示例:
一个 keyword 字段默认生成以下存储结构:- 倒排索引(用于搜索)
- Doc Values(用于聚合)
- _source(原始值)
- 若再设置 store: true,则额外存储一份原始值。
- 优化建议:
- 禁用不必要的 Doc Values:对无需聚合的字段设置 doc_values: false。
- 避免滥用 store: true:优先通过 _source 获取字段,仅在需要快速访问时启用。
- 高基数字段的存储膨胀
- 字典编码的局限性:对于高基数字段(如唯一 ID、哈希值),字典编码的压缩效率大幅下降,导致存储空间显著增加。
- 示例:
一个存储用户唯一 ID 的字段,若存在 1 亿个唯一值:- 字典编码需要维护 1 亿条映射关系。
- 存储空间可能超过原始值的 2 倍。
- 优化建议:
- 对高基数字段禁用 Doc Values,改用倒排索引或其他存储方式。
- 使用 eager_global_ordinals 优化聚合性能(预加载字典映射)。
2. 内存与性能的局限性
-
内存压力
- 文件系统缓存依赖:Doc Values 依赖操作系统的 Page Cache 加载数据,若物理内存不足,频繁的磁盘 IO 会严重降低聚合性能。
- 全局序号(Global Ordinals)的构建开销:高基数字段在聚合时需构建全局序号映射,首次查询延迟较高。
- 示例:
对包含 1 千万唯一值的 product_id 字段执行 terms 聚合:- 首次查询需构建全局序号,耗时可能达数百毫秒。
- 后续查询复用缓存,但内存占用较高。
- 优化建议:
- 增加物理内存,确保文件系统缓存充足。
- 对高频聚合的高基数字段启用 eager_global_ordinals,在段合并时预构建全局序号。
-
写入性能损耗
- 正排索引的构建成本:写入文档时,ES 需同步构建倒排索引和正排索引(Doc Values/Store Fields),增加 CPU 和 IO 开销。
- 实时性与吞吐量的权衡:高频写入场景下,正排索引的构建可能成为瓶颈,限制写入吞吐量。
- 优化建议:
- 对写入性能敏感的场景(如日志采集),关闭非必要字段的 Doc Values。
- 使用更快的存储介质(如 SSD)提升 IOPS。
3. 功能支持的局限性
-
不支持文本字段的 Doc Values
- 文本类型(text)的限制:text 字段默认不支持 Doc Values,因其内容经过分词处理,无法直接用于聚合或排序。
- 优化建议:
-
对需要聚合的文本字段,使用 keyword 类型子字段(Multi-fields):
bashPUT my_index { "mappings": { "properties": { "message": { "type": "text", "fields": { "keyword": { "type": "keyword" // 支持聚合 } } } } } }
-
-
不支持动态更新
- 段不可变性:正排索引(Doc Values)随 Lucene 段(Segment)的生成而固化,更新文档需重新构建整个段,无法原地修改。
- 近实时性限制:新写入的数据需通过 refresh 操作生成新段后,其正排索引才可见,默认延迟 1 秒。
- 优化建议:
- 调低 refresh_interval(如设置为 30s)减少段生成频率,平衡实时性与写入性能。
- 对实时性要求高的场景,使用 GET /_doc/{id} 直接访问文档(依赖 _source 而非正排索引)。
4. 查询场景的局限性
- 无法高效支持全文搜索
- 正排索引的定位:正排索引设计用于按文档访问字段值,而非通过词项定位文档。
- 全文搜索依赖倒排索引:若仅依赖正排索引,全文搜索需遍历所有文档,性能极差。
- 对比示例:
- 倒排索引:搜索 "error" 直接定位倒排列表,复杂度 O(1)。
- 正排索引:需遍历所有文档的 message 字段,复杂度 O(N)。
- 范围查询的局限性
- 非数值字段的低效性:对非数值字段(如 keyword)执行范围查询(如 "a" TO "z"),需遍历字典映射,性能较差。
- 优化建议:
- 对需要范围查询的字符串字段,使用 text 类型分词后结合倒排索引。
- 对数值或日期字段,优先使用 Doc Values 的范围查询优化。
六、正排索引的用途
Elasticsearch 的正排索引(Forward Index)主要用于支持高效的字段值访问和分析操作,与倒排索引(Inverted Index)形成互补,共同满足搜索、聚合、排序等复杂场景的需求。以下是正排索引的核心用途及其实际应用场景的详细说明:
-
聚合(Aggregations)
-
用途说明:正排索引通过 列式存储(Doc Values) 高效支持聚合操作,如统计字段分布、计算平均值/总和等。
-
优势:列式数据连续存储,便于批量遍历,压缩率高,减少磁盘 I/O。
-
示例:
bashGET sales/_search { "aggs": { "total_sales": { "sum": { "field": "amount" } }, "category_distribution": { "terms": { "field": "product_category.keyword" } } } }
- amount 字段的 Doc Values 直接遍历所有值求和。
- product_category.keyword 的 Doc Values 统计每个类别的文档数。
-
-
排序(Sorting)
-
用途说明:通过正排索引快速访问字段值,对搜索结果按指定字段排序。
-
优势:直接读取列式数据,避免解析 _source,性能显著提升。
-
示例:
bashGET products/_search { "sort": [ { "price": { "order": "desc" } }, // 使用 price 字段的 Doc Values { "_score": "desc" } ] }
-
-
脚本计算(Scripting)
-
用途说明:在查询脚本中动态访问字段值,支持复杂计算逻辑。
-
优势:通过 doc['field'].value 直接读取 Doc Values,延迟低。
-
示例:
bashGET products/_search { "script_fields": { "discounted_price": { "script": "doc['price'].value * 0.9" // 使用 price 字段的 Doc Values } } }
-
-
高亮显示(Highlighting)
-
用途说明:快速返回字段原始内容,用于搜索结果的高亮展示。
-
优势:若字段设置为 store: true,可直接从 Store Fields 读取数据,跳过解析 _source 的开销。
-
示例:
bashGET articles/_search { "query": { "match": { "content": "Elasticsearch" } }, "highlight": { "fields": { "content": {} } // 从 Store Fields 或 _source 获取原始内容 } }
-
-
部分字段返回(Stored Fields)
-
用途说明:直接返回指定字段的原始值,无需解析完整 _source。
-
优势:减少网络传输和 JSON 解析开销,提升响应速度。
-
示例:
bashGET logs/_search { "stored_fields": ["timestamp", "status_code"], // 从 Store Fields 获取 "query": { "match_all": {} } }
-
-
范围查询(Range Queries)
-
用途说明:对数值、日期等字段执行范围查询时,正排索引优化数据访问。
-
优势:列式存储天然有序,支持快速范围过滤。
-
示例:
bashGET logs/_search { "query": { "range": { "timestamp": { "gte": "2023-01-01", "lte": "2023-12-31" } } } }
-
-
字段值存在性检查(Exists Query)
-
用途说明:快速判断某字段是否存在非空值。
-
优势:直接遍历 Doc Values 检查字段值的非空性。
-
示例:
bashGET users/_search { "query": { "exists": { "field": "email" } // 检查 email 字段是否有值 } }
-
-
地理空间分析(Geospatial Analytics)
-
用途说明:对地理坐标字段(如 geo_point)进行聚合或距离计算。
-
优势:Doc Values 支持高效的地理数据遍历。
-
示例:
bashGET locations/_search { "aggs": { "heatmap": { "geohash_grid": { "field": "coordinates", "precision": 5 } } } }
-
-
多字段组合分析
-
用途说明:在复杂分析场景中,联合使用多个字段的 Doc Values。
-
示例:
bashGET sales/_search { "aggs": { "sales_by_region": { "terms": { "field": "region.keyword" }, // 使用 region 的 Doc Values "aggs": { "avg_amount": { "avg": { "field": "amount" } } // 使用 amount 的 Doc Values } } } }
-
-
时序数据分析(Time Series)
-
用途说明:针对时间序列数据(如日志、指标),利用 Doc Values 高效处理时间范围聚合。
-
示例:
bashGET metrics/_search { "aggs": { "hourly_stats": { "date_histogram": { "field": "timestamp", "calendar_interval": "1h" }, "aggs": { "avg_value": { "avg": { "field": "value" } } } } } }
-
七、正排索引的优化策略
Elasticsearch 的正排索引(Forward Index)优化策略需结合存储效率、查询性能、写入速度等多方面因素。以下是系统化的优化方案,涵盖配置调整、数据结构设计及硬件调优:
-
存储优化
- 按需启用 Doc Values
-
禁用非必要字段:对不参与聚合、排序的字段关闭 Doc Values。
bashPUT my_index/_mapping { "properties": { "log_message": { "type": "text", "doc_values": false // 关闭聚合能力,减少存储 } } }
-
高基数字段特殊处理:唯一ID等字段建议禁用 Doc Values,改用倒排索引。
-
- 避免冗余存储
-
慎用 store: true:仅对高频访问字段(如标题、摘要)启用存储字段。
bashPUT my_index/_mapping { "properties": { "title": { "type": "text", "store": true // 显式存储,用于快速返回 } } }
-
依赖 _source 作为主存储:默认通过 _source 返回数据,减少冗余。
-
- 压缩优化
- 数值类型优化:
- 使用最小化数据类型(如 byte 替代 integer)。
- 启用 index_options: docs 减少倒排索引开销(仅记录文档ID)。
- 字符串类型优化:
- 使用 keyword 类型代替 text 进行聚合。
- 对长文本禁用 norms 和 index_options。
- 数值类型优化:
- 按需启用 Doc Values
-
查询性能优化
-
全局序号(Global Ordinals)预热
-
启用 eager_global_ordinals:对高基数字段预加载字典映射,减少首次聚合延迟。
bashPUT my_index/_mapping { "properties": { "user_id": { "type": "keyword", "eager_global_ordinals": true // 预加载字典 } } }
-
-
缓存策略
-
聚合结果缓存:对重复查询使用 cache: true。
bashGET sales/_search { "aggs": { "total_sales": { "sum": { "field": "amount", "missing": 0 } } }, "size": 0, "request_cache": true // 启用查询缓存 }
-
-
分页优化
- 避免深度分页:使用 search_after 替代 from/size,减少内存占用。
- 聚合分页:对海量数据聚合使用 composite 分桶。
-
-
写入性能优化
-
降低 Refresh 频率
-
增大 refresh_interval,减少段生成频率:
bashPUT my_index/_settings { "index.refresh_interval": "30s" // 默认1s,调整为30秒 }
-
-
批量写入
- 使用 bulk API 批量提交数据,减少 IOPS 压力。
- 控制单批次文档数(建议 5-15MB/批次)。
-
关闭副本写入
-
写入高峰期临时关闭副本,写入完成后再恢复:
bashPUT my_index/_settings { "index.number_of_replicas": 0 }
-
-
-
数据结构与映射优化
-
字段类型选择
- 数值类型:优先使用 integer、short、byte 等最小化类型。
- 时间类型:使用 date 而非 text 或 keyword。
- 高维数据:地理位置使用 geo_point,IP地址使用 ip 类型。
-
多字段(Multi-fields)设计
-
对文本字段同时支持搜索和聚合:
bashPUT my_index/_mapping { "properties": { "message": { "type": "text", // 支持全文搜索 "fields": { "keyword": { "type": "keyword" // 支持聚合 } } } } }
-
-
避免嵌套对象
- 扁平化数据结构,减少 nested 类型使用(因其 Doc Values 效率较低)。
-
-
硬件与架构优化
- 分片策略
- 控制分片大小:单个分片建议 10-50GB(过小导致段过多,过大影响并行性)。
- 预创建索引:对时序数据按时间滚动(如每日索引),避免分片膨胀。
- 存储介质
- 使用 SSD 提升 IOPS,尤其对高写入场景。
- 确保足够内存,保障文件系统缓存(Page Cache)容量。
- 冷热架构
- 对历史数据迁移至冷节点(使用高压缩率存储),热节点专注实时查询。
- 分片策略