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

基于 ElasticSearch 的查询和过滤优化策略

2021-01-213.0k 阅读

ElasticSearch 基础查询与过滤机制概述

基础查询类型

ElasticSearch 提供了丰富的查询类型,以满足不同场景下的数据检索需求。其中,match 查询是最常用的文本查询之一。例如,假设我们有一个包含博客文章的索引,文章字段为 content。若要搜索包含 “大数据” 相关内容的文章,可以使用以下简单的 match 查询:

{
    "query": {
        "match": {
            "content": "大数据"
        }
    }
}

这种查询会对查询词进行分词处理,然后在指定字段中查找匹配的词项。与之相对的是 match_phrase 查询,它用于精确匹配短语。例如,若要搜索 “ElasticSearch 优化” 这个确切的短语:

{
    "query": {
        "match_phrase": {
            "content": "ElasticSearch 优化"
        }
    }
}

match_phrase 会确保查询的词项以相同顺序紧邻出现。

另外,term 查询主要用于精确匹配,通常用于结构化数据,如数字、日期、枚举类型等。假设我们有一个表示文章发布状态的字段 status,取值为 “published” 或 “draft”,若要查询已发布的文章:

{
    "query": {
        "term": {
            "status": "published"
        }
    }
}

过滤机制

过滤在 ElasticSearch 中用于缩小搜索范围,与查询不同,过滤通常不计算相关性分数,仅判断文档是否符合条件。filter 子句可以嵌套在各种查询类型中。例如,结合上面的博客文章索引,若要搜索已发布且包含 “大数据” 内容的文章,可以这样写:

{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "content": "大数据"
                }
            },
            "filter": {
                "term": {
                    "status": "published"
                }
            }
        }
    }
}

这里通过 bool 查询的 filter 子句,应用了 term 过滤,只返回状态为 “published” 的文档,同时 must 子句中的 match 查询确保文档内容包含 “大数据”。

range 过滤也是常用的过滤类型,用于筛选在某个范围内的值。比如,若文章有一个 publish_date 字段表示发布日期,要查询 2023 年 1 月 1 日之后发布的文章:

{
    "query": {
        "bool": {
            "filter": {
                "range": {
                    "publish_date": {
                        "gte": "2023-01-01"
                    }
                }
            }
        }
    }
}

gte 表示大于等于,类似的还有 lte(小于等于)、gt(大于)和 lt(小于)。

基于数据建模的优化策略

字段类型选择与优化

在 ElasticSearch 中,正确选择字段类型对查询和过滤性能至关重要。例如,对于数值类型的数据,应选择合适的数值类型,如 long 用于较大整数,floatdouble 用于浮点数。以电商产品价格字段为例,如果价格通常是整数且范围较大,选择 long 类型会更合适。在定义映射时:

{
    "mappings": {
        "properties": {
            "price": {
                "type": "long"
            }
        }
    }
}

这样在进行价格相关的过滤时,如查询价格大于 100 的产品:

{
    "query": {
        "bool": {
            "filter": {
                "range": {
                    "price": {
                        "gt": 100
                    }
                }
            }
        }
    }
}

由于字段类型合适,查询执行效率会更高。

对于文本字段,要根据实际需求选择是否分词。如果字段主要用于精确匹配,如产品型号字段,应设置 indexnot_analyzed(在 ElasticSearch 5.0 之后使用 keyword 类型)。例如:

{
    "mappings": {
        "properties": {
            "product_model": {
                "type": "keyword"
            }
        }
    }
}

这样在查询特定型号产品时:

{
    "query": {
        "term": {
            "product_model": "XYZ123"
        }
    }
}

可以直接进行精确匹配,避免分词带来的性能损耗。

嵌套与父子关系优化

当数据存在嵌套结构或父子关系时,合理设计有助于优化查询。对于嵌套关系,例如电商订单中包含多个订单项,订单项是订单的一部分,但每个订单项又有自己的属性(如产品名称、数量、价格等)。可以使用嵌套类型来处理这种关系。定义映射如下:

{
    "mappings": {
        "properties": {
            "order_items": {
                "type": "nested",
                "properties": {
                    "product_name": {
                        "type": "text"
                    },
                    "quantity": {
                        "type": "long"
                    },
                    "price": {
                        "type": "float"
                    }
                }
            }
        }
    }
}

在查询时,若要查找包含特定产品的订单:

{
    "query": {
        "nested": {
            "path": "order_items",
            "query": {
                "match": {
                    "order_items.product_name": "手机"
                }
            }
        }
    }
}

通过 nested 查询,ElasticSearch 可以在嵌套文档内部进行独立的查询和过滤,确保准确性和性能。

对于父子关系,比如论坛帖子和回复,帖子是父文档,回复是子文档。可以通过设置父子关系映射来处理:

{
    "mappings": {
        "parent_type": {
            "properties": {
                "title": {
                    "type": "text"
                }
            }
        },
        "child_type": {
            "properties": {
                "content": {
                    "type": "text"
                }
            },
            "_parent": {
                "type": "parent_type"
            }
        }
    }
}

查询时,若要查找某个帖子的所有回复:

{
    "query": {
        "has_parent": {
            "parent_type": "parent_type",
            "query": {
                "match": {
                    "title": "ElasticSearch 优化讨论"
                }
            }
        }
    }
}

这种父子关系设计使得查询能够快速定位相关子文档,提高查询效率。

查询语法优化策略

Bool 查询的合理使用

bool 查询是 ElasticSearch 中功能强大且灵活的查询类型,它可以组合多个查询条件,通过 must(必须满足)、should(应该满足)、must_not(必须不满足)和 filter 子句来控制逻辑。在复杂查询场景下,合理组织这些子句对性能影响很大。

例如,在电商搜索中,若要查找价格在 100 到 500 之间且品牌为 “Apple” 的产品,同时产品评分大于 4 分的产品也应包含在结果中,可以这样构造 bool 查询:

{
    "query": {
        "bool": {
            "must": [
                {
                    "range": {
                        "price": {
                            "gte": 100,
                            "lte": 500
                        }
                    }
                },
                {
                    "term": {
                        "brand": "Apple"
                    }
                }
            ],
            "should": {
                "range": {
                    "rating": {
                        "gt": 4
                    }
                }
            }
        }
    }
}

这里通过 must 子句确保价格和品牌条件必须满足,而 should 子句使得评分大于 4 分的产品也会被包含在结果中。注意,should 子句在没有 must 子句时,只要有一个 should 条件满足即可返回文档。

在使用 bool 查询时,应尽量将过滤条件放在 filter 子句中,因为 filter 子句不计算相关性分数,执行效率更高。例如,若要查找已发布且创建时间在特定范围内的博客文章,同时文章内容包含 “ElasticSearch”:

{
    "query": {
        "bool": {
            "must": {
                "match": {
                    "content": "ElasticSearch"
                }
            },
            "filter": [
                {
                    "term": {
                        "status": "published"
                    }
                },
                {
                    "range": {
                        "create_date": {
                            "gte": "2023-01-01",
                            "lte": "2023-12-31"
                        }
                    }
                }
            ]
        }
    }
}

多字段查询优化

在实际应用中,经常需要在多个字段中进行查询。例如,在电商产品搜索中,可能需要在产品名称、描述和品牌字段中搜索用户输入的关键词。multi_match 查询提供了一种方便的方式来实现多字段查询。基本的 multi_match 查询如下:

{
    "query": {
        "multi_match": {
            "query": "手机",
            "fields": ["product_name", "description", "brand"]
        }
    }
}

然而,默认的 multi_match 查询策略在某些情况下可能无法满足需求。例如,可能希望对不同字段设置不同的权重,因为产品名称可能比描述更重要。可以通过 boost 参数来设置权重:

{
    "query": {
        "multi_match": {
            "query": "手机",
            "fields": ["product_name^3", "description", "brand"]
        }
    }
}

这里 product_name^3 表示产品名称字段的权重是其他字段的 3 倍,这样在计算相关性分数时,产品名称匹配的文档会得到更高的分数,更有可能排在前面。

另外,multi_match 查询还支持不同的 type,如 best_fieldsmost_fieldscross_fieldsbest_fields 类型会在每个字段中分别查询,然后取相关性分数最高的字段的分数作为文档的最终分数。most_fields 类型会将每个字段的相关性分数相加,得到文档的最终分数。cross_fields 类型则会将所有字段视为一个大的文本块进行分词和查询,适用于多个字段包含相似语义信息的情况。

例如,若希望在多个字段中查找关键词,并且希望尽可能多的字段匹配,可使用 most_fields 类型:

{
    "query": {
        "multi_match": {
            "query": "手机",
            "fields": ["product_name", "description", "brand"],
            "type": "most_fields"
        }
    }
}

通过合理选择 multi_match 的类型和参数,可以优化多字段查询的准确性和性能。

索引优化策略

索引分片与副本优化

ElasticSearch 将索引划分为多个分片,每个分片可以分布在不同的节点上,以实现水平扩展和高可用性。合理设置分片数量对查询性能至关重要。如果分片数量过多,会增加索引管理的开销,每个分片的数据量过小,也会影响查询性能,因为查询时需要合并多个分片的结果。

在创建索引时,可以指定分片数量。例如,创建一个包含 5 个分片和 1 个副本的索引:

PUT /my_index
{
    "settings": {
        "number_of_shards": 5,
        "number_of_replicas": 1
    }
}

一般来说,对于数据量较小的索引,2 - 3 个分片可能就足够了;对于大数据量的索引,需要根据数据增长趋势和硬件资源进行评估。可以通过监控集群状态来查看分片的负载情况:

GET /_cluster/health

如果发现某个分片负载过高,可以考虑重新分配分片或增加节点。

副本用于提高数据的可用性和读取性能。增加副本数量可以提高读取的并行度,因为查询可以从多个副本中获取数据。但过多的副本会占用更多的磁盘空间和网络带宽,因为副本需要与主分片保持同步。在生产环境中,通常设置 1 - 2 个副本即可满足大多数需求。

索引刷新与合并策略

索引刷新(refresh)是将内存中的数据写入磁盘并使数据可搜索的过程。默认情况下,ElasticSearch 每秒自动刷新一次索引。虽然这使得数据近乎实时可搜索,但频繁的刷新会带来性能开销。在批量导入数据时,可以手动控制刷新频率。例如,在批量导入之前,先关闭自动刷新:

PUT /my_index/_settings
{
    "refresh_interval": -1
}

然后在数据导入完成后,再恢复自动刷新:

PUT /my_index/_settings
{
    "refresh_interval": "1s"
}

这样可以减少刷新次数,提高导入性能。

索引合并(merge)是将多个较小的段合并为一个较大段的过程,有助于减少段的数量,提高查询性能。ElasticSearch 会自动执行合并操作,但可以通过一些参数来调整合并策略。例如,index.merge.policy.max_merge_at_once 参数控制一次合并的最大段数,index.merge.policy.floor_segment 参数控制最小的段大小,低于这个大小的段会优先合并。

可以通过设置 index.merge.scheduler.max_thread_count 参数来控制合并线程数。例如,将合并线程数设置为 2:

PUT /my_index/_settings
{
    "index.merge.scheduler.max_thread_count": 2
}

合理调整这些参数可以在不影响系统性能的前提下,优化索引合并过程,提高查询效率。

缓存优化策略

字段数据缓存

ElasticSearch 使用字段数据缓存(field data cache)来加速对某些字段的聚合、排序和脚本操作。字段数据缓存存储在 JVM 堆内存中,对于经常用于聚合或排序的字段,启用字段数据缓存可以显著提高性能。

例如,在电商产品索引中,若经常根据价格进行聚合或排序,可以确保价格字段启用了字段数据缓存。默认情况下,数字类型字段会自动启用字段数据缓存,但对于文本字段,需要在映射中显式设置:

{
    "mappings": {
        "properties": {
            "product_name": {
                "type": "text",
                "fielddata": true
            }
        }
    }
}

注意,启用字段数据缓存会占用更多的堆内存,因此需要根据实际情况评估。可以通过监控 JVM 堆内存使用情况来确定是否需要调整字段数据缓存的使用。

过滤器缓存

过滤器缓存(filter cache)用于缓存过滤结果,避免重复执行相同的过滤操作。ElasticSearch 会自动管理过滤器缓存,当相同的过滤条件再次执行时,会从缓存中获取结果,而不是重新计算。

例如,在一个包含大量用户数据的索引中,经常根据用户所在地区进行过滤查询。如果该过滤条件被频繁使用,ElasticSearch 会将过滤结果缓存起来,下次查询相同地区的用户时,直接从缓存中获取结果,提高查询效率。

可以通过设置 index.cache.filter.type 参数来选择过滤器缓存的类型,默认是 LRU(最近最少使用)。如果希望使用基于时间的缓存策略,可以设置为 time,并通过 index.cache.filter.expire 参数设置缓存过期时间。例如,设置过滤器缓存过期时间为 1 小时:

PUT /my_index/_settings
{
    "index.cache.filter.type": "time",
    "index.cache.filter.expire": "1h"
}

这样可以在一定时间内保持缓存的有效性,同时避免缓存数据长时间占用内存。

性能监控与调优工具

ElasticSearch 内置监控 API

ElasticSearch 提供了一系列内置的监控 API,用于获取集群、索引和节点的性能指标。通过这些 API,可以实时了解系统的运行状态,发现性能瓶颈。

例如,/_cat/indices API 可以查看所有索引的基本信息,包括文档数量、存储大小等:

GET /_cat/indices

/_cluster/health API 用于获取集群的健康状态,包括集群状态(green、yellow、red)、节点数量、分片数量等:

GET /_cluster/health

/_nodes/stats API 可以获取每个节点的统计信息,如 CPU 使用率、内存使用情况、索引和搜索相关的指标等:

GET /_nodes/stats

通过定期调用这些 API,并分析返回的数据,可以及时发现性能问题。例如,如果发现某个节点的 CPU 使用率持续过高,可能需要进一步分析是哪些查询或操作导致的。

Kibana 监控与可视化

Kibana 是 ElasticSearch 的官方可视化工具,它与 ElasticSearch 紧密集成,提供了丰富的监控和可视化功能。通过 Kibana 的监控仪表盘,可以直观地查看集群、索引和节点的性能指标,并且可以进行趋势分析。

在 Kibana 中,可以创建自定义的监控面板,将关注的指标以图表、表格等形式展示出来。例如,可以创建一个面板来展示索引的文档增长趋势、查询响应时间的变化等。同时,Kibana 还支持告警功能,可以设置阈值,当性能指标超出阈值时,自动发送告警通知。

例如,通过设置查询响应时间的阈值,如果平均响应时间超过 500 毫秒,Kibana 可以通过邮件或其他方式发送告警,通知管理员及时处理性能问题。

第三方性能分析工具

除了 ElasticSearch 内置的监控 API 和 Kibana,还有一些第三方性能分析工具可以帮助优化查询和过滤性能。例如,ESHQ(ElasticSearch Head Query)是一个基于浏览器的工具,它提供了直观的界面来构建和执行 ElasticSearch 查询,并分析查询性能。

通过 ESHQ,可以可视化查询的执行计划,查看每个阶段的耗时,从而找到性能瓶颈。它还支持对查询进行优化建议,例如提示是否可以使用更合适的查询类型或调整查询参数。

另外,Elasticsearch-SQL 工具允许使用 SQL 语法来查询 ElasticSearch 数据,这对于熟悉 SQL 的开发人员来说更加方便。同时,它也提供了一些性能分析功能,帮助优化基于 SQL 的查询。通过这些第三方工具与 ElasticSearch 内置工具相结合,可以更全面地进行性能监控和调优。