MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

ElasticSearch搜索过程中的关键步骤详解

2023-05-183.5k 阅读

ElasticSearch搜索过程中的关键步骤详解

客户端请求

在使用ElasticSearch进行搜索时,一切始于客户端向ElasticSearch集群发送搜索请求。客户端可以使用各种编程语言的ElasticSearch客户端库来构建和发送请求。例如,使用Python的elasticsearch库:

from elasticsearch import Elasticsearch

es = Elasticsearch(['http://localhost:9200'])
query = {
    "query": {
        "match": {
            "title": "search keyword"
        }
    }
}
response = es.search(index='my_index', body=query)
print(response)

在上述代码中,首先创建了一个Elasticsearch客户端实例,然后定义了一个查询体query,这里使用了简单的match查询在title字段中搜索关键词。最后通过es.search方法向名为my_index的索引发送搜索请求并获取响应。

协调节点处理

当客户端请求到达ElasticSearch集群时,请求首先会被发送到一个协调节点(Coordinating Node)。集群中的任何节点都可以充当协调节点,它的主要职责是负责接收来自客户端的请求,并将请求分发给相关的数据节点进行处理,最后将这些数据节点返回的结果进行合并和排序,再返回给客户端。

假设集群中有多个数据节点,协调节点需要确定哪些数据节点包含与搜索请求相关的分片。ElasticSearch的索引由多个分片组成,这些分片可以分布在不同的数据节点上。协调节点通过内部的元数据信息(存储在主节点上并同步到其他节点),了解每个索引的分片分布情况。

分片搜索

协调节点确定相关的数据节点后,会将搜索请求并行发送到这些数据节点上的相关分片。每个分片在本地执行搜索操作。

以倒排索引为基础,分片上的搜索过程如下:

  1. 词法分析:对于文本类型的字段,ElasticSearch会对查询字符串进行词法分析。例如,对于查询字符串“quick brown fox”,标准分析器可能会将其分解为“quick”、“brown”和“fox”等词项(terms)。

  2. 倒排索引查找:根据词法分析得到的词项,在倒排索引中查找包含这些词项的文档列表。倒排索引是一种数据结构,它将每个词项映射到包含该词项的文档集合,同时记录词项在文档中的位置等信息。例如,假设倒排索引中“quick”词项对应的文档列表为[doc1, doc3],“brown”词项对应的文档列表为[doc1, doc2]。

  3. 评分计算:对于找到的每个文档,ElasticSearch会根据查询条件计算一个相关性得分(relevance score)。评分算法是ElasticSearch的核心特性之一,它综合考虑了多种因素,如词项在文档中的频率(词项出现得越频繁,文档相关性可能越高)、文档的长度(较短的文档可能更相关)、索引的统计信息等。以经典的TF - IDF(词频 - 逆文档频率)算法为例,词频(TF)表示一个词项在文档中出现的次数,逆文档频率(IDF)表示包含该词项的文档数在总文档数中的反比。公式大致为:$TF - IDF = TF \times IDF$。ElasticSearch在此基础上进行了扩展和优化,例如引入了BM25算法等,以更好地适应不同的应用场景。

假设文档doc1中“quick”出现2次,“brown”出现1次,文档doc2中“brown”出现3次,“quick”未出现。文档doc1长度为100词,文档doc2长度为200词。假设总文档数为1000,包含“quick”的文档数为100,包含“brown”的文档数为200。则根据TF - IDF算法简单计算:

  • 对于doc1
    • “quick”的$TF = 2/100$,$IDF = \log(1000/100)$,“quick”的$TF - IDF = (2/100) \times \log(1000/100)$
    • “brown”的$TF = 1/100$,$IDF = \log(1000/200)$,“brown”的$TF - IDF = (1/100) \times \log(1000/200)$
    • doc1的总得分约为这两个词项的$TF - IDF$得分之和。
  • 对于doc2
    • “brown”的$TF = 3/200$,$IDF = \log(1000/200)$,“brown”的$TF - IDF = (3/200) \times \log(1000/200)$
    • doc2的得分仅为“brown”的$TF - IDF$得分(因为“quick”未出现)。

在实际的ElasticSearch评分计算中,还会考虑更多复杂因素,如字段权重、查询类型等。

  1. 结果收集:每个分片将本地搜索得到的文档及对应的评分结果收集起来,形成一个局部的搜索结果集。这个局部结果集包含了该分片上与查询相关的文档信息及其相关性得分。

结果合并与排序

数据节点将各自分片的局部搜索结果返回给协调节点。协调节点接收到所有相关分片的结果后,开始进行结果的合并与排序。

  1. 合并:协调节点将来自不同分片的局部结果合并成一个全局结果集。这个全局结果集包含了所有分片上与查询相关的文档及其得分。

  2. 排序:协调节点根据相关性得分(默认情况下)对全局结果集进行排序。如果查询中指定了其他排序方式,如按照某个数值字段进行升序或降序排列,协调节点会按照指定的排序规则进行排序。例如,如果查询要求按照“price”字段升序排列商品文档,协调节点会根据每个文档中的“price”字段值对文档进行重新排序。

  3. 分页处理:如果客户端请求中包含分页参数(如fromsize),协调节点会对排序后的结果进行分页处理。from参数指定从结果集的第几个文档开始返回,size参数指定返回的文档数量。例如,from = 0size = 10表示返回前10个文档;from = 10size = 10表示返回第11到第20个文档。

高亮显示

除了返回搜索结果文档及其得分外,ElasticSearch还支持对搜索结果进行高亮显示,以便用户更直观地看到文档中与查询词匹配的部分。

  1. 高亮请求:在客户端请求中,可以通过设置highlight参数来开启高亮功能。例如:
query = {
    "query": {
        "match": {
            "content": "search keyword"
        }
    },
    "highlight": {
        "fields": {
            "content": {}
        }
    }
}
response = es.search(index='my_index', body=query)

上述代码中,在查询体中添加了highlight部分,指定对content字段进行高亮。

  1. 高亮处理:在分片搜索过程中,当找到与查询词匹配的文档时,除了计算得分外,还会对指定字段进行高亮标记。ElasticSearch使用一种称为“偏移量查询”(offset query)的技术,在原始文本中根据词项的位置信息找到匹配的片段,并使用HTML标签(默认是<em>标签)将其包裹起来,形成高亮片段。

例如,假设文档内容为“这是一段包含search keyword的文本”,当对“content”字段进行高亮时,ElasticSearch会将匹配的“search keyword”部分标记为<em>search keyword</em>,并将这些高亮片段返回给协调节点。

  1. 结果整合:协调节点在合并和排序结果时,将各个分片返回的高亮片段整合到最终的响应结果中。客户端接收到的响应中,除了文档内容和得分外,还会包含高亮字段的高亮片段信息,以便在前端展示时突出显示与查询相关的部分。

深度分页与Scroll API

在处理大量数据的搜索时,传统的分页方式(通过fromsize参数)可能会遇到性能问题。因为随着from值的增大,协调节点需要从各个分片获取并合并大量中间结果,这会消耗大量的网络带宽和内存资源。

为了解决这个问题,ElasticSearch提供了Scroll API。

  1. Scroll请求:使用Scroll API时,客户端首先发送一个初始的搜索请求,并在请求中设置scroll参数指定滚动的保持时间。例如:
query = {
    "query": {
        "match_all": {}
    }
}
response = es.search(index='my_index', body=query, scroll='1m')
scroll_id = response['_scroll_id']

上述代码中,使用match_all查询获取所有文档,并设置scroll为1分钟,表示滚动上下文将保持1分钟。响应中会返回一个_scroll_id,用于后续的滚动操作。

  1. 滚动操作:客户端使用返回的scroll_id通过_search/scroll接口进行后续的滚动请求,每次请求可以获取指定数量(通过size参数)的文档。例如:
while True:
    response = es.scroll(scroll_id=scroll_id, scroll='1m')
    hits = response['hits']['hits']
    if not hits:
        break
    for hit in hits:
        print(hit['_source'])
    scroll_id = response['_scroll_id']

上述代码通过循环不断滚动获取文档,直到没有更多文档为止。每次滚动请求都会更新scroll_id,并且滚动上下文会根据设置的保持时间不断延续,直到超时。

通过Scroll API,ElasticSearch可以在保持较低内存开销的情况下,处理大量数据的分页搜索需求。

搜索类型与优化

ElasticSearch支持多种搜索类型,每种类型适用于不同的场景,合理选择搜索类型可以优化搜索性能。

  1. Query - then - Fetch:这是默认的搜索类型。在这种类型下,协调节点首先向各个分片发送查询请求(Query阶段),每个分片返回文档ID和得分等简要信息。协调节点收集并合并这些信息,根据得分进行排序,然后根据排序结果向相关分片发送获取文档内容的请求(Fetch阶段),最终返回完整的文档。这种方式适用于大多数场景,因为它减少了网络传输的数据量,先通过简要信息进行排序,再获取完整文档。

  2. DFS Query - then - Fetch:与Query - then - Fetch类似,但在Query阶段之前增加了一个分布式词频统计(DFS)步骤。在DFS步骤中,协调节点会收集所有分片上的词频信息,以便更准确地计算文档的相关性得分。这种方式适用于需要更精确评分的场景,但由于增加了DFS步骤,性能开销相对较大。

  3. Count:这种搜索类型专门用于统计与查询匹配的文档数量,而不返回文档内容。例如:

query = {
    "query": {
        "match": {
            "category": "electronics"
        }
    }
}
response = es.count(index='products', body=query)
print(response['count'])

上述代码通过es.count方法统计“products”索引中“category”为“electronics”的文档数量。在只需要知道匹配文档数量的场景下,使用Count搜索类型可以避免返回大量文档内容,提高性能。

  1. Search - After:与传统的fromsize分页方式不同,Search - After基于上一页的最后一个文档的排序值进行分页。例如,假设按照“timestamp”字段降序排列文档,第一页返回了时间戳最大的10个文档,那么第二页的请求中可以将第一页最后一个文档的“timestamp”值作为search_after参数的值,这样ElasticSearch可以直接从该时间戳之后的文档开始返回下一页结果,避免了随着from值增大带来的性能问题,适用于大数据量的分页场景。

通过深入理解ElasticSearch搜索过程中的这些关键步骤,包括客户端请求、协调节点处理、分片搜索、结果合并与排序、高亮显示、深度分页以及搜索类型的选择与优化等,开发者可以更好地利用ElasticSearch的强大功能,构建高效、准确的搜索应用。同时,通过实际的代码示例,可以更直观地掌握如何在不同场景下运用这些特性。在实际应用中,还需要根据具体的数据规模、查询需求等因素,灵活调整搜索策略和参数,以达到最佳的搜索性能和用户体验。