MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

存储字段在ElasticSearch中的管理

2024-05-124.4k 阅读

ElasticSearch 存储字段概述

在 ElasticSearch 中,存储字段是用于持久化文档中特定数据的重要机制。与传统关系型数据库不同,ElasticSearch 是基于文档的搜索引擎,每个文档可以包含多个字段,这些字段的管理对于数据的存储、检索和分析起着关键作用。

字段类型与存储

ElasticSearch 支持多种字段类型,如文本(text)、关键词(keyword)、数值(number)、日期(date)等。每种类型在存储和处理上都有其特点。

  1. 文本类型:文本类型字段主要用于存储长文本内容,例如文章正文、产品描述等。ElasticSearch 会对文本字段进行分词处理,将文本拆分成一个个单词,以便进行全文搜索。例如,对于一篇新闻文章的标题字段:
{
    "title": "ElasticSearch 存储字段管理详解"
}

在 ElasticSearch 中,这个标题字段会被分词成类似 ["ElasticSearch", "存储", "字段", "管理", "详解"] 这样的词项,存储在倒排索引中。需要注意的是,文本类型字段默认是不存储原始值的,也就是说,如果我们只定义了 title 为文本类型,直接获取文档时,无法得到完整的标题内容。如果需要存储原始值,我们可以设置 storetrue

{
    "mappings": {
        "properties": {
            "title": {
                "type": "text",
                "store": true
            }
        }
    }
}
  1. 关键词类型:关键词类型字段通常用于存储不需要分词的精确值,比如产品编号、类别名称等。这些字段会被完整地存储和索引,适合用于过滤、排序和聚合操作。例如,定义一个产品编号字段:
{
    "product_id": {
        "type": "keyword"
    }
}

当我们存储一个产品文档 {"product_id": "P001"} 时,P001 这个值会被完整地存储在索引中,在进行过滤查询 product_id:P001 时,能快速定位到对应的文档。 3. 数值类型:ElasticSearch 支持多种数值类型,如 byteshortintegerlongfloatdouble 等。数值类型字段主要用于存储数字,并且可以进行数值相关的查询和聚合操作。例如,定义一个商品价格字段:

{
    "price": {
        "type": "float"
    }
}

我们可以对这个 price 字段进行范围查询,如查询价格在 100 到 200 之间的商品:price:[100 TO 200]

  1. 日期类型:日期类型字段用于存储日期和时间信息。ElasticSearch 支持多种日期格式,并且可以方便地进行日期范围查询和时间序列分析。例如,定义一个订单创建时间字段:
{
    "order_created": {
        "type": "date"
    }
}

存储时,可以使用常见的日期格式,如 2023-10-01 或者 2023-10-01T12:00:00Z。查询时,可以查询某个时间段内创建的订单,如 order_created:[2023-01-01 TO 2023-12-31]

动态映射与字段管理

ElasticSearch 具有动态映射(Dynamic Mapping)的特性,当我们向一个新的索引中添加文档时,如果文档中的字段在索引映射中不存在,ElasticSearch 会自动根据字段值推断其类型并添加到映射中。例如,我们向一个空索引 my_index 中添加如下文档:

{
    "name": "John Doe",
    "age": 30
}

ElasticSearch 会自动创建如下的映射:

{
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "fields": {
                    "keyword": {
                        "type": "keyword",
                        "ignore_above": 256
                    }
                }
            },
            "age": {
                "type": "integer"
            }
        }
    }
}

虽然动态映射很方便,但在生产环境中,我们通常需要对字段进行更精确的控制,以确保数据的一致性和性能。我们可以通过手动定义映射来管理字段。例如,创建一个包含特定字段映射的索引:

PUT my_index
{
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "standard",
                "store": true
            },
            "age": {
                "type": "integer",
                "store": true
            }
        }
    }
}

在这个映射中,我们明确指定了 name 字段为文本类型,使用 standard 分析器,并存储原始值;age 字段为整数类型并存储原始值。

存储字段的配置与优化

存储策略选择

在 ElasticSearch 中,我们需要根据业务需求来选择合适的存储策略。对于一些不需要进行全文搜索,只需要进行精确匹配和聚合的字段,我们可以选择使用关键词类型并合理设置存储选项。例如,对于一个电商平台的商品类别字段,我们可以这样定义:

{
    "mappings": {
        "properties": {
            "category": {
                "type": "keyword",
                "store": true,
                "eager_global_ordinals": true
            }
        }
    }
}

这里设置 eager_global_ordinalstrue,可以在索引构建时就创建全局序数,这对于基于该字段的聚合操作非常有帮助,能显著提高聚合性能。

对于需要进行全文搜索的文本字段,我们要根据文本内容的特点选择合适的分析器。例如,如果文本是中文,使用 ik 分析器可能会比默认的 standard 分析器更合适。以创建一个包含中文文本字段的索引为例:

PUT chinese_index
{
    "settings": {
        "analysis": {
            "analyzer": {
                "ik_analyzer": {
                    "type": "ik_max_word"
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "content": {
                "type": "text",
                "analyzer": "ik_analyzer",
                "store": true
            }
        }
    }
}

这样,在存储中文文本时,ik_max_word 分析器会将文本更合理地分词,提高搜索的准确性。

字段存储开销与优化

每个存储字段都会占用一定的磁盘空间,尤其是当我们存储大量文档和字段时,存储开销会变得很显著。为了优化存储开销,我们可以采取以下措施:

  1. 合理设置 store 选项:对于不需要直接获取原始值的字段,尽量不设置 storetrue。例如,对于一些只用于搜索但不需要在查询结果中返回原始值的文本字段,就可以不存储。比如一个日志索引中的日志详情字段,我们主要用于搜索特定关键词,而不需要每次查询都返回完整的日志详情:
{
    "mappings": {
        "properties": {
            "log_detail": {
                "type": "text",
                "store": false
            }
        }
    }
}
  1. 使用字段数据类型优化:选择合适的数据类型可以减少存储开销。例如,如果我们知道某个数值字段的范围不会超过 byte 类型的范围(-128 到 127),就可以定义为 byte 类型,而不是使用更大的 integer 类型。
{
    "mappings": {
        "properties": {
            "status_code": {
                "type": "byte"
            }
        }
    }
}
  1. 文档设计优化:避免在文档中存储过多不必要的字段。例如,对于一些历史数据,某些字段可能在后续的查询和分析中不再使用,我们可以考虑将这些字段从文档中移除或者单独存储在其他地方。

存储字段的查询与检索

基本查询操作

  1. 匹配查询:匹配查询是最常见的查询方式之一,用于在文本字段中查找包含特定词项的文档。例如,在一个博客文章索引中,查询标题包含 “ElasticSearch” 的文章:
GET blog_index/_search
{
    "query": {
        "match": {
            "title": "ElasticSearch"
        }
    }
}
  1. 精确匹配查询:对于关键词类型字段,我们可以使用精确匹配查询。例如,在一个员工索引中,查询员工编号为 “E001” 的员工信息:
GET employee_index/_search
{
    "query": {
        "term": {
            "employee_id": "E001"
        }
    }
}
  1. 范围查询:范围查询适用于数值类型和日期类型字段。例如,在一个销售记录索引中,查询销售额在 1000 到 5000 之间的记录:
GET sales_index/_search
{
    "query": {
        "range": {
            "amount": {
                "gte": 1000,
                "lte": 5000
            }
        }
    }
}

对于日期类型字段,查询 2023 年 1 月 1 日之后创建的订单:

GET order_index/_search
{
    "query": {
        "range": {
            "order_created": {
                "gte": "2023-01-01"
            }
        }
    }
}

复合查询操作

  1. 布尔查询:布尔查询允许我们组合多个查询条件,通过 must(必须满足)、should(应该满足)、must_not(必须不满足)等子句来构建复杂的查询逻辑。例如,在一个产品索引中,查询价格大于 100 且类别为 “electronics” 的产品,并且品牌为 “Apple” 更好(但不是必须):
GET product_index/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "range": {
                        "price": {
                            "gt": 100
                        }
                    }
                },
                {
                    "term": {
                        "category": "electronics"
                    }
                }
            ],
            "should": [
                {
                    "term": {
                        "brand": "Apple"
                    }
                }
            ]
        }
    }
}
  1. 嵌套查询:当文档中包含嵌套类型字段时,我们需要使用嵌套查询来处理。例如,一个电商订单文档包含多个订单项,每个订单项是一个嵌套文档,查询订单项中产品名称为 “iPhone 14” 的订单:
{
    "mappings": {
        "properties": {
            "order_items": {
                "type": "nested",
                "properties": {
                    "product_name": {
                        "type": "keyword"
                    }
                }
            }
        }
    }
}
GET order_index/_search
{
    "query": {
        "nested": {
            "path": "order_items",
            "query": {
                "term": {
                    "order_items.product_name": "iPhone 14"
                }
            }
        }
    }
}

存储字段的聚合分析

聚合操作基础

  1. 桶聚合:桶聚合是将文档按照特定条件进行分组的操作,类似于 SQL 中的 GROUP BY。例如,在一个博客文章索引中,按照文章类别进行分组,统计每个类别下的文章数量:
GET blog_index/_search
{
    "size": 0,
    "aggs": {
        "category_count": {
            "terms": {
                "field": "category"
            }
        }
    }
}

这里 size: 0 表示我们只关心聚合结果,不返回具体的文档。terms 聚合会根据 category 字段的值将文档分成不同的桶,每个桶包含属于该类别的文档,并统计每个桶中的文档数量。 2. 指标聚合:指标聚合用于对桶内的文档进行统计计算,如平均值、总和、最大值、最小值等。例如,在一个销售记录索引中,计算每个销售人员的总销售额:

GET sales_index/_search
{
    "size": 0,
    "aggs": {
        "salesperson_total": {
            "terms": {
                "field": "salesperson"
            },
            "aggs": {
                "total_amount": {
                    "sum": {
                        "field": "amount"
                    }
                }
            }
        }
    }
}

这里先通过 terms 聚合按照 salesperson 字段将文档分组,然后在每个分组内使用 sum 指标聚合计算 amount 字段的总和。

嵌套聚合与复杂分析

  1. 嵌套聚合:我们可以在桶聚合中嵌套指标聚合,也可以在指标聚合中嵌套桶聚合,以实现更复杂的分析。例如,在一个电商产品索引中,先按照产品类别分组,然后在每个类别中计算平均价格,并找出价格最高的产品:
GET product_index/_search
{
    "size": 0,
    "aggs": {
        "category_analysis": {
            "terms": {
                "field": "category"
            },
            "aggs": {
                "avg_price": {
                    "avg": {
                        "field": "price"
                    }
                },
                "max_price_product": {
                    "top_hits": {
                        "sort": [
                            {
                                "price": {
                                    "order": "desc"
                                }
                            }
                        ],
                        "size": 1
                    }
                }
            }
        }
    }
}
  1. 管道聚合:管道聚合用于对其他聚合的结果进行二次处理。例如,在按照产品类别统计销售额后,计算每个类别销售额占总销售额的比例:
GET product_index/_search
{
    "size": 0,
    "aggs": {
        "category_sales": {
            "terms": {
                "field": "category"
            },
            "aggs": {
                "category_total": {
                    "sum": {
                        "field": "sales_amount"
                    }
                }
            }
        },
        "total_sales": {
            "sum": {
                "field": "sales_amount"
            }
        },
        "category_percentage": {
            "bucket_script": {
                "buckets_path": {
                    "category_total": "category_sales>category_total",
                    "total": "total_sales"
                },
                "script": "params.category_total / params.total * 100"
            }
        }
    }
}

这里先通过 terms 聚合和 sum 指标聚合计算每个类别和总的销售额,然后使用 bucket_script 管道聚合计算每个类别销售额占总销售额的百分比。

存储字段的更新与维护

字段更新操作

  1. 全文档更新:在 ElasticSearch 中,更新文档最直接的方式是全文档更新。我们可以通过 PUT 操作更新整个文档,例如,更新一个员工文档的年龄字段:
PUT employee_index/_doc/1
{
    "name": "John Doe",
    "age": 31
}

这里假设员工文档的 ID 为 1,通过 PUT 操作将整个文档内容替换为新的内容,包含更新后的年龄字段。需要注意的是,这种方式会重新索引整个文档,开销较大。 2. 部分更新:为了减少更新开销,我们可以使用部分更新操作。例如,只更新员工文档的薪资字段:

POST employee_index/_update/1
{
    "doc": {
        "salary": 5000
    }
}

这里使用 POST 请求的 _update 端点,通过 doc 字段指定需要更新的部分内容。ElasticSearch 会在内部将新的部分内容合并到原文档中,然后重新索引,相比全文档更新,这种方式开销较小。

字段维护与管理

  1. 映射更新:在 ElasticSearch 中,更新映射是一个相对复杂的操作,因为 ElasticSearch 不允许直接修改现有字段的类型。如果我们需要修改字段类型,通常需要创建一个新的索引,将数据从旧索引迁移到新索引。例如,我们想将一个文本字段 description 从默认的文本类型改为使用 keyword 类型,并且设置 storetrue
    • 创建新索引
PUT new_index
{
    "mappings": {
        "properties": {
            "description": {
                "type": "keyword",
                "store": true
            }
        }
    }
}
- **迁移数据**:可以使用 ElasticSearch 的 `reindex` API 将数据从旧索引迁移到新索引:
POST _reindex
{
    "source": {
        "index": "old_index"
    },
    "dest": {
        "index": "new_index"
    }
}
  1. 字段删除:虽然 ElasticSearch 没有直接删除字段的操作,但我们可以通过更新映射来达到类似效果。例如,我们想删除一个字段 unused_field,可以通过更新映射将该字段设置为 null,然后重新索引文档:
POST old_index/_update_by_query
{
    "script": {
        "source": "ctx._source.unused_field = null"
    }
}

然后,在新的映射中不再包含 unused_field 字段。这样,当我们重新索引文档时,unused_field 字段将不再存在。

通过以上对 ElasticSearch 存储字段的全面介绍,包括字段的类型、存储策略、查询检索、聚合分析以及更新维护等方面,希望能帮助读者深入理解并有效地管理 ElasticSearch 中的存储字段,从而更好地构建高效的搜索和分析应用。