死磕Elasticsearch(五)Get流程和Search流程 - Go语言中文社区

死磕Elasticsearch(五)Get流程和Search流程


1 前言

  ES 读取过程分为GET和Search两种操作。

GET/MGET(批量GET):
	需要指定_index、_type、_id。也就是根据id从正排索引中获取内容。
Search:
	Search不指定_id,根据关键词从哪个倒排索引中获取内容

参考文献:《Elasticsearch源码解析与优化实战》

2 GET/MGET

2.1 GET/MGET:官网介绍

https://www.elastic.co/guide/en/elasticsearch/reference/5.4/docs-get.html#generated-fields

eq:

curl -X GET "localhost:9200/twitter/tweet/0"

{
    "_index" : "twitter",
    "_type" : "tweet",
    "_id" : "0",
    "_version" : 1,
    "found": true,
    "_source" : {
        "user" : "kimchy",
        "date" : "2009-11-15T14:12:12",
        "likes": 0,
        "message" : "trying out Elasticsearch"
    }
}

2.2 可选参数:

参数名称 参数解释
Realtime 默认为true。默认是实时的,不受索引刷新频率设置的影响,如果文档已近更新,但还没有refresh到segment,GET API将会发出一个refresh调用,使文档可见。
Optional Type get api允许_类型是可选的。将其设置为“_all”时,以便获取所有类型中与ID匹配的第一个文档。
Source filtering 默认情况下(true),返回文档所有内容,可以将_source设置为false,不返回文档内容。比如:curl -X GET "localhost:9200/twitter/tweet/0?_source=false"同时可以使用_source_include和_source_exclude过滤返回文档中的部分字段。比如:curl -X GET “localhost:9200/twitter/tweet/0?_source_include=*.id&_source_exclude=entities”
Stored Fields 返回索引映射中store设置为true的字段。
Routing 自定义routing。比如:curl -X GET “localhost:9200/twitter/tweet/2?routing=user1”
Preference 默认情况下,GET API从分片的多个副本中随机选择一个,通过指定优先级(preference)可以选择从主分片或者本地读取。
Refresh 默认是false,设置为true,可以在读取之前先执行刷新操作,但是这时对写入速度是有负面影响的
Versioning support 如果在GET API中指定版本号,那么当文档实际版本号与请求版本号不一致时,将会返回409错误。

2.3 GET粗粒度流程

      搜索和读取文档都属于读操作,可以从主分片或副分片中读取数据。
读取单个文档的流程(图片来自官网)如下图所示:

      这个例子中的索引有一个主分片和两个副分片。以下是从主分片或副分片中读取时的步骤:
在这里插入图片描述

  1. 客户端向NODE1发送读请求。
  2. NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求转发到NODE2。
    NODE2将文档返回给 NODE1,NODE1将文档返回给客户端。NODE1作为协调节点,会将客户端请求轮询发送到集群的所有副本来实现负载均衡。
  3. 在读取时,文档可能已经存在于主分片上,但还没有复制到副分片。在这种情况下,读请求命中副分片时可能会报告文档不存在,但是命中主分片可能成功返回文档。一旦写请求成功返回给客户端,则意味着文档在主分片和副分片都是可用的。

2.4 GET详细分析

      GET/MGET流程涉及两个节点:协调节点和数据节点,流程如下图所示:
在这里插入图片描述

2.4.1协调节点执行流程

2.4.1.1内容路由

  1. 获取集群状态,主要是获取分片状态,red、yelllow、green。
  2. 计算目标shard列表,根据内容路由算法,计算目标shardId,也就是文档应该落在哪个分片中。
  3. 计算出目标shardId后,结合请求参数中指定的优先级和集群状态确定目标节点,由于分片可能存在多个副本,因此计算出的是一个列表。

2.4.1.2 转发请求

      作为协调节点,像目标节点转发请求(目标节点也许是本地节点),读取数据。

  1. 先检查是否为本地节点。
  2. 如果为本地节点,则直接执行获取数据。
  3. 如果非本地节点,异步发送请求(sendRequest),并等待回复。
  4. 如果数据节点执行成功,则返回数据给客户端;如果执行失败,则进行重试,将请求发送给另一个副本节点。

2.4.2 数据节点

1、接收来自协调节点的请求,执行messageReceived。
2、根据参数判断是否需要执行refresh,然后调用indexShard.getService().get()读取数据并存储到GetResult中。

2.4.2.1 读取和过滤

      这里分为获取数据和过滤两步,主要过程都是在ShardGetService#inneGet()函数中():
1、调用indexShard.get获取Engine.GetResult读取数据,底层会调用InternalEngine.get方法。
2、通过ShardGetService#innerGetLoadFromStroredFields,通过type、id等参数对指定的field source进行过滤,结果存于GetResult中。

向协调接待返回是否获取成功。

3 Search流程

      Get操作只能对精确的单个文档进行处理,而MGET也无非是对多个精确的文档进程处理,都需要由_index、_type和_id三元组来确定唯一文档。但搜索需要一种更复杂的模型,因为不知道查询会命中哪些文档。

      从粗粒度来理解search流程,可以分为这三步:

  1. 所有分片参与搜索。
  2. 协调节点合并结果。
  3. 根据上一步的文档id获取文档内容。

      需要两个阶段才能完成搜索的原因是,在查询的时候不知道文档位于哪个分片,因此索引的所有分片(某个副本)都要参与搜索,然后协调节点将结果合并,再根据文档ID获取文档内容。例如,有5个分片,查询返回前10个匹配度最高的文档,那么每个分片都查询出当前分片的TOP 10,协调节点将5×10 = 50的结果再次排序,返回最终TOP 10的结果给客户端。

      一个简单的搜索请求示例如下:
在这里插入图片描述

      在上面的例子中,我们从所有字段搜索“first”关键词,返回信息中几个基本字段的含义如下:

  1. took代表搜索执行时间(单位:毫秒);
  2. total代表本次搜索命中的文档数量;
  3. max_score为最大得分,代表文档匹配度;
  4. hits为搜索命中的结果列表,默认为10条。

3.1 索引和搜索

      ES中的数据可以分为两类:精确值和全文。

  1. 精确值,比如日期和用户id、IP地址等。
  2. 全文,指文本内容,比如一条日志,或者邮件的内容。

      这两种类型的数据在查询时是不同的:对精确值的比较是二进制的,查询要么匹配,要么不匹配;全文内容的查询无法给出“有”还是“没有”的结果,它只能找到结果是“看起来像”你要查询的东西,因此把查询结果按相似度排序,评分越高,相似度越大。
对数据建立索引和执行搜索的原理如下图所示:
在这里插入图片描述

3.1.1 建立索引

      如果是全文数据,则对文本内容进行分析,这项工作在 ES 中由分析器实现。分析器实现如下功能:

  1. 字符过滤器。主要是对字符串进行预处理,例如,去掉HTML,将&转换成and等。
  2. 分词器(Tokenizer)。将字符串分割为单个词条,例如,根据空格和标点符号分割,输出的词条称为词元(Token)。
  3. Token过滤器。根据停止词(Stop
    word)删除词元,例如,and、the等无用词,或者根据同义词表增加词条,例如,jump和leap。
  4. 语言处理。对上一步得到的Token做一些和语言相关的处理,例如,转为小写,以及将单词转换为词根的形式。语言处理组件输出的结果称为词(Term)。分析完毕后,将分析器输出的词(Term)传递给索引组件,生成倒排和正排索引,再存储到文件系统中。

3.1.2 执行搜索

     搜索调用Lucene完成,如果是全文检索,则:

  1. 对检索字段使用建立索引时相同的分析器进行分析,产生Token列表;
  2. 根据查询语句的语法规则转换成一棵语法树;
  3. 查找符合语法树的文档;
  4. 对匹配到的文档列表进行相关性评分,评分策略一般使用TF/IDF;
  5. 根据评分结果进行排序。

3.2 Search Type

ES目前有四种搜索类型:

3.2.1 query and fetch

     向索引的所有分片 ( shard)都发出查询请求, 各分片返回的时候把元素文档 ( document)和计算后的排名信息一起返回。
  这种搜索方式是最快的。 因为相比下面的几种搜索方式, 这种查询方法只需要去 shard查询一次。 但是各个 shard 返回的结果的数量之和可能是用户要求的 size 的 n 倍。

优点:这种搜索方式是最快的。因为相比后面的几种es的搜索方式,这种查询方法只需要去shard查询一次。
缺点:返回的数据量不准确, 可能返回(N*分片数量)的数据并且数据排名也不准确,
	 同时各个shard返回的结果的数量之和可能是用户要求的size的n倍。

3.2.2 query then fetch(默认)。

     如果你搜索时, 没有指定搜索方式, 就是使用的这种搜索方式。 这种搜索方式, 大概分两个步骤:
  第一步, 先向所有的 shard 发出请求, 各分片只返回文档 id(注意, 不包括文档 document)和排名相关的信息(也就是文档对应的分值), 然后按照各分片返回的文档的分数进行重新排序和排名, 取前 size 个文档。
  第二步, 根据文档 id 去相关的 shard 取 document。 这种方式返回的 document 数量与用户要求的大小是相等的。

优点:返回的数据量是准确的。
缺点:性能一般,并且数据排名不准确。

3.2.3 DFS query and fetch

     这种方式比第一种方式多了一个 DFS 步骤,有这一步,可以更精确控制搜索打分和排名。也就是在进行查询之前, 先对所有分片发送请求, 把所有分片中的词频和文档频率等打分依据全部汇总到一块(包括文档), 再执行后面的操作。

优点:数据排名准确
缺点:性能一般;返回的数据量不准确, 可能返回(N*分片数量)的数据

3.2.4 DFS query then fetch

     在进行查询之前, 先对所有分片发送请求, 把所有分片中的词频和文档频率等打分依据全部汇总到一块(不包括文档,等计算出分数排名后,根据id查询), 再执行后面的操作。

优点:返回的数据量是准确的;数据排名准确
缺点:性能最差【 这个最差只是表示在这四种查询方式中性能最慢, 也不至于不能忍受,
	 如果对查询性能要求不是非常高, 而对查询准确度要求比较高的时候可以考虑这个】

两种不同的搜索类型的区别在于查询阶段,DFS查询阶段的流程要多一些,它使用全局信息来获取更准确的评分。

3.3 分布式搜索过程

     一个搜索请求必须询问请求的索引中所有分片的某个副本来进行匹配。假设一个索引有5个主分片,每个主分片有1个副分片,共10个分片,一次搜索请求会由5个分片来共同完成,它们可能是主分片,也可能是副分片。也就是说,一次搜索请求只会命中所有分片副本中的一个。

     当搜索任务执行在分布式系统上时,整体流程如下图所示:
在这里插入图片描述

3.3.1 协调节点流程

      两阶段相应的实现位置:查询(Query)阶段:search.InitialSearchPhase;取回(Fetch)阶段:search.FetchSearchPhase。

     它们都继承自SearchPhase,如下图所示:
在这里插入图片描述

3.3.3.1 Query阶段

     在初始查询阶段,查询会广播到索引中每一个分片副本(主分片或副分片)。每个分片在本地执行搜索并构建一个匹配文档的优先队列。
     优先队列是一个存有topN匹配文档的有序列表。优先队列大小为分页参数from + size。
分布式搜索的Query阶段(图片来自官网)如下图所示:
在这里插入图片描述

     QUERY_THEN_FETCH搜索类型的查询阶段步骤如下:

  1. 客户端发送search请求到NODE 3。NODE3会解析请求,会将本集群shard列表和远程集群shard列表合并。
  2. Node 3将遍历所有shard,将请求转发到索引的每个主分片或副分片中。
  3. 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from +size的本地有序优先队列中。
  4. 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点。协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

     协调节点广播查询请求到所有相关分片时,可以是主分片或副分片,协调节点将在之后的请求中轮询所有的分片副本来分摊负载。
     查询阶段并不会对搜索请求的内容进行解析,无论搜索什么内容,只看本次搜索需要命中哪些shard,然后针对每个特定shard选择一个副本,转发搜索请求。

3.3.3.2 Fetch阶段

     Query阶段知道了要取哪些数据,但是并没有取具体的数据,这就是Fetch阶段要做的。

Fetch阶段相当于GET

     分布式搜索的Fetch阶段(图片来自官网)如下图所示:
在这里插入图片描述
     Fetch阶段由以下步骤构成:

  1. 协调节点向相关NODE发送GET请求。
  2. 分片所在节点向协调节点返回数据。
  3. 协调节点等待所有文档被取得,然后返回给客户端。
  4. 片所在节点在返回文档数据时,处理有可能出现的_source字段和高亮参数。

     协调节点首先决定哪些文档“确实”需要被取回,例如,如果查询指定了{ "from": 90, "size":10 },则只有从第91个开始的10个结果需要被取回。
     为了避免在协调节点中创建的number_of_shards * (from + size)优先队列过大,应尽量控制分页深度。

3.4 执行数据节点流程

3.4.1 Query阶段

     常见的Query请求为:

indices:data/read/search/[phase/query]
  1. 响应Qurey请求
  2. 检测是否允许cache,默认为true,查询时,先从cache获取,cache由节点所有分片共享,基于LRU算法实现,空间满时会删除最近最少使用的数据。cache并不会缓存全部的检索结果。
  3. 使用lucene实现检索。核心方法为:queryPhase.execute(context)

其中包含几个核心功能:

a) execute(),调用lucene,searcher.search()实现搜索。
b) rescorePhare,全文检索且需要打分
c) suggestPhase,自动补全以及纠错
d) aggregationPhase,实现聚合。(在es中,而非Lucene中,在检索后完成)

3.4.2 Fetch阶段

     响应Fetch请求,以常见的基于id进程fetch请求为例:

indices:data/read/search/[phase/fetch/id]

     主要过程是执行Fetch,然后发送Response:
对Fetch响应的实现封装在searchService.executeFetchPhase中,核心是调用fetchPhase.executor(context),按照命中的文档获取相关内容,填充到SearchHits中,最终封装到FetchSearchResult中。

4 小结

1、聚合是在ES中实现的,而非Lucene。
2、Query和Fetch请求之间是无状态的,除非是scroll方式。
3、分页搜索不会单独“cache”,cache和分页没有关系。
4、每次分页的请求都是一次重新搜索的过程,而不是从第一次搜索的结果中获取。看上去不太符合常规的做法,事实上互联网的搜索引擎都是重新执行了搜索过程:人们基本只看前几页,很少深度分页;重新执行一次搜索很快;如果缓存第一次搜索结果等待翻页命中,则这种缓存的代价较大,意义却不大,因此不如重新执行一次搜索。
5、搜索需要遍历分片所有的Lucene分段,因此合并Lucene分段对搜索性能有好处。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u012133048/article/details/91344586
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢