ElasticSearch Query_string + match_phrase 在千亿级检索中的思考

Posted 水的精神

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ElasticSearch Query_string + match_phrase 在千亿级检索中的思考相关的知识,希望对你有一定的参考价值。

在舆情分析的应用场景中,数据规模通常在千亿以上。使用Elasticsearch 去构建搜索引擎,做相关的分析,面临着非常多的挑战。
先介绍一下,在舆情分析场景中,要用到的是 match phrase语法,针对文章做精准的句子匹配!
在这篇文章中:
1.我会先讲一下我们面临的挑战;
2.接着我会带着问题,分析一下 match phrase语法的检索过程;
3. 偏向底层的原理。
4.根据检索原理,考虑可以做哪些优化;
5.以及针对我们面临的挑战,我的一些优化方法。

目标

探索ES在千亿规模数据的检索场景下,句子精准匹配的性能优化方案。在实时交互的场景中,应对这么多的检索,达到注重3秒内的目标。本文会先讲一下,在舆情分析场景下使用ES做检索面临的诸多挑战。接着会对ES的检索原理做一个深度的拆解。

挑战背景

  • 数据规模大,通常舆情分析,至少要在三个月的范围内做检索。加入将互联网上现有的所有的媒体数据拿到,是非常多的。例如微博、抖音、facebook、推特、等等。日均数据量假如都放在es中,可能少说也有N T(至少要在2T)。 假如在这样的数据规模中,三个月的数据就是 90 * N T 。通常我们会有一些数据是在一年范围内,甚至两年范围内做检索。那就是 365 * 2 * N T。这个规模去做实时交互,且想在个位数的秒级别中获取结果,需要非常多的计算资源。通常,一个es实例,可以高效的运行2T的数据。超过就性能会有下降。因为买不起机器,我们单台机器负载8T的数据。

  • 要用到句子的精准匹配。通常在舆情检索场景中,用的并不是es的相关性检索,而是句子的精准匹配。这就不得不去使用ES中的 match_phrase 语法。去做句子的精准匹配。熟悉es的同学,应该会知道,es最擅长的是做term查询,其次是match 搜索,然后才是 match_phrase。 这就相当于是做的 like %中国%的检索。

  • 检索条件复杂,检索的关键词多。通常要用很多的must 和must not,查询语句中包含多个操作符、子句和过滤器。也就是在一波检索中,可能要输出100+的检索词。所以这就不得不去使用 query string 搜索语法,且匹配的模式用 phrase(和match_phrase)一样的逻辑。

  • 要命中全量的数据。在问答系统中,就像百度谷歌,只需要要返回与问题最相关的答案即可,通常在舆情场景下,要命中全量的数据。因为用户通常想要这个条件命中了多少条结果。

  • 要有非常多的聚合分析。es聚合分析的性能并不高。因为它需要大量的CPU和磁盘的IO。在数据规模远超机器规模的情况下。整体的检索效果会非常的差。我有去考虑够其他类型的擅长做OLAP类型的数据库,例如CK,奈何它做句子匹配不太行。

  • 在非结构化的数据中做检索。通常是在文章中做检索,而不是在某个字段中做检索。这回让检索变得格外的难。在做聚合分析的时候,像CK这样的数据库完全用不上。

  • 实时交互。尽可能在3-5s内返回结果。以上问题,在实时交互的要求下,都变得格外严重。

总结一下:

说了这么多,就是资源不足。任何问题都可以通过加资源解决。问题是资源非常昂贵,所以我们要做的是,在有限的资源条件下,做无限的优化。

ES的 match_phrase的检索原理

先看看为什么match_phrase慢。

match_phrase 查询是 Elasticsearch 中的一种查询类型,它用于精确匹配包含一组特定词汇的文档。具体来说,match_phrase 查询会找到那些包含特定词组、并且词组中的单词以正确的顺序出现在文档中的文档。
es组织数据的方式只有一种,那就是切分词语,然后保存在倒排表中。理论上来说,性能最佳的一定是,根据一个词语,这个词语尽可能的短。然后做term查询。因为它只需要

match_phrase 查询的过程分析

  1. Query Parsing:将用户输入的查询字符串解析为相应的查询语法。在解析的过程中,Elasticsearch会根据匹配类型、搜索字段、匹配条件等信息,生成相应的查询语句。例如,对于 match phrase 查询,Elasticsearch会生成一个 MatchPhraseQuery 对象,并将查询字符串作为参数传递给该对象。将查询语句解析成内部查询对象(Query Object),并进行语法和语义检查。在这个阶段,Elasticsearch会使用查询解析器(Query Parser)将查询语句解析成查询对象。

  1. Query Optimization:对内部查询对象进行优化,以提高查询性能。这个阶段包括优化查询逻辑、合并重复查询、减少查询范围等操作。在优化阶段,Elasticsearch会使用查询优化器(Query Optimizer)对查询对象进行优化。

  1. Query Execution:执行查询操作,将查询对象转换成对倒排索引(Inverted Index)进行搜索的操作。在执行查询阶段,Elasticsearch会使用倒排索引(Inverted Index)来查找符合查询条件的文档。具体地,Elasticsearch会使用以下文件进行查询操作:

  • .tim文件和.tip文件:这两个文件保存了词项的位置信息,用于在倒排索引中定位词项所在的文档。

  • .doc文件:这个文件保存了文档ID和词项的位置信息,用于在倒排索引中定位词项所在的文档。

  • .pos文件和.pay文件:这两个文件保存了词项在文档中的位置信息和附加信息,用于在倒排索引中计算相关性得分。

  • .liv文件和.del文件:这两个文件用于表示文档是否存在和是否被标记为删除。

  • .fdt文件和.fdx文件:这两个文件用于存储文档的内容和结构信息。

  • .nvd文件和.nvm文件:这两个文件保存了词项的文档频率、逆文档频率和文档长度等信息,用于在计算相关性得分时进行加权。

  • .tvx文件和.tvd文件:这两个文件用于提供快速访问文档中字段的信息。

在查询执行阶段,Elasticsearch会使用以上文件进行查询操作,从而找到符合查询条件的文档ID。为了提高查询性能,Elasticsearch还会使用一些技术,如布尔运算优化、term lookup optimization等。

  1. Scoring:根据相关性得分对文档进行排序,得出最终的搜索结果。在计算相关性得分阶段,Elasticsearch会使用BM25算法或TF-IDF算法等机器学习模型来计算相关性得分。同时,Elasticsearch还会使用查询向量(Query Vector)和文档向量(Document Vector)等技术来计算相关性得分。在计算相关性得分时,Elasticsearch会使用.nvd文件和.nvm文件中保存的词项相关性得分信息。.nvd文件中保存的是词项的文档频率和逆文档频率信息,.nvm文件中保存的是每个文档的长度和平方和等信息。这些信息会被用于计算查询词项的相关性得分。得分高的文档会排在搜索结果的前面。计算查询结果的相关性得分(Relevance Score),并按照相关性得分进行排序。

  1. 删除文档处理:Elasticsearch 会根据.liv文件和.del文件中的信息,处理被标记为删除的文档。.liv文件用于表示哪些文档是存在的(live),哪些文档已经被标记为删除(deleted)。.del文件则是用于存储已经被标记为删除的文档的信息。在搜索时,Elasticsearch会使用.liv文件来跳过已经被标记为删除的文档,确保这些文档不会被包含在搜索结果中。

  1. 缓存处理:为了提高搜索性能,Elasticsearch会将一些查询结果缓存到内存中,以便快速响应后续的查询请求。缓存处理包括两个方面,一是根据查询语句生成查询缓存(Query Cache),二是根据文档ID生成过滤缓存(Filter Cache)。查询缓存用于缓存查询语句匹配到的文档ID,过滤缓存用于缓存文档ID的过滤结果。这些缓存会被存储到内存中或者磁盘中,以便后续查询时使用。

  1. 结果返回:最后,Elasticsearch将匹配的文档ID返回给用户,并根据需要返回文档的内容。返回的文档内容从.fdt文件中读取。.tvx.tvd文件用于提供快速访问文档中字段的信息,以便在返回结果时能够快速获取相应的字段值。

match_phrase 查询的源码解析-在es中的源码

在 Elasticsearch 中,处理 match_phrase 查询的源码主要分布在以下几个文件中:

  1. org.elasticsearch.index.query.MatchPhraseQueryBuilder:这个类定义了 match_phrase 查询的查询语句结构。它继承自 org.elasticsearch.index.query.MatchQueryBuilder 类,实现了查询的解析、构建和执行等操作。

  1. org.elasticsearch.index.query.MatchPhraseQueryParser:这个类用于解析 match_phrase 查询语句,生成 MatchPhraseQueryBuilder 对象。它实现了 org.elasticsearch.index.query.QueryParser 接口,可以通过 Elasticsearch 的查询解析器来调用。

  1. org.elasticsearch.index.mapper.TextFieldMapper:这个类用于定义文本字段的映射规则。它实现了 org.elasticsearch.index.mapper.Mapper 接口,并且包含了诸如解析文本、分词、建立倒排索引等操作的实现。

  1. org.elasticsearch.index.search.MatchPhraseQuery:这个类是 match_phrase 查询的实现类。它继承自 org.apache.lucene.search.MultiTermQuery 类,实现了查询的匹配和评分等操作。

这些文件的源码可以在 Elasticsearch 的 Github 仓库中找到。具体来说,你可以前往以下链接找到这些文件:

请注意,以上文件是 Elasticsearch 7.x 版本中的源码,如果你使用的是其他版本的 Elasticsearch,可能需要到对应版本的仓库中查找相应的文件。

match_phrase 查询的源码解析-在lucene中

  1. Lucene Query Syntax:match_phrase 查询是基于 Lucene Query Syntax 的一种查询类型。Lucene Query Syntax 是一种用于构建查询表达式的语法,它支持诸如 AND、OR、NOT、*、? 等查询操作符,并且可以使用括号、引号等来调整查询的优先级和逻辑。这个语法不仅可以在 Elasticsearch 中使用,也可以在其他基于 Lucene 的搜索引擎中使用,如 Solr、Amazon CloudSearch 等。

  1. Elasticsearch REST API:Elasticsearch 提供了 REST API 接口来管理和操作 Elasticsearch 集群。你可以使用 REST API 来执行 match_phrase 查询,例如使用 HTTP POST 方法来向 /index/_search 请求发送一个 JSON 查询语句,其中包含 match_phrase 查询条件。Elasticsearch REST API 还提供了一些参数和选项,用于控制查询的行为和结果格式。

  1. Elasticsearch Java API:Elasticsearch Java API 是 Elasticsearch 官方提供的 Java 客户端库,它提供了一组 Java 接口和类,用于连接和操作 Elasticsearch 集群。你可以使用 Java API 来执行 match_phrase 查询,例如使用 MatchPhraseQueryBuilder 类来构建查询语句,然后使用 SearchRequest 类来执行查询,并处理返回的查询结果。

以上这些工具和库都涉及到 match_phrase 查询的使用和实现,可以帮助你更加深入地了解和应用 match_phrase 查询。你可以查阅 Elasticsearch 的官方文档和相关教程,学习如何使用和优化 match_phrase 查询。

我能想到的优化方案

构建一个不错规模的集群,这就需要合理规划集群规模。充分的资源,一定能够解决这些问题。

在挑战背景中,已经列出来的问题,从问题的本质出发,去尝试解决问题。

  • 数据剪枝。在这样的规模下,在资源有限的情况下。应该从数据剪枝的角度出发,去减少数据量,降规模。整体的方向,是使用异构来减少原数据的存储,把es只当做索引引擎,而不是取数据的地方。降低单词检索的数据范围,调整数据组织方式,例如按照时间去分区数据。降低搜索命中的数据,搜索条件应该尽可能的精简。

  • 分段请求:数据规模大,且交互方式是实时交互,用户期望在3-5秒内得到相应。从数据规格大的角度出发,只能想想如何减少数据规模。这其实就是数据剪枝的思路。 如果是为了交互,可以设计一个交互模式,将扫描全量数据的思路,变成扫描部分数据的思路。这样就可以把检索 A B C三个月的逻辑,拆分成,检索A月的数据,将结果提前返回,此时能尽可能的满足 3-5s内得到响应的需求。假如A月已经满足了用户的查看需求,那么BC就不用检索了。在乐观的情况下,检索3个月,就变成了检索一个月。这样数据规模就只有三分之一了。 这个思路下的难题是,排序,如果想基于全局的排序,应该怎么做。假如是基于时间的排序,不用担心。假如需要根据点赞量,转发量排序,就不行了。分段请求的好处:分段匹配:可以将长句子切分成多个段落,分别进行匹配,最后将结果合并。这种方式可以有效降低内存消耗,同时还可以利用多线程并发处理,提高查询性能

  • 尝试跳过打分过程,也及时去掉相关性的计算。在Elasticsearch中可以通过设置相关参数来跳过打分的过程,从而加快查询速度。具体来说,可以使用bool查询的constant_score过滤器来跳过打分过程,该过滤器将所有符合条件的文档赋予一个固定的分值,不再计算相关性得分。使用该过滤器可以在一些特定场景下提升查询性能,如过滤掉某些不符合条件的文档等。同时,也可以通过修改查询参数中的boost值来影响相关性得分,从而控制文档在查询结果中的排名。

  • 要命中全量的数据。应该用异步的方式,将count值单独返回。其实很多分析类的请求,是可以朝着异步或者离线的方向寻找出路的。大量的聚合分析,会花费大量的资源,很难在5s内得到响应。

  • 优化分词器,分词器选择:对于长句子的匹配,可以选择适合的分词器,如N-gram分词器、Edge N-gram分词器等,将长句子切分成若干短语进行匹配,从而减小内存消耗和查询时间。

  • 还需要继续整理。我始终觉得在很多个关键词匹配的过程中,倒排链的合并过程可以优化。这一块后续会研究,等有成果了,再分享出来。

  • 在缓存利用率方向,做探索,把最需要的东西放在缓存里边。

  • 底层IO的研究。最近也在研究,等有成果了,再补充分享。

Elasticsearch:以更简单的方式编写具有逻辑条件的 Elasticsearch 查询 - query_string

当涉及到诸如 NOT、AND 和 OR 之类的布尔运算时,我们通常使用带有 must、should、must_not 子句的布尔查询。 是的,bool 查询非常强大,可用于执行所有类型的高级搜索。 但是,对于具有基本 NOT、AND 和 OR 条件的简单搜索,使用 bool 查询有点矫枉过正,因为你需要编写大量样板代码。 这是 query_string 查询适合的地方,因为它具有更简单的语法。针对 query_string,在我的另外一篇文章 “Elasticsearch: query_string 查询” 有详细介绍。

准备数据

如果你还没有准备好你的数据,请阅读我之前的文章 “Elasticsearch:通过例子快速入门”。我们通过文章里介绍的方法把索引  laptops-demo 建立起来。这里不再累述了。

NOT operation

让我们找出 brand 不是 HP 的笔记本电脑。 配合 bool 查询,代码如下:

GET laptops-demo/_search

  "query": 
    "bool": 
      "must_not": [
        
          "match": 
            "brand": "hp"
          
        
      ]
    
  

如你所见,我们需要使用 bool/must_not 样板代码(以及大量括号)来编写具有简单 NOT 条件的查询。 使用 query_string 查询,语法更简单、更灵活。 以下所有将起作用并给出相同的结果:

GET laptops-demo/_search

  "query": 
    "query_string": 
      "default_field": "brand",
      "query": "NOT hp"
    
  


GET laptops-demo/_search

  "query": 
    "query_string": 
      "default_field": "brand",
      "query": "!hp"
    
  


GET laptops-demo/_search

  "query": 
    "query_string": 
      "query": "brand:(NOT hp)"
    
  


GET laptops-demo/_search

  "query": 
    "query_string": 
      "query": "brand:(!hp)"
    
  
  • 要搜索的字段可以使用 default_fieldfields 参数指定,或直接在查询中指定。 当在查询中指定时,它后面应该跟一个冒号,布尔条件应该放在括号中。
  • 使用 query_string 以更简单的方式指定布尔条件。 例如,你可以使用 NOT 或感叹号 ! 指定 NOT 条件。 当存在 AND 或 OR 条件时,它会更加突出。

AND operation

让我们找到 brand 为 HP 且名称中包含 EliteBook 的笔记本电脑:

GET laptops-demo/_search

  "query": 
    "bool": 
      "must": [
        
          "match": 
            "brand": "hp"
          
        ,
        
          "match": 
            "name": "elitebook"
          
        
      ]
    
  

嗯,这里有点失控了,因为你需要非常小心嵌套查询,不要错过任何方括号或大括号。 同样,使用 query_string 更简单:

GET laptops-demo/_search

  "query": 
    "query_string": 
      "query": "brand: hp AND name: elitebook"
    
  

哇,这简单多了,不是吗? 没有样板代码和带有令人眼花缭乱的括号的疯狂嵌套。

请注意,当使用 query_string 时,如果查询具有逻辑条件,则需要将查询放在括号中。 例如,我们只列出 brand 为 HP 但不是 EliteBook 的笔记本电脑:

GET laptops-demo/_search

  "query": 
    "query_string": 
      "query": "brand: hp AND name: (NOT elitebook)"
    
  

它非常简单易读。 如果使用 bool 查询,则需要同时使用 must 和 must_not 子句,这会变得更加复杂:

GET laptops-demo/_search

  "query": 
    "bool": 
      "must": [
        
          "match": 
            "brand": "hp"
          
        
      ],
      "must_not": [
        
          "match": 
            "name": "elitebook"
          
        
      ]
    
  

OR operation

为了完整起见,让我们检查 OR 条件并找到 brand 为 Dell 或 name 中有 EliteBook 的笔记本电脑(仅供演示 😃)。 对于 bool 查询,你需要使用 should 子句:

GET laptops-demo/_search

  "query": 
    "bool": 
      "should": [
        
          "match": 
            "brand": "dell"
          
        ,
        
          "match": 
            "name": "elitebook"
          
        
      ]
    
  

使用 query_string 查询,就简单多了:

GET laptops-demo/_search

  "query": 
    "query_string": 
      "query": "brand: dell OR name: elitebook"
    
  

我认为你已经明白使用 query_string 比使用 bool 查询要简单得多。 我们刚刚介绍了 query_string 查询的本质,还有很多其他有用的功能可以在官方文档中找到。 但是,只有在需要使用更高级的功能时才需要学习它们。

尽管 query_string 对于简单查询非常方便,但如官方文档所述,它不方便也不推荐用于 nested 文档。 对于 nested 文档的查询,最好使用这篇文章中更详细演示的 nested 查询。

以上是关于ElasticSearch Query_string + match_phrase 在千亿级检索中的思考的主要内容,如果未能解决你的问题,请参考以下文章

PHP获取路径

php 获取URL

$_SRRVER常用用法

PHP获取当前页面地址

用html或配合JS获取来路域名怎么写

Elasticsearch 教程