在舆情分析的应用场景中,数据规模通常在千亿以上。使用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内返回结果。以上问题,在实时交互的要求下,都变得格外严重。
总结一下:
说了这么多,就是资源不足。任何问题都可以通过加资源解决。问题是资源非常昂贵,所以我们要做的是,在有限的资源条件下,做无限的优化。
先看看为什么match_phrase慢。
match_phrase 查询是 Elasticsearch 中的一种查询类型,它用于精确匹配包含一组特定词汇的文档。具体来说,match_phrase 查询会找到那些包含特定词组、并且词组中的单词以正确的顺序出现在文档中的文档。
es组织数据的方式只有一种,那就是切分词语,然后保存在倒排表中。理论上来说,性能最佳的一定是,根据一个词语,这个词语尽可能的短。然后做term查询。因为它只需要
Query Parsing:将用户输入的查询字符串解析为相应的查询语法。在解析的过程中,Elasticsearch会根据匹配类型、搜索字段、匹配条件等信息,生成相应的查询语句。例如,对于 match phrase 查询,Elasticsearch会生成一个 MatchPhraseQuery 对象,并将查询字符串作为参数传递给该对象。将查询语句解析成内部查询对象(Query Object),并进行语法和语义检查。在这个阶段,Elasticsearch会使用查询解析器(Query Parser)将查询语句解析成查询对象。
Query Optimization:对内部查询对象进行优化,以提高查询性能。这个阶段包括优化查询逻辑、合并重复查询、减少查询范围等操作。在优化阶段,Elasticsearch会使用查询优化器(Query Optimizer)对查询对象进行优化。
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等。
Scoring:根据相关性得分对文档进行排序,得出最终的搜索结果。在计算相关性得分阶段,Elasticsearch会使用BM25算法或TF-IDF算法等机器学习模型来计算相关性得分。同时,Elasticsearch还会使用查询向量(Query Vector)和文档向量(Document Vector)等技术来计算相关性得分。在计算相关性得分时,Elasticsearch会使用.nvd文件和.nvm文件中保存的词项相关性得分信息。.nvd文件中保存的是词项的文档频率和逆文档频率信息,.nvm文件中保存的是每个文档的长度和平方和等信息。这些信息会被用于计算查询词项的相关性得分。得分高的文档会排在搜索结果的前面。计算查询结果的相关性得分(Relevance Score),并按照相关性得分进行排序。
删除文档处理:Elasticsearch 会根据.liv文件和.del文件中的信息,处理被标记为删除的文档。.liv文件用于表示哪些文档是存在的(live),哪些文档已经被标记为删除(deleted)。.del文件则是用于存储已经被标记为删除的文档的信息。在搜索时,Elasticsearch会使用.liv文件来跳过已经被标记为删除的文档,确保这些文档不会被包含在搜索结果中。
缓存处理:为了提高搜索性能,Elasticsearch会将一些查询结果缓存到内存中,以便快速响应后续的查询请求。缓存处理包括两个方面,一是根据查询语句生成查询缓存(Query Cache),二是根据文档ID生成过滤缓存(Filter Cache)。查询缓存用于缓存查询语句匹配到的文档ID,过滤缓存用于缓存文档ID的过滤结果。这些缓存会被存储到内存中或者磁盘中,以便后续查询时使用。
结果返回:最后,Elasticsearch将匹配的文档ID返回给用户,并根据需要返回文档的内容。返回的文档内容从.fdt文件中读取。.tvx和.tvd文件用于提供快速访问文档中字段的信息,以便在返回结果时能够快速获取相应的字段值。
在 Elasticsearch 中,处理 match_phrase 查询的源码主要分布在以下几个文件中:
org.elasticsearch.index.query.MatchPhraseQueryBuilder:这个类定义了 match_phrase 查询的查询语句结构。它继承自 org.elasticsearch.index.query.MatchQueryBuilder 类,实现了查询的解析、构建和执行等操作。
org.elasticsearch.index.query.MatchPhraseQueryParser:这个类用于解析 match_phrase 查询语句,生成 MatchPhraseQueryBuilder 对象。它实现了 org.elasticsearch.index.query.QueryParser 接口,可以通过 Elasticsearch 的查询解析器来调用。
org.elasticsearch.index.mapper.TextFieldMapper:这个类用于定义文本字段的映射规则。它实现了 org.elasticsearch.index.mapper.Mapper 接口,并且包含了诸如解析文本、分词、建立倒排索引等操作的实现。
org.elasticsearch.index.search.MatchPhraseQuery:这个类是 match_phrase 查询的实现类。它继承自 org.apache.lucene.search.MultiTermQuery 类,实现了查询的匹配和评分等操作。
这些文件的源码可以在 Elasticsearch 的 Github 仓库中找到。具体来说,你可以前往以下链接找到这些文件:
MatchPhraseQueryBuilder:https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/index/query/MatchPhraseQueryBuilder.java
MatchPhraseQueryParser:https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/index/query/MatchPhraseQueryParser.java
TextFieldMapper:https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java
MatchPhraseQuery:https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/index/search/MatchPhraseQuery.java
请注意,以上文件是 Elasticsearch 7.x 版本中的源码,如果你使用的是其他版本的 Elasticsearch,可能需要到对应版本的仓库中查找相应的文件。
Lucene Query Syntax:match_phrase 查询是基于 Lucene Query Syntax 的一种查询类型。Lucene Query Syntax 是一种用于构建查询表达式的语法,它支持诸如 AND、OR、NOT、*、? 等查询操作符,并且可以使用括号、引号等来调整查询的优先级和逻辑。这个语法不仅可以在 Elasticsearch 中使用,也可以在其他基于 Lucene 的搜索引擎中使用,如 Solr、Amazon CloudSearch 等。
Elasticsearch REST API:Elasticsearch 提供了 REST API 接口来管理和操作 Elasticsearch 集群。你可以使用 REST API 来执行 match_phrase 查询,例如使用 HTTP POST 方法来向 /{index}/_search 请求发送一个 JSON 查询语句,其中包含 match_phrase 查询条件。Elasticsearch REST API 还提供了一些参数和选项,用于控制查询的行为和结果格式。
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的研究。最近也在研究,等有成果了,再补充分享。