调整搜索速度编辑

为文件系统缓存提供内存编辑

Elasticsearch 严重依赖文件系统缓存来实现快速搜索。通常,您应该确保至少一半的可用内存用于文件系统缓存,以便 Elasticsearch 可以将索引的热点区域保留在物理内存中。

通过在 Linux 上使用适度的预读值来避免页面缓存抖动编辑

搜索会导致大量随机读取 I/O。当底层块设备具有较高的预读值时,可能会执行很多不必要的读取 I/O,尤其是在使用内存映射访问文件时(请参阅存储类型)。

大多数 Linux 发行版对单个普通设备使用128KiB的合理预读值,但是,当使用软件 RAID、LVM 或 dm-crypt 时,生成的块设备(支持 Elasticsearch path.data)最终可能具有非常大的预读值(在几 MiB 的范围内)。这通常会导致严重的页面(文件系统)缓存抖动,从而对搜索(或更新)性能产生负面影响。

您可以使用lsblk -o NAME,RA,MOUNTPOINT,TYPE,SIZE检查KiB中的当前值。有关如何更改此值,请参阅您的发行版的文档(例如,使用udev规则在重新启动后保持持久性,或者通过blockdev --setra作为临时设置)。我们建议预读值为128KiB

blockdev期望以 512 字节扇区为单位的值,而lsblk报告以KiB为单位的值。例如,要为/dev/nvme0n1临时将预读设置为128KiB,请指定blockdev --setra 256 /dev/nvme0n1

使用更快的硬件编辑

如果您的搜索受 I/O 限制,请考虑增加文件系统缓存的大小(请参阅上文)或使用更快的存储。每次搜索都涉及跨多个文件的顺序读取和随机读取的混合,并且每个分片上可能同时运行许多搜索,因此 SSD 驱动器的性能往往优于旋转磁盘。

如果您的搜索受 CPU 限制,请考虑使用更多数量的更快 CPU。

本地存储与远程存储编辑

直接连接(本地)存储的性能通常优于远程存储,因为它更易于配置,并且避免了通信开销。

某些远程存储的性能非常差,尤其是在 Elasticsearch 施加的负载类型下。但是,通过仔细调整,有时也可以使用远程存储来获得可接受的性能。在提交到特定的存储架构之前,请使用实际工作负载对您的系统进行基准测试,以确定任何调整参数的影响。如果您无法达到预期的性能,请与您的存储系统供应商合作以确定问题。

文档建模编辑

文档的建模应该使搜索时的操作尽可能便宜。

特别是,应避免联接。nested 会使查询速度降低几倍,而父子关系 会使查询速度降低数百倍。因此,如果可以通过反规范化文档在不使用联接的情况下回答相同的问题,则可以预期速度会显着提高。

搜索尽可能少的字段编辑

query_stringmulti_match 查询的目标字段越多,速度就越慢。提高多个字段搜索速度的常用技术是在索引时将其值复制到单个字段中,然后在搜索时使用该字段。这可以通过映射的copy-to 指令自动完成,而无需更改文档的来源。下面是一个包含电影的索引示例,该索引通过将两个值都索引到name_and_plot 字段中来优化同时搜索电影名称和情节的查询。

response = client.indices.create(
  index: 'movies',
  body: {
    mappings: {
      properties: {
        name_and_plot: {
          type: 'text'
        },
        name: {
          type: 'text',
          copy_to: 'name_and_plot'
        },
        plot: {
          type: 'text',
          copy_to: 'name_and_plot'
        }
      }
    }
  }
)
puts response
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 聚合,则可以通过将范围预索引到索引中并使用terms 聚合来加快此聚合的速度。

例如,如果文档如下所示

response = client.index(
  index: 'index',
  id: 1,
  body: {
    designation: 'spoon',
    price: 13
  }
)
puts response
PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13
}

并且搜索请求如下所示

response = client.search(
  index: 'index',
  body: {
    aggregations: {
      price_ranges: {
        range: {
          field: 'price',
          ranges: [
            {
              to: 10
            },
            {
              from: 10,
              to: 100
            },
            {
              from: 100
            }
          ]
        }
      }
    }
  }
)
puts response
GET index/_search
{
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 10 },
          { "from": 10, "to": 100 },
          { "from": 100 }
        ]
      }
    }
  }
}

然后可以在索引时通过price_range 字段丰富文档,该字段应映射为keyword

response = client.indices.create(
  index: 'index',
  body: {
    mappings: {
      properties: {
        price_range: {
          type: 'keyword'
        }
      }
    }
  }
)
puts response

response = client.index(
  index: 'index',
  id: 1,
  body: {
    designation: 'spoon',
    price: 13,
    price_range: '10-100'
  }
)
puts response
PUT index
{
  "mappings": {
    "properties": {
      "price_range": {
        "type": "keyword"
      }
    }
  }
}

PUT index/_doc/1
{
  "designation": "spoon",
  "price": 13,
  "price_range": "10-100"
}

然后,搜索请求可以聚合此新字段,而不是在price 字段上运行range 聚合。

response = client.search(
  index: 'index',
  body: {
    aggregations: {
      price_ranges: {
        terms: {
          field: 'price_range'
        }
      }
    }
  }
)
puts response
GET index/_search
{
  "aggs": {
    "price_ranges": {
      "terms": {
        "field": "price_range"
      }
    }
  }
}

考虑将标识符映射为keyword编辑

并非所有数字数据都应映射为数字字段数据类型。Elasticsearch 优化了数字字段(例如integerlong),用于range 查询。但是,keyword 字段更适合term 和其他词条级 查询。

标识符(例如 ISBN 或产品 ID)很少在range 查询中使用。但是,它们通常使用词条级查询来检索。

如果满足以下条件,请考虑将数字标识符映射为keyword

  • 您不打算使用range 查询搜索标识符数据。
  • 快速检索很重要。keyword 字段上的term 查询搜索通常比数字字段上的term 搜索更快。

如果您不确定要使用哪个,可以使用多字段将数据同时映射为keyword *和* 数字数据类型。

避免使用脚本编辑

如果可能,请避免在聚合中使用基于script的排序、脚本以及script_score 查询。请参阅脚本、缓存和搜索速度

搜索舍入日期编辑

使用now 的日期字段上的查询通常是不可缓存的,因为正在匹配的范围一直在变化。但是,就用户体验而言,切换到舍入日期通常是可以接受的,并且具有更好地利用查询缓存的好处。

例如,以下查询

response = client.index(
  index: 'index',
  id: 1,
  body: {
    my_date: '2016-05-11T16:30:55.328Z'
  }
)
puts response

response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          range: {
            my_date: {
              gte: 'now-1h',
              lte: 'now'
            }
          }
        }
      }
    }
  }
)
puts response
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"
          }
        }
      }
    }
  }
}

可以替换为以下查询

response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          range: {
            my_date: {
              gte: 'now-1h/m',
              lte: 'now/m'
            }
          }
        }
      }
    }
  }
)
puts response
GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "my_date": {
            "gte": "now-1h/m",
            "lte": "now/m"
          }
        }
      }
    }
  }
}

在这种情况下,我们舍入到分钟,因此如果当前时间是16:31:29,则范围查询将匹配my_date 字段的值介于15:31:0016:31:59 之间的所有内容。如果多个用户在同一分钟内运行包含此范围的查询,则查询缓存可以帮助加快速度。用于舍入的时间间隔越长,查询缓存的帮助就越大,但请注意,过于激进的舍入也可能会损害用户体验。

您可能会想将范围拆分为一个大的可缓存部分和一个小的不可缓存部分,以便能够利用查询缓存,如下所示

response = client.search(
  index: 'index',
  body: {
    query: {
      constant_score: {
        filter: {
          bool: {
            should: [
              {
                range: {
                  my_date: {
                    gte: 'now-1h',
                    lte: 'now-1h/m'
                  }
                }
              },
              {
                range: {
                  my_date: {
                    gt: 'now-1h/m',
                    lt: 'now/m'
                  }
                }
              },
              {
                range: {
                  my_date: {
                    gte: 'now/m',
                    lte: 'now'
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
)
puts response
GET index/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "bool": {
          "should": [
            {
              "range": {
                "my_date": {
                  "gte": "now-1h",
                  "lte": "now-1h/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gt": "now-1h/m",
                  "lt": "now/m"
                }
              }
            },
            {
              "range": {
                "my_date": {
                  "gte": "now/m",
                  "lte": "now"
                }
              }
            }
          ]
        }
      }
    }
  }
}

但是,这种做法可能会导致查询在某些情况下运行速度变慢,因为bool 查询引入的开销可能会抵消更好地利用查询缓存带来的节省。

强制合并只读索引编辑

只读索引可能会受益于合并到单个段。基于时间的索引通常就是这种情况:只有当前时间范围的索引正在获取新文档,而较旧的索引是只读的。已强制合并到单个段中的分片可以使用更简单、更高效的数据结构来执行搜索。

不要强制合并您仍在写入或将来会再次写入的索引。相反,请依靠自动后台合并过程根据需要执行合并,以保持索引平稳运行。如果您继续写入强制合并的索引,则其性能可能会变得更差。

预热全局序号编辑

全局序号是一种用于优化聚合性能的数据结构。它们是延迟计算的,并作为字段数据缓存的一部分存储在 JVM 堆中。对于大量用于桶聚合的字段,您可以告诉 Elasticsearch 在收到请求之前构建和缓存全局序号。这应该谨慎操作,因为它会增加堆使用量,并可能使刷新花费更长时间。可以通过设置eager global ordinals映射参数来动态更新现有映射上的选项

response = client.indices.create(
  index: 'index',
  body: {
    mappings: {
      properties: {
        foo: {
          type: 'keyword',
          eager_global_ordinals: true
        }
      }
    }
  }
)
puts response
PUT index
{
  "mappings": {
    "properties": {
      "foo": {
        "type": "keyword",
        "eager_global_ordinals": true
      }
    }
  }
}

预热文件系统缓存编辑

如果运行 Elasticsearch 的机器重启,文件系统缓存将为空,因此操作系统需要一些时间才能将索引的热点区域加载到内存中,以便搜索操作快速进行。您可以使用index.store.preload设置,根据文件扩展名明确告诉操作系统哪些文件应该预先加载到内存中。

如果文件系统缓存不够大,无法容纳所有数据,那么在太多索引或太多文件上预先将数据加载到文件系统缓存中会使搜索速度*变慢*。请谨慎使用。

使用索引排序来加速连接查询编辑

索引排序可以用来加快连接查询的速度,但代价是索引速度略有降低。在索引排序文档中了解更多信息。

使用preference优化缓存利用率编辑

有多个缓存可以帮助提高搜索性能,例如文件系统缓存请求缓存查询缓存。然而,所有这些缓存都是在节点级别维护的,这意味着如果您连续两次运行相同的请求,并且有 1 个或多个副本,并使用循环(默认路由算法),那么这两个请求将被发送到不同的分片副本,从而阻止节点级缓存发挥作用。

由于搜索应用程序的用户通常会一个接一个地运行类似的请求,例如为了分析索引中更窄的子集,因此使用标识当前用户或会话的首选项值可以帮助优化缓存的使用。

副本可能有助于提高吞吐量,但并非总是如此编辑

除了提高弹性之外,副本还可以帮助提高吞吐量。例如,如果您有一个单分片索引和三个节点,您需要将副本数设置为 2,以便总共有 3 个分片副本,以便所有节点都被利用。

现在假设您有一个 2 分片索引和两个节点。在一种情况下,副本数为 0,这意味着每个节点都包含一个分片。在第二种情况下,副本数为 1,这意味着每个节点都有两个分片。哪种设置在搜索性能方面表现最佳?通常,每个节点分片总数较少的设置性能更好。原因是它为每个分片提供了更大份额的可用文件系统缓存,而文件系统缓存可能是 Elasticsearch 的头号性能因素。同时,请注意,在单个节点故障的情况下,没有副本的设置容易出现故障,因此在吞吐量和可用性之间需要权衡。

那么,什么是合适的副本数呢?如果您的集群有num_nodes个节点,*总共*有num_primaries个主分片,并且您希望能够同时应对最多max_failures个节点故障,那么适合您的副本数是max(max_failures, ceil(num_nodes / num_primaries) - 1)

使用搜索分析器调整查询编辑

分析 API提供了有关查询和聚合的每个组件如何影响请求处理时间的详细信息。

Kibana 中的搜索分析器可以轻松导航和分析分析结果,并让您深入了解如何调整查询以提高性能并减少负载。

由于分析 API 本身会给查询增加大量开销,因此这些信息最好用于了解各种查询组件的相对成本。它不能提供实际处理时间的可靠度量。

使用index_phrases实现更快的短语查询编辑

text字段有一个index_phrases选项,用于索引 2-shingles,查询解析器会自动利用它来运行没有 slop 的短语查询。如果您的用例涉及运行大量短语查询,这可以显著加快查询速度。

使用index_prefixes实现更快的词缀查询编辑

text字段有一个index_prefixes选项,用于索引所有词缀的词缀,查询解析器会自动利用它来运行词缀查询。如果您的用例涉及运行大量词缀查询,这可以显著加快查询速度。

使用constant_keyword加速过滤编辑

一般来说,过滤器的成本主要取决于匹配文档的数量。假设您有一个包含自行车的索引。有大量的自行车,许多搜索都会对cycle_type: bicycle执行过滤。这个非常常见的过滤器不幸的是也非常昂贵,因为它匹配大多数文档。有一种简单的方法可以避免运行此过滤器:将自行车移动到它们自己的索引中,并通过搜索此索引来过滤自行车,而不是向查询添加过滤器。

不幸的是,这会使客户端逻辑变得棘手,而这正是constant_keyword的用武之地。通过在包含自行车的索引上将cycle_type映射为值为bicycleconstant_keyword,客户端可以继续运行与在整体索引上运行的完全相同的查询,而 Elasticsearch 将在自行车索引上执行正确的操作,方法是忽略对cycle_type的过滤器(如果值为bicycle),否则不返回任何结果。

以下是映射的外观

response = client.indices.create(
  index: 'bicycles',
  body: {
    mappings: {
      properties: {
        cycle_type: {
          type: 'constant_keyword',
          value: 'bicycle'
        },
        name: {
          type: 'text'
        }
      }
    }
  }
)
puts response

response = client.indices.create(
  index: 'other_cycles',
  body: {
    mappings: {
      properties: {
        cycle_type: {
          type: 'keyword'
        },
        name: {
          type: 'text'
        }
      }
    }
  }
)
puts response
PUT bicycles
{
  "mappings": {
    "properties": {
      "cycle_type": {
        "type": "constant_keyword",
        "value": "bicycle"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

PUT other_cycles
{
  "mappings": {
    "properties": {
      "cycle_type": {
        "type": "keyword"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

我们将索引分成两个:一个只包含自行车,另一个包含其他自行车:独轮车、三轮车等。然后在搜索时,我们需要搜索这两个索引,但我们不需要修改查询。

response = client.search(
  index: 'bicycles,other_cycles',
  body: {
    query: {
      bool: {
        must: {
          match: {
            description: 'dutch'
          }
        },
        filter: {
          term: {
            cycle_type: 'bicycle'
          }
        }
      }
    }
  }
)
puts response
GET bicycles,other_cycles/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "description": "dutch"
        }
      },
      "filter": {
        "term": {
          "cycle_type": "bicycle"
        }
      }
    }
  }
}

bicycles索引上,Elasticsearch 将简单地忽略cycle_type过滤器,并将搜索请求重写为以下内容

response = client.search(
  index: 'bicycles,other_cycles',
  body: {
    query: {
      match: {
        description: 'dutch'
      }
    }
  }
)
puts response
GET bicycles,other_cycles/_search
{
  "query": {
    "match": {
      "description": "dutch"
    }
  }
}

other_cycles索引上,Elasticsearch 将很快发现bicyclecycle_type字段的词项字典中不存在,并返回一个没有结果的搜索响应。

这是一种通过将常用值放在专用索引中来降低查询成本的有效方法。这个想法也可以组合多个字段:例如,如果您跟踪每辆自行车的颜色,并且您的bicycles索引最终包含了大部分黑色自行车,您可以将其拆分为bicycles-blackbicycles-other-colors索引。

此优化并非严格要求constant_keyword:也可以更新客户端逻辑,以便根据过滤器将查询路由到相关索引。然而,constant_keyword使其变得透明,并允许以极小的开销将搜索请求与索引拓扑分离。