ElasticSearch 映射属性设置的性能考量
ElasticSearch 映射属性设置的性能考量
数据类型选择
在 ElasticSearch 中,正确选择数据类型对性能有着深远影响。例如,对于数值类型,如果取值范围明确且较小,使用 byte
或 short
比 long
更节省空间,从而提升查询性能。因为较小的数据类型占用更少的磁盘空间和内存,在检索时 I/O 操作和内存处理压力更小。
示例代码:
PUT my_index
{
"mappings": {
"properties": {
"my_byte_field": {
"type": "byte"
},
"my_short_field": {
"type": "short"
},
"my_long_field": {
"type": "long"
}
}
}
}
在这个示例中,我们分别定义了 byte
、short
和 long
类型的字段。如果我们确定某个数值字段的值范围在 -128 到 127 之间,那么使用 byte
类型就足够了,避免了使用 long
类型带来的空间浪费。
对于日期类型,date
类型是常用的选择。但是如果只需要精确到日期,而不需要时间部分,那么可以考虑使用 date_nanos
类型并设置合适的格式。date_nanos
类型在存储日期时更加紧凑,能提升存储效率和查询性能。
示例代码:
PUT my_date_index
{
"mappings": {
"properties": {
"my_date_field": {
"type": "date_nanos",
"format": "yyyy-MM-dd"
}
}
}
}
这里将日期字段定义为 date_nanos
类型,并指定了日期格式,使得 ElasticSearch 在存储和检索日期时能更高效地处理。
字符串类型的选择也很关键。text
类型用于全文搜索,它会对文本进行分词处理。而 keyword
类型用于精确匹配,不会分词。如果一个字段主要用于过滤、排序或精确匹配,如产品编号、身份证号等,应使用 keyword
类型。例如:
PUT my_string_index
{
"mappings": {
"properties": {
"product_id": {
"type": "keyword"
},
"product_description": {
"type": "text"
}
}
}
}
这样,product_id
字段适合精确查找,而 product_description
字段适合全文搜索,不同的类型选择满足了不同的业务需求,同时也优化了性能。
字段的多态性
ElasticSearch 支持字段的多态性,即一个字段可以同时包含多种数据类型的值。然而,这种灵活性可能会带来性能问题。当一个字段具有多态性时,ElasticSearch 需要额外的空间和计算资源来处理不同类型的值。
例如,假设我们有一个字段 my_field
,它有时存储字符串,有时存储数值。
PUT polymorphic_index
{
"mappings": {
"properties": {
"my_field": {
"type": "object",
"enabled": false
}
}
}
}
POST polymorphic_index/_doc/1
{
"my_field": "string_value"
}
POST polymorphic_index/_doc/2
{
"my_field": 123
}
在这种情况下,ElasticSearch 为了存储和检索不同类型的值,需要进行额外的处理,这会降低性能。如果可能,应尽量避免字段的多态性,保持字段类型的一致性。如果业务需求确实需要存储不同类型的数据,可以考虑将其拆分为多个字段,每个字段具有单一的数据类型。
字段的索引设置
- 是否索引
在 ElasticSearch 中,并非所有字段都需要索引。只有需要进行搜索、排序或聚合的字段才应该被索引。对于那些仅用于展示但不需要搜索的字段,如图片的 URL 等,可以设置
index: false
来避免建立索引,从而节省存储空间和索引构建时间。
示例代码:
PUT my_index_with_non_indexed_field
{
"mappings": {
"properties": {
"image_url": {
"type": "keyword",
"index": false
}
}
}
}
这里将 image_url
字段设置为不索引,这样 ElasticSearch 在处理文档时就不会为该字段构建倒排索引,减少了索引的大小和构建索引的开销。
- 索引策略
对于索引的字段,ElasticSearch 提供了不同的索引策略。
doc_values
是一种重要的索引策略,它主要用于排序和聚合操作。当一个字段设置了doc_values
为true
(默认值)时,ElasticSearch 会在文档写入时创建一份按文档顺序排列的字段值副本,用于快速的排序和聚合。
例如,对于一个数值类型的字段 price
,如果我们经常基于这个字段进行价格排序和聚合分析:
PUT price_index
{
"mappings": {
"properties": {
"price": {
"type": "float",
"doc_values": true
}
}
}
}
通过设置 doc_values
,在进行价格相关的排序和聚合操作时,ElasticSearch 可以快速地从这个副本中获取数据,提升了操作的性能。然而,doc_values
会占用额外的磁盘空间,所以对于一些很少用于排序和聚合的字段,可以考虑设置 doc_values: false
来节省空间。
另一个索引策略是 fielddata
,它主要用于对 text
类型字段进行排序、聚合或脚本操作。但是,fielddata
是在查询时加载到内存中的,并且会占用大量的堆内存,可能导致内存溢出等问题。因此,除非必要,不建议对 text
类型字段使用 fielddata
。如果确实需要对 text
类型字段进行聚合等操作,可以考虑将其拆分为 text
和 keyword
两个字段,对 keyword
字段进行聚合等操作。
嵌套和父子关系
- 嵌套类型 在 ElasticSearch 中,嵌套类型用于处理对象数组,其中每个对象都可以独立地被索引和查询。例如,一个订单文档可能包含多个订单项,每个订单项是一个对象,并且我们可能需要独立地对订单项进行查询。
示例代码:
PUT order_index
{
"mappings": {
"properties": {
"order_items": {
"type": "nested",
"properties": {
"product_name": {
"type": "text"
},
"quantity": {
"type": "integer"
}
}
}
}
}
}
POST order_index/_doc/1
{
"order_items": [
{
"product_name": "Product A",
"quantity": 2
},
{
"product_name": "Product B",
"quantity": 3
}
]
}
在这个示例中,order_items
字段被定义为 nested
类型。这样,我们可以针对每个订单项进行独立的查询,如查找包含 Product A
的订单项。然而,嵌套类型会增加索引的复杂性和存储开销,因为每个嵌套对象都被单独索引。在查询时,嵌套查询也相对复杂且性能消耗较大,所以在使用嵌套类型时,需要权衡业务需求和性能影响。
- 父子关系 父子关系用于处理文档之间存在父子层级关系的场景,例如博客文章和评论,文章是父文档,评论是子文档。
示例代码:
PUT blog_index
{
"mappings": {
"_parent": {
"type": "blog_post"
},
"properties": {
"comment_text": {
"type": "text"
}
}
}
}
PUT blog_index/_doc/1?routing=1
{
"title": "My Blog Post",
"content": "This is my blog post content."
}
POST blog_index/_doc/2?routing=1&parent=1
{
"comment_text": "This is a great post!"
}
在这个例子中,评论文档通过 _parent
字段关联到对应的博客文章。父子关系的优势在于可以在查询父文档时快速获取相关的子文档,并且在更新父文档时,子文档的相关性也能得到维护。但是,父子关系同样会增加索引和查询的复杂性,因为 ElasticSearch 需要维护文档之间的父子关系。在大规模数据场景下,父子关系的性能开销可能会比较大,需要谨慎使用。
动态映射与静态映射
- 动态映射 ElasticSearch 支持动态映射,即当新的文档写入时,如果文档中的字段在映射中不存在,ElasticSearch 会自动根据字段值推断其数据类型并添加到映射中。动态映射方便了开发,减少了预先定义映射的工作量。
例如,我们向一个没有预定义映射的索引写入文档:
POST dynamic_index/_doc/1
{
"new_field": "some value"
}
ElasticSearch 会自动为 new_field
推断数据类型,如 text
类型,并添加到映射中。然而,动态映射也有一些潜在的性能问题。首先,每次动态添加字段都需要更新映射,这会导致索引的短暂不可用。其次,如果数据类型推断错误,可能会导致后续的查询和分析出现问题,并且纠正数据类型需要重建索引,这是一个非常耗时的操作。
- 静态映射 静态映射是在索引创建之前就明确地定义好所有字段的映射。通过静态映射,我们可以精确控制每个字段的数据类型、索引设置等,避免了动态映射可能带来的问题。
示例代码:
PUT static_index
{
"mappings": {
"properties": {
"field1": {
"type": "text"
},
"field2": {
"type": "integer"
}
}
}
}
在这个示例中,我们预先定义了 field1
和 field2
两个字段的映射。静态映射可以提高索引的稳定性和性能,因为在索引创建后不需要频繁更新映射。但是,静态映射要求开发人员对业务数据有深入的了解,提前规划好所有字段,这在一些快速迭代的项目中可能不太方便。因此,在实际应用中,需要根据项目的特点和需求,合理选择动态映射或静态映射,或者结合两者使用。
副本和分片设置
- 分片设置 ElasticSearch 将索引划分为多个分片,每个分片是一个独立的 Lucene 索引。合理设置分片数量对性能至关重要。如果分片数量过少,在处理大规模数据时,单个分片可能会变得过大,导致查询性能下降,因为查询时需要处理的数据量较大。另一方面,如果分片数量过多,会增加索引管理的开销,如索引的合并、数据的复制等,同时也会占用更多的文件描述符等系统资源。
在确定分片数量时,需要考虑数据量的大小、硬件资源以及查询模式等因素。一般来说,对于较小的数据集,可以使用较少的分片,如 1 - 3 个分片。对于大规模数据集,可以根据预估的数据量和硬件性能来计算合适的分片数量。例如,如果每个分片预计存储 20GB 数据,而总数据量预计为 200GB,那么可以考虑设置 10 个分片。
示例代码:
PUT my_index_with_shards
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"my_field": {
"type": "text"
}
}
}
}
这里我们将索引设置为 5 个分片和 1 个副本。在实际应用中,需要根据实际情况进行调整。
- 副本设置 副本是分片的拷贝,用于提高数据的可用性和查询性能。通过设置副本数量,可以在某个分片出现故障时,数据仍然可用。同时,副本也可以分担查询请求,提高查询的并发处理能力。
然而,副本数量过多也会带来性能问题。每个副本都需要占用额外的磁盘空间和网络带宽来进行数据复制,并且在数据写入时,需要同步更新所有副本,这会增加写入的延迟。一般来说,对于读多写少的场景,可以适当增加副本数量,如设置 2 - 3 个副本;对于写多读少的场景,副本数量可以设置为 1 个。
在上述示例中,我们设置了 number_of_replicas
为 1,表示每个分片有一个副本。在实际应用中,可以根据业务的读写特性来调整副本数量,以达到最佳的性能平衡。
分析器设置
- 内置分析器
ElasticSearch 提供了多种内置分析器,如
standard
分析器、simple
分析器、whitespace
分析器等。不同的分析器适用于不同的文本处理需求,选择合适的分析器对查询性能有重要影响。
standard
分析器是默认的分析器,它会将文本按词边界进行分词,并进行小写转换等操作。例如,对于文本 "Hello, World!",standard
分析器会将其分词为 "hello" 和 "world"。
simple
分析器会按非字母字符进行分词,并将所有词转换为小写。对于文本 "Hello-World!",simple
分析器会分词为 "hello" 和 "world"。
whitespace
分析器则按空白字符进行分词,对于文本 "Hello World!",whitespace
分析器会分词为 "Hello" 和 "World"。
示例代码:
PUT analyzer_index
{
"mappings": {
"properties": {
"text_field": {
"type": "text",
"analyzer": "standard"
}
}
}
}
在这个示例中,我们将 text_field
字段的分析器设置为 standard
。如果业务需求是对文本进行更简单的按空白字符分词,那么可以将分析器设置为 whitespace
。
- 自定义分析器 除了使用内置分析器,ElasticSearch 还允许我们创建自定义分析器。自定义分析器可以根据业务需求灵活组合字符过滤器、分词器和令牌过滤器。
例如,假设我们需要对中文文本进行更精准的分词,我们可以使用 IK 分词器(需要先安装 IK 插件)来创建自定义分析器。
示例代码:
PUT custom_analyzer_index
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": [
"lowercase"
]
}
}
}
},
"mappings": {
"properties": {
"chinese_text_field": {
"type": "text",
"analyzer": "my_custom_analyzer"
}
}
}
}
在这个示例中,我们创建了一个名为 my_custom_analyzer
的自定义分析器,它使用 ik_max_word
分词器对中文文本进行分词,并通过 lowercase
过滤器将分词结果转换为小写。通过使用自定义分析器,我们可以更好地满足业务对文本处理的特定需求,从而提升查询性能。
映射更新对性能的影响
- 部分更新 在 ElasticSearch 中,可以对映射进行部分更新,即添加新字段或修改现有字段的某些设置。例如,我们可以在现有索引中添加一个新字段:
PUT my_index/_mapping
{
"properties": {
"new_field": {
"type": "text"
}
}
}
部分更新映射相对来说对性能的影响较小,因为它不需要重建整个索引。ElasticSearch 会在后台更新映射,并在必要时调整索引结构。然而,部分更新仍然会导致索引的短暂不可用,特别是在大规模索引上,这个过程可能需要一些时间。
- 完全重建映射 有时,由于业务需求的变化,可能需要完全重建映射,例如修改字段的数据类型。这种情况下,需要先创建一个新的索引,将旧索引的数据迁移到新索引,然后删除旧索引。
示例代码:
PUT new_index
{
"mappings": {
"properties": {
"field_to_change": {
"type": "keyword"
}
}
}
}
POST _reindex
{
"source": {
"index": "old_index"
},
"dest": {
"index": "new_index"
}
}
DELETE old_index
完全重建映射是一个非常耗时且资源消耗大的操作,因为它涉及到数据的迁移和索引的重建。在执行之前,需要充分评估对业务的影响,尽量选择在业务低峰期进行操作,以减少对用户的影响。同时,在重建映射之前,需要备份好数据,以防出现意外情况。
在 ElasticSearch 的实际应用中,合理设置映射属性对于提升性能至关重要。从数据类型的选择、字段的索引设置,到嵌套和父子关系的处理,以及副本和分片的设置等多个方面,都需要综合考虑业务需求和性能因素。通过深入理解这些性能考量点,并结合实际场景进行优化,可以使 ElasticSearch 在存储和检索数据时达到最佳的性能表现。