跳到主要内容

Elasticsearch的高阶语法与分片策略:从概念到实战

· 阅读需 6 分钟
季冠臣
后端研发工程师

作为后端开发者,我们对 Elasticsearch(后文简称 ES)绝不陌生。它是一个强大的分布式搜索引擎,我们常常用它来做日志分析、全文检索等。但很多时候,我们可能只是停留在"会用"的层面:创建索引、插入数据、用 match 查询。当数据量和服务规模上来后,各种问题便接踵而至:"为什么我的查询这么慢?"、"为什么集群CPU突然就满了?"、"分片数到底设多少合适?"

如果你也遇到过这些问题,那么这篇文章就是为你准备的。我们将深入探讨那些能让你的 ES 使用水平提升一个台阶的核心知识:高阶查询语法与分片策略。这不仅仅是"知其然",更是"知其所以然"的过程,帮助你从根源上理解并解决性能问题。

拒绝"慢查询":高阶语法精讲

ES 的查询 DSL (Domain Specific Language) 功能极其丰富,远不止 matchterm。掌握更高阶的语法,是写出高效、精准查询的第一步。

1. bool 查询:must vs filter 的小剧场

我们都用过 bool 查询来组合多个条件,但 mustfilter 的区别你真的清楚吗?

  • must: 条件必须匹配,并且 会计算相关性得分 _score。它会问:"这个文档有多符合条件?"
  • filter: 条件必须匹配,但它处于"过滤器上下文(filter context)"中,不会计算得分,并且会利用缓存。它只回答:"是"或"不是"。

实战场景:假设我们在电商网站搜索"华为手机",要求必须是"5G"并且"8GB内存以上"的。

一个不够优化的查询可能是:

{
"query": {
"bool": {
"must": [
{ "match": { "title": "华为手机" } },
{ "term": { "network_type": "5G" } },
{ "range": { "memory": { "gte": 8 } } }
]
}
}
}

这里,ES 不仅要找"5G"和"8GB内存"的手机,还要煞有介事地为"5G"和"8GB内存"这两个条件计算相关性得分,这完全是多余的计算。

更优的查询应该是:

{
"query": {
"bool": {
"must": [
{ "match": { "title": "华为手机" } }
],
"filter": [
{ "term": { "network_type": "5G" } },
{ "range": { "memory": { "gte": 8 } } }
]
}
}
}

【划重点】:将所有不需要计算相关性得分的精确匹配、范围查询等条件,统统放进 filter 子句。这能让 ES 大胆地使用缓存,性能提升非常显著。这是 ES 查询优化的第一黄金法则。

2. 聚合(Aggregations):不只是搜索,更是分析

很多人以为 ES 只是一个"搜索框",但它强大的聚合能力让它拥有了轻量级数据分析的能力。

实战场景:我们想知道网站上每个品牌的手机平均售价是多少。

{
"size": 0, // 我不关心搜索结果,只关心聚合数据
"aggs": {
"brands": { // 给这个聚合起个名字叫 "brands"
"terms": { "field": "brand.keyword" }, // 按品牌进行分组(桶聚合)
"aggs": {
"avg_price": { // 在每个品牌的分组下,再进行子聚合
"avg": { "field": "price" } // 计算价格的平均值(度量聚合)
}
}
}
}
}

这个查询会返回类似这样的结果:

"aggregations" : {
"brands" : {
"buckets" : [
{
"key" : "华为",
"doc_count" : 150,
"avg_price" : { "value" : 4999.50 }
},
{
"key" : "小米",
"doc_count" : 200,
"avg_price" : { "value" : 2999.00 }
}
]
}
}

你看,我们没有写一行后端代码,就完成了一个简单的统计分析。聚合是 ES 的精髓之一,善用它可以极大地减轻后端服务的压力。

集群的根基:分片策略

如果说查询语法是"术",那么分片策略就是"道"。错误的策略会给整个集群带来灾难性的、且难以挽回的后果。

1. 什么是分片和副本?

  • 主分片 (Primary Shard): 每个索引被分成一个或多个"部分",这就是主分片。每个主分片都是一个功能完备的 Lucene 索引。一个索引的主分片数量在创建时设定,后续无法修改!
  • 副本分片 (Replica Shard): 它是主分片的一个完整拷贝。它的作用有两个:
    1. 高可用:当主分片所在的节点宕机,副本分片可以被提升为新的主分片,保证数据不丢失。
    2. 提升读性能:搜索请求可以同时在主分片和副本分片上执行,提升并发查询能力。

2. 主分片数,该如何设定?

这是个经典问题,也是最重要的问题。设多了,每个分片都很小,会造成元数据管理的开销和查询时的额外协调成本;设少了,单个分片过大,会导致数据迁移困难、恢复缓慢。

【经验法则】: 官方和社区的普遍建议是,让单个分片的大小保持在 10GB 到 50GB 之间

所以,你可以这样规划:

  1. 估算你一个索引未来一到两年内可能增长到的总数据量(例如,500GB)。
  2. 选择一个合适的目标分片大小(例如,30GB)。
  3. 计算所需主分片数:500GB / 30GB/shard ≈ 17 shards

所以,你可以在创建索引时将主分片数设为 17。当然,这只是一个起点,你还需要根据你的业务场景(读写比例、数据增长速度)进行微调。

3. 自定义路由 (Custom Routing)

默认情况下,ES 使用文档的 _id 来决定它应该被存到哪个分片。但在某些场景下,我们可以手动控制。

实战场景:一个多租户的 SaaS 应用,每个用户的数据都存在同一个索引里。当一个用户查询自己的数据时,ES 不得不查询所有分片,然后过滤出属于该用户的数据,效率很低。

这时,我们可以用 user_id 作为路由键:

PUT /my-index/_doc/1?routing=user_A
{
"user_id": "user_A",
"message": "some message"
}

在查询时,也带上同样的 routing 参数:

GET /my-index/_search?routing=user_A
{
"query": {
"match": { "message": "some" }
}
}

这样,ES 就能精准地只去 user_A 的数据所在的那一个分片上执行查询,避免了不必要的跨分片搜索,性能会得到质的飞跃。

总结

我们今天聊的,都是 ES 从"能用"到"好用"乃至"卓越"的关键。

  • 语法层面:永远记得把非相关性的查询放进 filter 上下文,善用 aggregations 为你的系统赋能。
  • 架构层面:谨慎规划你的主分片数量,把它当成一次数据库的 CREATE TABLE 操作来对待。并根据场景,考虑是否需要自定义路由来打破性能瓶颈。

当然,ES 的世界远不止于此,但掌握了这些,你已经超越了 80% 的使用者。剩下的,就是在实践中不断监控、测试、调优,真正成为驾驭数据的专家。

浏览量:加载中...