11.1 索引设计最佳实践 Elasticsearch 索引设计最佳实践详解 索引设计的重要性 索引设计是 Elasticsearch 项目成功的基石。如同数据库中的表结构设计一样,索引的设计直接影响着数据存储、检索效率、以及集群的整体健康状况。一个优秀的索引设计应该兼顾以下几个关键方面: 性能: 快速的查询响应时间是 Elasticsearch 的核心价值。合理的索引设计能够优化查询路径,减少不必要的资源消耗,从而提升查询性能。 扩展性: 随着数据量的增长,索引需要能够平滑扩展。良好的索引设计应该考虑到未来的数据增长,并预留足够的扩展空间。 资源利用率: 索引的设计应该尽可能高效地利用集群资源,包括磁盘空间、内存和 CPU。避免资源浪费,降低运营成本。
索引设计是 Elasticsearch 项目成功的基石。如同数据库中的表结构设计一样,索引的设计直接影响着数据存储、检索效率、以及集群的整体健康状况。一个优秀的索引设计应该兼顾以下几个关键方面:
性能: 快速的查询响应时间是 Elasticsearch 的核心价值。合理的索引设计能够优化查询路径,减少不必要的资源消耗,从而提升查询性能。
扩展性: 随着数据量的增长,索引需要能够平滑扩展。良好的索引设计应该考虑到未来的数据增长,并预留足够的扩展空间。
资源利用率: 索引的设计应该尽可能高效地利用集群资源,包括磁盘空间、内存和 CPU。避免资源浪费,降低运营成本。
维护性: 一个易于维护的索引设计能够简化日常管理工作,例如索引重建、数据迁移、以及故障排除。
在深入具体实践之前,我们先来了解一些索引设计的核心原则,这些原则将贯穿整个索引设计过程:
了解你的数据: 在设计索引之前,务必深入了解你的数据结构、数据量、数据增长速度、以及数据的使用场景(读多写少还是读写均衡)。
明确查询需求: 索引设计应该围绕查询需求展开。你需要明确你的用户会如何查询数据,查询的字段、条件、排序方式等。
平衡性能与资源: 索引设计需要在查询性能和资源消耗之间找到平衡点。过度优化可能会导致资源浪费,而资源不足则会影响性能。
持续优化: 索引设计不是一蹴而就的,需要随着业务发展和数据变化持续优化。定期监控索引性能,并根据实际情况进行调整。
分片是 Elasticsearch 实现水平扩展的关键机制。一个索引可以被分成多个分片,分布在集群的不同节点上。合理的分片策略对于提升索引性能和扩展性至关重要。
分片数量直接影响索引的并行处理能力和数据分布。
过少的分片: 会导致单个分片数据量过大,查询时单个分片压力过高,无法充分利用集群资源,影响查询性能和扩展性。
过多的分片: 会增加集群管理的开销,例如路由计算、元数据管理等。每个分片都会消耗一定的资源(例如文件句柄、内存)。过多的分片还会导致 “shard over-allocation” 问题,影响集群稳定性。
最佳实践:
根据数据量估算: 一般来说,每个分片的大小控制在 20GB - 40GB 左右是一个比较好的经验值。你可以根据你的总数据量和这个经验值来估算初始的分片数量。例如,如果你的数据量预计为 200GB,那么可以考虑 5-10 个主分片。
考虑未来增长: 在估算分片数量时,要考虑到未来数据量的增长。预留一定的分片数量,以便未来能够平滑扩展。
测试和调整: 最佳的分片数量往往需要通过实际测试来确定。你可以尝试不同的分片数量,并监控查询性能和资源消耗,找到最佳平衡点。
代码示例 (创建索引时指定分片数量)
PUT /my_index { "settings": { "index": { "number_of_shards": 5, // 设置主分片数量为 5 "number_of_replicas": 1 // 设置副本分片数量为 1 } }, "mappings": { "properties": { "field1": { "type": "text" }, "field2": { "type": "keyword" } } } }
mermaid 图示 (分片分布)
图示解释:
my_index 索引被划分为 5 个主分片 (Shard 1-5, denoted as P for Primary) 和 5 个副本分片 (Shard 1R-5R, denoted as R for Replica).
主分片和副本分片分散在 3 个 Elasticsearch 节点 (Node 1, Node 2, Node 3) 上,实现了数据的分布式存储和高可用性。
默认情况下,Elasticsearch 使用文档 ID 的哈希值来决定文档被路由到哪个分片。在某些高级场景下,你可以自定义路由策略,例如:
基于业务字段路由: 将同一业务类型的数据路由到相同的分片,可以提高特定业务场景的查询效率。
冷热数据分离: 将热数据 (经常访问的数据) 和冷数据 (很少访问的数据) 路由到不同的索引或分片,可以优化资源利用率。
代码示例 (创建索引时指定路由字段)
PUT /my_index { "settings": { "index": { "number_of_shards": 3, "number_of_replicas": 1 } }, "mappings": { "_routing": { "required": true, // 强制指定路由字段 "path": "user_id" // 使用 user_id 字段作为路由字段 }, "properties": { "user_id": { "type": "keyword" }, "product_id": { "type": "keyword" }, "timestamp": { "type": "date" } } } }
代码解释:
"_routing": { "required": true, "path": "user_id" } 配置表示在索引文档时,必须指定 routing 参数,并且路由字段为 user_id。
在索引文档时,需要显式指定 routing 参数:
POST /my_index/_doc?routing=user123 { "user_id": "user123", "product_id": "product456", "timestamp": "2023-10-27T10:00:00" }
注意: 路由策略的设计需要仔细考虑,一旦确定后,更改路由策略将非常困难。
副本分片是主分片的复制品,用于提供数据冗余和提高读取性能。
没有副本: 数据安全性较低,如果主分片所在节点故障,数据可能会丢失,并且读取请求只能由主分片处理,影响读取性能。
过多的副本: 会增加集群的存储开销和写入延迟。每次写入操作都需要同步到所有副本分片。
最佳实践:
至少一个副本: 建议至少配置一个副本分片,以保证数据的高可用性。
根据读取需求调整: 如果读取请求量很大,可以适当增加副本数量,提高读取吞吐量。
考虑集群规模: 副本数量也要考虑集群的节点数量。副本分片应该尽可能分布在不同的节点上,避免单点故障。
代码示例 (创建索引时指定副本数量)
PUT /my_index { "settings": { "index": { "number_of_shards": 3, "number_of_replicas": 2 // 设置副本分片数量为 2 } }, "mappings": { "properties": { "field1": { "type": "text" }, "field2": { "type": "keyword" } } } }
mermaid 图示 (副本分布)
图示解释:
my_index 索引被划分为 3 个主分片和 6 个副本分片 (每个主分片 2 个副本).
副本分片分散在不同的节点上,提高了数据的冗余度和读取性能。
副本数量可以动态调整,无需重启集群或索引。你可以根据实际情况,例如读取压力变化、节点故障等,动态调整副本数量。
代码示例 (动态调整副本数量)
PUT /my_index/_settings { "index": { "number_of_replicas": 2 // 将副本数量调整为 2 } }
字段映射定义了索引中字段的数据类型、索引方式、分词器等重要属性。合理的字段映射能够显著提升搜索性能和节省存储空间。
Elasticsearch 提供了丰富的数据类型,例如 text, keyword, date, integer, float, boolean, geo_point 等。选择合适的数据类型至关重要。
text: 用于全文搜索,会被分词器处理,适合存储文本内容,例如文章内容、评论等。
keyword: 用于精确匹配、排序和聚合,不会被分词器处理,适合存储标签、类别、ID 等。
date: 用于存储日期和时间,可以进行日期范围查询和排序。
integer, float: 用于存储数值数据,可以进行数值范围查询和排序。
boolean: 用于存储布尔值 (true/false)。
geo_point: 用于存储地理位置信息,可以进行地理位置查询。
最佳实践:
根据字段用途选择: 根据字段的实际用途选择最合适的数据类型。例如,如果字段需要进行全文搜索,则使用 text 类型;如果字段只需要进行精确匹配,则使用 keyword 类型。
避免过度使用 text 类型: text 类型会占用更多的存储空间和索引资源。对于不需要全文搜索的字段,尽量使用 keyword 或其他更合适的数据类型。
代码示例 (定义字段映射)
PUT /my_index { "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word" }, // 使用 ik_max_word 分词器 "tags": { "type": "keyword" }, "publish_date": { "type": "date" }, "price": { "type": "float" }, "is_active": { "type": "boolean" }, "location": { "type": "geo_point" } } } }
index 参数index 参数控制字段是否被索引,以及如何被索引。
index: true (默认): 字段会被索引,可以被搜索。
index: false: 字段不会被索引,无法被搜索,但仍然可以被检索 (例如通过 _source 字段)。适用于不需要搜索的字段,可以节省索引空间和写入时间。
index: not_analyzed (已弃用): 在旧版本 Elasticsearch 中使用,等同于 keyword 类型。
index_options: 控制索引时存储的信息,例如 docs, freqs, positions, offsets。可以根据查询需求进行优化,例如如果只需要匹配文档,不需要词频和位置信息,可以设置为 docs,节省索引空间。
最佳实践:
仅索引需要搜索的字段: 对于不需要搜索的字段,设置为 index: false,可以显著节省索引空间和写入时间。
根据查询需求优化 index_options: 默认的 index_options 为 positions,会存储词频、位置和偏移量信息。如果不需要位置信息,可以设置为 freqs 或 docs,节省索引空间。
代码示例 (优化 index 参数)
PUT /my_index { "mappings": { "properties": { "product_name": { "type": "text" }, "description": { "type": "text", "index_options": "freqs" }, // 优化 index_options "price": { "type": "float", "index": false }, // 不索引 price 字段 "category": { "type": "keyword" } } } }
analyzer 参数analyzer 参数指定字段使用的分词器。分词器负责将文本内容分解成词项 (term),用于建立倒排索引。
选择合适的分词器: Elasticsearch 提供了多种内置分词器,例如 standard, simple, whitespace, stop, keyword, ik_max_word, ik_smart 等。选择合适的分词器对于提高搜索精度和召回率至关重要。
自定义分词器: 你可以根据业务需求自定义分词器,例如使用插件、组合分词器等。
最佳实践:
根据语言和业务选择分词器: 对于中文文本,通常使用 ik_max_word 或 ik_smart 分词器。对于英文文本,可以使用 standard 分词器。
测试分词效果: 可以使用 Elasticsearch 的 _analyze API 测试分词器的分词效果,确保分词结果符合预期。
代码示例 (指定分词器)
PUT /my_index { "mappings": { "properties": { "content_zh": { "type": "text", "analyzer": "ik_max_word" }, // 中文内容使用 ik_max_word 分词器 "content_en": { "type": "text", "analyzer": "english" } // 英文内容使用 english 分词器 } } }
norms 参数norms 参数用于存储字段的文档长度归一化因子,用于计算文档相关性评分。
norms: true (默认): 启用 norms,会占用一定的存储空间。
norms: false: 禁用 norms,可以节省存储空间,但会影响相关性评分的准确性。如果你的查询不依赖于文档长度归一化,可以禁用 norms。例如,对于 keyword 类型字段,通常可以禁用 norms。
最佳实践:
对于 keyword 类型字段,可以禁用 norms: keyword 类型字段通常用于精确匹配,不需要计算相关性评分,可以禁用 norms 节省空间。
对于需要相关性评分的字段,保持默认 norms: true: 例如,对于 text 类型字段,通常需要计算相关性评分,保持默认 norms: true。
代码示例 (禁用 norms 参数)
PUT /my_index { "mappings": { "properties": { "category": { "type": "keyword", "norms": false }, // 禁用 category 字段的 norms "title": { "type": "text" } // text 类型字段默认启用 norms } } }
doc_values 参数doc_values 参数用于启用列式存储,可以加速排序、聚合和脚本操作。
doc_values: true (默认): 启用 doc_values,会占用一定的存储空间,但可以显著提升排序、聚合和脚本操作的性能。
doc_values: false: 禁用 doc_values,可以节省存储空间,但会降低排序、聚合和脚本操作的性能。对于不需要排序、聚合和脚本操作的字段,可以禁用 doc_values。例如,对于只用于全文搜索的 text 类型字段,可以禁用 doc_values。
最佳实践:
对于需要排序、聚合和脚本操作的字段,保持默认 doc_values: true: 例如,对于 keyword, date, integer, float 等类型字段,通常需要进行排序和聚合操作,保持默认 doc_values: true。
对于只用于全文搜索的 text 类型字段,可以禁用 doc_values: text 类型字段主要用于全文搜索,排序和聚合操作较少,可以禁用 doc_values 节省空间。
代码示例 (禁用 doc_values 参数)
PUT /my_index { "mappings": { "properties": { "content": { "type": "text", "doc_values": false }, // 禁用 content 字段的 doc_values "price": { "type": "float" } // float 类型字段默认启用 doc_values } } }
store 参数store 参数控制字段的值是否存储在 _source 字段之外,单独存储。
store: false (默认): 字段的值不单独存储,只能通过 _source 字段检索。
store: true: 字段的值单独存储,可以独立于 _source 字段检索。适用于需要频繁访问但 _source 字段过大的场景,可以提高检索效率。但会增加存储空间。
最佳实践:
默认情况下,保持 store: false: 大多数情况下,通过 _source 字段检索字段值已经足够满足需求。
在特定场景下,可以启用 store: true: 例如,如果你的 _source 字段非常大,但你需要频繁访问某个字段的值,可以考虑将该字段设置为 store: true,提高检索效率。
代码示例 (启用 store 参数)
PUT /my_index { "mappings": { "properties": { "product_id": { "type": "keyword", "store": true }, // 单独存储 product_id 字段 "product_name": { "type": "text" } } } }
动态映射允许 Elasticsearch 在索引文档时自动推断字段的数据类型并创建映射。
dynamic: true (默认): 允许动态映射。
dynamic: false: 禁止动态映射。如果索引文档中包含未定义的字段,则会报错。
dynamic: strict: 严格禁止动态映射。如果索引文档中包含未定义的字段,则会报错,并且拒绝索引文档。
最佳实践:
生产环境建议使用 dynamic: strict 或 dynamic: false: 避免意外的字段类型推断错误,保证数据质量和索引结构的稳定性。
开发和测试环境可以使用 dynamic: true: 方便快速原型开发和测试。
代码示例 (禁用动态映射)
PUT /my_index { "mappings": { "dynamic": "strict", // 严格禁止动态映射 "properties": { "field1": { "type": "text" }, "field2": { "type": "keyword" } } } }
数据建模是指如何组织和存储你的数据,以便在 Elasticsearch 中高效地进行搜索和分析。
在关系型数据库中,通常采用范式化来减少数据冗余。但在 Elasticsearch 中,为了提高查询性能,通常采用反范式化,将相关的数据冗余存储在一个文档中。
最佳实践:
将相关数据聚合到一个文档: 例如,订单数据可以包含用户信息、商品信息、支付信息等,将这些信息聚合到一个订单文档中,避免跨索引或跨文档关联查询。
权衡数据冗余和查询性能: 反范式化会增加数据冗余,但可以显著提升查询性能。需要在数据冗余和查询性能之间找到平衡点。
mermaid 图示 (反范式化数据模型)
图示解释:
Order Document 包含了订单的所有相关信息,例如订单 ID、订单日期、总价、用户信息、商品列表等。
避免了跨索引或跨文档关联查询,提高了查询效率。
Elasticsearch 支持多种文档结构,例如扁平结构、嵌套对象、父子文档 (已逐渐被 Join 数据类型取代)。
扁平结构: 最简单的文档结构,所有字段都位于文档的根级别。适用于简单的数据模型。
嵌套对象 (Nested Object): 允许在一个文档中存储多个相关对象,例如订单中的商品列表。嵌套对象之间是独立的文档,可以独立查询。
父子文档 (Parent-Child): 用于表示父子关系的数据,例如博客文章和评论。父子文档之间通过 join 数据类型关联。
最佳实践:
根据数据关系选择文档结构: 对于简单的数据模型,可以使用扁平结构。对于具有复杂关系的数据,可以使用嵌套对象或父子文档。
避免过度嵌套: 过深的嵌套会影响查询性能和索引效率。
代码示例 (使用嵌套对象)
PUT /my_index { "mappings": { "properties": { "order_id": { "type": "keyword" }, "order_date": { "type": "date" }, "customer": { "properties": { "customer_id": { "type": "keyword" }, "customer_name": { "type": "text" } } }, "products": { // 嵌套对象字段 "type": "nested", "properties": { "product_id": { "type": "keyword" }, "product_name": { "type": "text" }, "quantity": { "type": "integer" } } } } } }
随着时间的推移,数据会不断增长,旧数据可能会变得不常访问。索引生命周期管理 (ILM) 允许你根据数据的年龄和访问频率,自动化管理索引的生命周期,例如滚动更新、冷热分离、删除旧数据等。