1. 使用 filter 代替 must (在可能的情况下)

在Elasticsearch中,query 上下文和 filter 上下文的处理方式不同。

  • query 上下文: 用于计算相关性得分 (_score),会执行分词,过程相对耗时。
  • filter 上下文: 不计算相关性得分,只做简单的匹配判断,并且结果会被缓存。

优化点:
当您只需要判断文档是否符合某个条件,而不需要根据该条件进行排序时,应优先使用 filter。这在聚合、范围查询、精确匹配等场景尤为适用。

示例:

// 不推荐,如果不需要评分
GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "term": { "status": "active" } } // 此处会计算评分
      ]
    }
  }
}

// 推荐,使用 filter,不计算评分,且可缓存
GET /my_index/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "active" } } // 此处不计算评分,结果可缓存
      ]
    }
  }
}

2. 精确匹配时使用 termterms 代替 match

match 查询会进行分词处理,而 termterms 查询是精确匹配。

优化点:
对于不应分词的字段(如ID、状态码、标签、枚举值等),使用 term (匹配单个值) 或 terms (匹配多个值) 会比 match 更高效,因为它们避免了不必要的分词过程。确保这些字段的映射类型是 keyword 而不是 text

示例:

// 不推荐,如果 product_id 是 keyword 类型,且是精确匹配
GET /my_index/_search
{
  "query": {
    "match": {
      "product_id": "12345" // 可能会被分词,即使 product_id 是 keyword
    }
  }
}

// 推荐,精确匹配,更高效
GET /my_index/_search
{
  "query": {
    "term": {
      "product_id": "12345" // 精确匹配
    }
  }
}

// 推荐,匹配多个精确值
GET /my_index/_search
{
  "query": {
    "terms": {
      "category_id": ["cat_A", "cat_B"]
    }
  }
}

3. 使用 prefixwildcard 替代 regexp (当可能时)

正则查询 (regexp) 是最慢的字符串匹配方式之一,因为它需要遍历大量数据并进行复杂的模式匹配。

优化点:

  • 对于前缀匹配,优先使用 prefix 查询。它比 wildcardregexp 更高效,因为它只匹配特定前缀的文档。
  • 对于简单通配符(例如 *?),可以考虑使用 wildcard。但要注意,如果通配符在词条开头,性能会很差。
  • 避免在 regexpwildcard 查询的模式开头使用通配符 (如 *keyword),因为这会导致全量扫描倒排索引,性能极差。

示例:

// 不推荐,使用 regexp 进行前缀匹配,效率低
GET /my_index/_search
{
  "query": {
    "regexp": {
      "product_name": "apple.*"
    }
  }
}

// 推荐,使用 prefix 进行前缀匹配,更高效
GET /my_index/_search
{
  "query": {
    "prefix": {
      "product_name": "apple"
    }
  }
}

// 尽量避免的通配符开头的查询
GET /my_index/_search
{
  "query": {
    "wildcard": {
      "product_name": "*phone" // 极其慢
    }
  }
}

4. 避免使用通配符开头的模糊查询

如上所述,以 *? 开头的 wildcardregexp 查询会强制Elasticsearch扫描所有词条,因为无法利用倒排索引的有序性。这会导致非常差的性能,尤其是在大型数据集上。

优化点:

  • 如果确实需要模糊匹配,可以考虑使用 ngramedge_ngram 分词器在索引时生成N-gram词条,然后在查询时使用 matchterm 查询这些N-gram词条。
  • 或者,如果模糊匹配的场景不多,可以限制其使用范围,或者考虑使用更专业的全文搜索技术。

5. 使用 existsmissing 代替范围查询

如果您只需要检查某个字段是否存在,而不是关心其具体值,使用 exists (ES 7.x+) 或 bool + must_not + exists (对于 missing 行为) 比使用 range 查询更高效。

优化点:
当您只需要判断字段存在或不存在时,直接使用专门的查询类型。

示例:

// 不推荐,检查字段是否存在
GET /my_index/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 0 // 仅仅是为了检查 price 字段是否存在
      }
    }
  }
}

// 推荐,检查字段是否存在
GET /my_index/_search
{
  "query": {
    "exists": {
      "field": "price"
    }
  }
}

// 推荐,检查字段是否不存在 (ES 7.x+ 推荐这种方式替代 missing)
GET /my_index/_search
{
  "query": {
    "bool": {
      "must_not": [
        { "exists": { "field": "price" } }
      ]
    }
  }
}

6. 使用 ids 查询优化根据 ID 批量获取

当您知道文档的 _id 时,使用 ids 查询是最高效的批量获取文档的方式。

优化点:
避免对 _id 字段使用 termmatch 查询,直接使用 ids

示例:

// 不推荐
GET /my_index/_search
{
  "query": {
    "terms": {
      "_id": ["id1", "id2", "id3"]
    }
  }
}

// 推荐
GET /my_index/_search
{
  "query": {
    "ids": {
      "values": ["id1", "id2", "id3"]
    }
  }
}

7. 减少返回字段 (_source 过滤)

在很多场景下,我们可能只需要文档中的部分字段,而不是整个 _source

优化点:

通过 _source 过滤,明确指定需要返回的字段,可以显著减少网络传输量和 Elasticsearch 从磁盘读取数据的 I/O 成本,从而加快查询速度。

示例:

JSON

GET /my_index/_search
{
  "_source": [ "title", "author", "publish_date" ], // 只返回这三个字段
  "query": {
    "match": {
      "content": "elasticsearch performance"
    }
  }
}

8. 使用 routing 优化特定场景下的查询

如果您的索引是基于某个字段(例如 user_idtenant_id)进行路由的,那么在查询时指定 routing 参数可以直接将请求发送到包含相关数据的分片上,从而跳过在所有分片上广播查询的步骤。

优化点:

对于使用了自定义路由的索引,查询时带上 routing 参数可以极大地提升查询效率。

示例:

JSON

GET /my_index/_search?routing=user_A // 指定 routing 到 user_A 对应的分片
{
  "query": {
    "term": {
      "user_id": "user_A"
    }
  }
}

9. 优化 sort 字段

排序操作通常涉及大量数据加载和比较。

优化点:

  • 避免对 text 类型字段直接排序: text 字段会经过分词,无法直接用于排序。如果需要对文本内容排序,应该为该字段额外建立一个 keyword 子字段,并对 keyword 子字段进行排序。
  • 利用 doc_values: Elasticsearch 使用 doc_values 来进行排序和聚合。doc_values 是列式存储,专为这些操作优化。确保您用于排序的字段类型支持 doc_values(如 keyword、数值类型、日期类型)。默认情况下,大多数非 text 类型字段都会开启 doc_values
  • _id 排序: 如果按 _id 排序,性能会比较好,因为 _id 内部处理高效。
  • 缓存不足导致的问题: 如果内存不足以缓存 doc_values,可能会导致频繁的磁盘 I/O,影响性能。

示例:

JSON

PUT /my_index
{
  "mappings": {
    "properties": {
      "product_name": {
        "type": "text",
        "fields": {
          "keyword": { // 添加一个 keyword 子字段用于排序或精确匹配
            "type": "keyword"
          }
        }
      },
      "price": {
        "type": "float"
      }
    }
  }
}

GET /my_index/_search
{
  "query": {
    "match": {
      "description": "electronics"
    }
  },
  "sort": [
    { "price": "asc" }, // 对数值字段排序
    { "product_name.keyword": "desc" } // 对 keyword 子字段排序
  ]
}

10. 使用 bool 查询组合优化

合理使用 bool 查询的 mustfiltershouldmust_not 可以构建出高效复杂的查询。

优化点:

  • 优先将可缓存且不计分的条件放入 filter: 这是最基本的优化原则。
  • 减少 should 数量: should 查询会尝试匹配所有条件并计算评分,当 should 条件过多时,性能会下降。如果只要求满足任意一个条件且不需要评分,可以考虑用 terms 放在 filter 中。
  • mustshould 善用 minimum_should_match: 这个参数可以控制 should 子句的匹配数量,有助于平衡召回率和精确度,同时避免不必要的计算。

示例:

JSON

GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "search" } } // 评分
      ],
      "filter": [
        { "term": { "status": "published" } }, // 不评分,可缓存
        { "range": { "publish_date": { "gte": "2023-01-01" } } } // 不评分,可缓存
      ],
      "should": [
        { "match": { "tags": "big data" } },
        { "match": { "tags": "machine learning" } }
      ],
      "minimum_should_match": 1 // 至少匹配一个 tags
    }
  }
}

11. 理解和优化字段映射 (Mapping)

合理的字段映射是查询性能的基础。

优化点:

  • 选择正确的字段类型

    • 字符串如果不需要全文搜索,只用于精确匹配、排序或聚合,请使用 keyword 类型。
    • 数值和日期类型应使用对应的 integerlongfloatdoubledate 类型,而不是 textkeyword

示例:

JSON

PUT /my_index
{
  "mappings": {
    "properties": {
      "user_id": {
        "type": "keyword", // 精确匹配和聚合
        "norms": false // 如果 user_id 不用于评分,可以禁用 norms
      },
      "comment": {
        "type": "text" // 全文搜索
      },
      "last_updated": {
        "type": "date"
      },
      "internal_note": {
        "type": "text",
        "index": false // 如果此字段永不查询,则不索引
      }
    }
  }
}
最后修改:2025 年 06 月 24 日
如果觉得我的文章对你有用,请随意赞赏