提高索引的查询速度是一个优化的系统性能的重要角度,有哪些手段可以提高索引的查询速度呢?
文档建模:合理的文档模型
应该对文档进行合理的建模,这样可以提高搜索的效率。
禁用join关系。nested
会让查询慢几倍,parent-child
关系会让查询慢几百倍。
搜索尽可能少的字段
query_string
和 multi_match
查询的字段越多,越慢。可以将多个字段的值拷贝到一个字段中,以提高多个字段的搜索速度。拷贝可以使用 copy_to
完成。例如,下面对字段 name
和 plot
的查询,可以通过将 name
和 plot
字段的值拷贝到 name_and_plot
字段中,只对 name_and_plot
字段查询。
PUT movies
{
"mappings": {
"properties": {
"name_and_plot": {
"type": "text"
},
"name": {
"type": "text",
"copy_to": "name_and_plot"
},
"plot": {
"type": "text",
"copy_to": "name_and_plot"
}
}
}
}
提前索引数据
例如,索引中有个 price
字段,大多数查询都是发生在该字段上的 range
查询,并且范围是固定的。那就可以提前计算出文档对应的 price_range
,以便后续的查询和聚合使用。
例如,文档是:
PUT index/_doc/1
{
"designation": "spoon",
"price": 13
}
查询是:
GET index/_search
{
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 10 },
{ "from": 10, "to": 100 },
{ "from": 100 }
]
}
}
}
}
按照上面说的优化思路,可以在文档写入时,进行优化,增加字段 price_range,用来记录 price
字段对应的范围:
PUT index
{
"mappings": {
"properties": {
"price_range": {
"type": "keyword"
}
}
}
}
PUT index/_doc/1
{
"designation": "spoon",
"price": 13,
"price_range": "10-100"
}
这样,查询时,就可以直接使用 price_range
了
GET index/_search
{
"aggs": {
"price_ranges": {
"terms": {
"field": "price_range"
}
}
}
}
这个思路,还可以拓展,比如,
- 查询时需要使用脚本计算排序值,可以在文档写入时提前计算好
- 不使用es的脚本生成字段,而是写入前提前计算好
- …
这个优化的角度就是,让es少干点活,能提前搞好的,就提前搞,但是对于一些小索引可能没必要,所以还是要结合实际的场景
字段类型:选择合适的类型
并不是所有的数值类型的数据都应当映射为 numberic
类型的字段。虽然Elasticsearch 会为 range
查询优化数值类型的字段,例如 integer 和 long,但是 keyword 类型的字段在 term
或者其他 term-level
的查询时表现更好。
例如:如果有些数据的 ID 不会使用 range
查询,但会使用 term
查询。那么就可以考虑将其类型设为 keyword
来优化。
- 如果不使用
range
查询 - 为了快速检索,应该使用
keyword
类型,因为term
查询中,keyword
通过比数值类型搜索的更快。
如果不确定使用哪个,可以通过 multi-field,映射出 keyword 和数值类型的字段。
避免使用脚本
如果有可能的话,避免使用脚本,包括:使用脚本排序,使用脚本聚合,脚本计算评分。
虽然脚本非常有用,但不能使用 Elasticsearch 的索引结构或相关优化。使用脚本有时会导致搜索速度变慢。
日期搜索优化
对使用的日期字段的查询now
通常不可缓存,因为匹配的范围一直在变化。然而,就用户体验而言,切换到四舍五入的日期通常是可以接受的,并且具有更好地利用查询缓存的好处。
例如下面的查询:
PUT index/_doc/1
{
"my_date": "2016-05-11T16:30:55.328Z"
}
GET index/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"my_date": {
"gte": "now-1h",
"lte": "now"
}
}
}
}
}
}
可以用以下查询替换:
GET index/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"my_date": {
"gte": "now-1h/m",
"lte": "now/m"
}
}
}
}
}
}
在这种情况下,我们四舍五入到分钟,所以如果当前时间是16:31:29
,范围查询将匹配my_date
字段值介于15:31:00
和之间的所有内容16:31:59
。如果多个用户在同一分钟内运行包含此范围的查询,则查询缓存可以帮助加快速度。用于舍入的间隔越长,查询缓存的帮助就越大,但要注意过于激进的舍入也可能会损害用户体验。
分段合并:强制合并只读索引
只读索引可以通过 _forcemerge 进行分段合并。这通常是基于时间的索引的情况:只有当前时间范围的索引正在获取新文档,而旧索引是只读的。已被强制合并为单个段的分片可以使用更简单、更有效的数据结构来执行搜索。
不要强制合并仍会写入的索引。依靠自动后台合并进程根据需要执行合并,以保持索引平稳运行。如果继续写入强制合并索引,那么性能可能会变得更糟。
使用 preference
提高缓存利用率
有多个缓存可以帮助提高搜索性能,例如 文件系统缓存、 请求缓存或查询缓存。然而,所有这些缓存都是在节点级别维护的,这意味着如果您连续两次运行相同的请求,拥有 1 个或更多副本并使用默认路由算法round-robin,那么这两个请求将转到不同的分片副本,这显然不会使用到节点级缓存。
由于搜索应用程序的用户一个接一个地运行类似请求是很常见的,例如为了更好的使用到缓存,同一个搜索请求使用相同的 preference
标识可以提升缓存的使用率。
副本:副本可以提升吞吐量,但不总是
副本可以帮助提高吞吐量。例如,如果您有一个单分片索引和三个节点,则需要将副本数设置为 2,总共拥有 3 个分片副本,以便利用所有节点。
现在假设您有一个 2个主分片的索引和两个节点。在一种情况下,副本数为 0,这意味着每个节点都拥有一个分片。在第二种情况下,副本数为 1,这意味着每个节点都有两个分片。哪种设置在搜索性能方面表现最好?通常,每个节点总分片较少的设置会表现得更好。原因是它为每个分片提供了更大份额的可用文件系统缓存,而文件系统缓存可能是 Elasticsearch 的第一大性能因素。同时要注意,没有副本的设置在单节点故障的情况下会失败,因此需要在吞吐量和可用性之间进行权衡。
那么正确的副本数是多少?如果您有一个包含 num_nodes
节点、总共num_primaries
有主分片的集群,并且您希望最多能够同时处理节点故障为max_failures
,那么适合您的副本数量是 max(max_failures, ceil(num_nodes / num_primaries) - 1)
总结
以上的几个可以着手优化的点,是开发者需要重点关注的,除了这些之外,还有一些是运维需要关注的,这些在官方文档中也有提及。
参考文档
官方文档:[tune-for-search-speed](