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. 精确匹配时使用 term
或 terms
代替 match
match
查询会进行分词处理,而 term
或 terms
查询是精确匹配。
优化点:
对于不应分词的字段(如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. 使用 prefix
或 wildcard
替代 regexp
(当可能时)
正则查询 (regexp
) 是最慢的字符串匹配方式之一,因为它需要遍历大量数据并进行复杂的模式匹配。
优化点:
- 对于前缀匹配,优先使用
prefix
查询。它比wildcard
和regexp
更高效,因为它只匹配特定前缀的文档。 - 对于简单通配符(例如
*
和?
),可以考虑使用wildcard
。但要注意,如果通配符在词条开头,性能会很差。 - 避免在
regexp
和wildcard
查询的模式开头使用通配符 (如*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. 避免使用通配符开头的模糊查询
如上所述,以 *
或 ?
开头的 wildcard
或 regexp
查询会强制Elasticsearch扫描所有词条,因为无法利用倒排索引的有序性。这会导致非常差的性能,尤其是在大型数据集上。
优化点:
- 如果确实需要模糊匹配,可以考虑使用
ngram
或edge_ngram
分词器在索引时生成N-gram词条,然后在查询时使用match
或term
查询这些N-gram词条。 - 或者,如果模糊匹配的场景不多,可以限制其使用范围,或者考虑使用更专业的全文搜索技术。
5. 使用 exists
和 missing
代替范围查询
如果您只需要检查某个字段是否存在,而不是关心其具体值,使用 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
字段使用 term
或 match
查询,直接使用 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_id
或 tenant_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
查询的 must
、filter
、should
、must_not
可以构建出高效复杂的查询。
优化点:
- 优先将可缓存且不计分的条件放入
filter
: 这是最基本的优化原则。 - 减少
should
数量:should
查询会尝试匹配所有条件并计算评分,当should
条件过多时,性能会下降。如果只要求满足任意一个条件且不需要评分,可以考虑用terms
放在filter
中。 - 对
must
和should
善用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
类型。 - 数值和日期类型应使用对应的
integer
、long
、float
、double
、date
类型,而不是text
或keyword
。
- 字符串如果不需要全文搜索,只用于精确匹配、排序或聚合,请使用
示例:
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 // 如果此字段永不查询,则不索引
}
}
}
}