专栏名称: Java知音
专注于Java,推送技术文章,热门开源项目等。致力打造一个有实用,有情怀的Java技术公众号!
目录
相关文章推荐
51好读  ›  专栏  ›  Java知音

Elasticsearch最佳生产实践,推荐收藏!

Java知音  · 公众号  · 架构  · 2025-05-09 10:05

主要观点总结

本文主要介绍了Elasticsearch的基础概念、使用规范、注意事项、常见优化以及工具使用。包括Elasticsearch的建索引规范、数据类型对比、索引与搜索效率优化、基础架构评估、分片策略、集群基本信息查看和查询语法等。

关键观点总结

关键观点1: Elasticsearch建索引规范

在建立索引时需搞清楚每个字段存在的用途,并根据字段的不同用途,不同数据类型来匹配合适的Elasticsearch中的数据类型。

关键观点2: 数据类型对比

Elasticsearch中的数据类型包括string、long、integer、short、byte、double、float、half_float、scaled_float、date、boolean等,每种类型的使用场景和存储方式都不同。

关键观点3: 索引与搜索效率优化

包括index.refresh_interval、Use bulk requests、id采用自动生成、按需查询、深度分页等优化策略,提高索引和搜索效率。

关键观点4: 基础架构评估

包括master-eligible node、data node、coordination node、ingest node、machine learning node等节点类型的角色和部署策略。

关键观点5: 分片策略

包括分片数量多少合适、副本分片数量建议、分片大小多少合适以及如何避免单分片过大等策略。

关键观点6: 集群基本信息查看

使用Elasticsearch的cat命令来查看集群中各种运行情况的相关信息,包括cat aliases、cat allocation、cat count等。

关键观点7: 查询语法

包括v表示显示详细信息、help用于输出其他一些列的信息、Numeric formats单位格式化输出、sort排序等查询语法。


正文

请到「今天看啥」查看全文


如果对text类型进行排序,得到的报错信息如下,注意有提到Fielddata默认是关闭的,可以为nickname设置 fielddata=true 这样的属性。

GET /emp/_search
{"sort":[{"nickname":{"order":"desc"}}]}

# 报错信息如下:
{
"error": {
    "root_cause": [
      {
        "type""illegal_argument_exception",
        "reason""Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
      }
    ],
    "type""search_phase_execution_exception",
    "reason""all shards failed",
    "phase""query",
    "grouped"true,
    "failed_shards": [
      {
        "shard": 0,
        "index""emp",
        "node""CKN5Zo86QTmwjnK7NHHNQQ",
        "reason": {
          "type""illegal_argument_exception",
          "reason""Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
        }
      }
    ],
    "caused_by": {
      "type""illegal_argument_exception",
      "reason""Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.",
      "caused_by": {
        "type""illegal_argument_exception",
        "reason""Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead."
      }
    }
  },
"status": 400
}

按照如下方式在构建text类型时,设置 fielddata=true 即可进行排序。

PUT /emp_fielddata/
{
"mappings": {
    "_doc": {
      "properties": {
        "nickname": {
          "type""text",
          "fielddata"true
        }
      }
    }
  }
}

POST /emp_fielddata/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san"}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng"}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng"}


GET /emp_fielddata/_search
{"sort":[{"nickname":{"order":"desc"}}]}

关于fielddata的使用,如官方介绍,它会大量消耗堆内存,并且驻留在内存中的生命周期是跟随segment的,所以生产上应当尽量避免使用。

官方介绍

替代方案

解决方案也很简单了,在一开始的报错信息中已经说的很清楚了,可以用keyword来代替。

Fielddata is disabled on text fields by default. Set fielddata=true on [nickname] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.

当然keyword不会分词,不过为什么一个字段既要分词又要排序或聚合?当你仔细思考使用场景后你会发现这样做通常是没有意义的。

索引与评分
  • 不需要索引的字段,index属性设置为false。
  • 如果不需要计算文档评分,建议将norms设置为false。

根据字段的实际使用情况,确定是否需要索引、文档评分,这样将会减少磁盘存储(每条文档中每个开启了norms的字段需占用1个字节),大多数字段类型默认是开启的。

PUT my_index/_mapping/_doc
{
  "properties": {
    "title": {
      "type""text",
      "norms"false
    }
  }
}
source存储

source是用来存储原始数据的,默认情况下都是存储的,但如果文本本身比较大,确实会消耗一定的存储资源,如果该文本字段本身不常被用来展示,可以考虑不进行存储,偶尔需要展示时,可以通过id再去mysql或其他数据库查出来。

注意,虽然不存储source了,但索引信息还是正常存储的,因此不影响检索,请参考以下的示例:

# remark信息不存储到_source中
PUT my_source
{"mappings":{"_doc":{"_source":{"excludes":["remark"]},"properties":{"remark":{"type":"text"}}}}}

POST /my_source/_doc/_bulk
{"index":{"_id":"1"}}
{"remark":"这是一段用于测试source作用的文本"}

# 可以搜索到结果,但没有_source信息
GET my_source/_search
{"query":{"match":{"remark":"本"}}}

# 搜索结果信息
{
"took" : 0,
"timed_out" : false,
"_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
"hits" : {
    "total" : 1,
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "my_source",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.2876821,
        "_source" : { }
      }
    ]
  }
}
doc_values

doc_values与fielddata作用是类似的,都是用来做正排索引的,只不过doc_values是按列的方式构建与存储,且都是在磁盘上完成的,而fielddata是从磁盘中构建之后再存储到JVM堆内存中。

关于倒排索引和正排索引

  • 倒排索引: 解决的是找到那些包含某个特定术语的文档?
  • 正排索引: 解决的是该字段所在的文档具体值是什么?

因此一旦涉及到类似聚合、排序这样的需求时,再使用倒排索引就不太合适了,比如排序场景,如果是用倒排索引就需要遍历所有索引,然后提取出对应的文档,再进行去重、排序。

了解了这个背景以后,我们就知道了为什么Elasticsearch默认会对除text以外的所有数据类型默认都开启了doc_values(text因为本身会被分词,所以没法按列的方式构建)。

以下案例说明了当 doc_values 关闭时,无法被排序的情况。

# age字段关闭了doc_values
PUT /emp_doc_values/
{"mappings":{"_doc":{"properties":{"name":{"type":"keyword"},"age":{"type":"integer","doc_values":false},"nick_name":{"type":"text"}}}}}

POST /emp_doc_values/_doc/_bulk
{"index":{"_id":"1"}}
{"nickname":"zhang san","name":"zhang san","age":28}
{"index":{"_id":"2"}}
{"nickname":"zhang san feng","name":"zhang san feng","age":88}
{"index":{"_id":"3"}}
{"nickname":"zhang san bu feng","name":"zhang san bu feng","age":8}

# name可排序
GET /emp_doc_values/_search
{"sort":[{"name":{"order":"desc"}}]}

# age字段由于关闭了doc_values,因此无法排序
GET /emp_doc_values/_search
{"sort":[{"age":{"order":"desc"}}]}

# nickname本身就是text字段,因此也无法排序
GET /emp_doc_values/_search
{"sort":[{"nickname":{"order":"desc"}}]}

基于上述结果可得,如果当前字段没有排序、聚合、脚本操作的需求,可以考虑关闭 doc_values ,节省磁盘存储空间。

优化建议

index.refresh_interval

此参数用于控制文档从被写入到可被搜索的处理时间,默认频率为每秒执行一次(仅针对在过去 index.search.idle.after 秒内至少接收到一次搜索请求的索引),也就是说数据至少要在写入1s以后才能被搜索到,如果索引搜索请求不高,可以不调整,如果索引请求频率较高,则建议适当延长这个时间以减少集群压力(建议调到30s)。

Use bulk requests

几乎所有涉及到I/O的操作,一定都有批量的能力,而且批量的能力要远远大于单条逐一执行,至于生产上实际一批设置多大,这得根据实际情况测试,具体使用方式在上面的一些案例中都有用到。

id采用自动生成

如果ID非自动生成,那么Elasticsearch就需要检查同一分片内是否已经存在具有相同ID的文档,这是一个成本较高的操作,并且随着索引的增长,这个操作的成本会变得更高。通过使用自动生成的ID,Elasticsearch可以跳过这个检查,这样索引速度会更快。

按需查询

通过_source指定返回实际需要的字段,减少网络传输,提升查询效率。

以下查询展示了如何只返回nickname字段信息的方式。

GET /emp/_search
{
  "_source": ["nickname"], 
  "query": {
    "match": {
      "nickname""zhang"
    }
  }
}
深度分页

为什么会有深度分页的问题?应该说所有分布式存储的架构都会涉及到这个问题,假设我的数据均匀存储在5个分片中,我现在要按照每页10条,请求第1000页的数据,即: 1001~1010 ,所以5个分片都必须现在查出前1010条结果,然后全部汇总到协调节点中,协调节点再对5050条数据进行排序,取出前10条,丢弃5040条。

所以,如果是第2000页,第5000页呢?页数越深,协调节点一次性要处理的数据就越多(分片数 * (第几页 + 每页条数)),这就是深度分页的问题所在。

为此Elasticsearch还特意做了保护,通过 index.max_result_window 参数来约束页数的大小,默认是10000。

解决方式

Elasticsearch提供了两种解决方式,一种是 scroll api ,一种是 search_after scroll api 本身也存在一些弊端,建议直接使用 search_after ,它是根据一个游标位来处理,通过上一页的结果来帮助检索下一页。

以下案例说明了如何使用search_after实现分页查询:

普通的分页查询,得到前两条记录分别为id为:2和1的数据。







请到「今天看啥」查看全文