概述

在昨天员外分享的《TorchV的RAG实践分享(1)——RAG的定位、技术选型和RAG技术文章目录》一文中介绍了TorchV的由来,也分享了我们的几个基线产品和应用架构的方向,我们想的是在创业的过程中,将我们自己的一些产品理念、技术心得都通过公众号发文的方式分享出来,更多的和行业内的专家们共同交流,这对我们自己也是一种提升和锻炼,也期待和客户一起共创成长,逐步把产品打磨好。

在目前大模型应用技术架构中,通过召回上下文来回答用户的问题是解决大模型当下的幻觉问题最靠谱/经济实惠的一种解决方案,RAG检索增强技术在整个LLM技术架构体系中的作用越来越明显。而检索召回和用户的query问句的质量则直接关系到最终大模型的生成结果。在向量数据库基础设施普及的今天,仅仅通过语义搜索召回已经无法满足企业级的需求,大家发现传统的搜索技术(基于关键词、词频等相关性的搜索)的作用也显得尤为重要,混合检索也成为了目前在RAG的技术架构中的主流检索方式,混合检索通过扬长避短的方式,在不同的业务应用场景中形成了很好的互补,对于不同的业务场景需求中,可以更灵活的进行配置满足业务需要,是RAG技术架构体系中非常重要的重要一环。

本文中所提到的混合检索主要是两种搜索技术的结合,主要如下:

  • 相关性搜索: 基于BM25、TF-IDF算法,主要适用于文本精确匹配的相关性匹配搜索,它在匹配特定术语(如产品名或专业术语)方面表现出色,但对拼写错误和同义词较为敏感,可能会忽略一些重要的上下文信息。
  • 语义搜索: 基于向量的Knn算法进行的语义检索,它能够基于用户的query语义含义进行多语言和多模态搜索,对拼写错误具有较好的容错性,但可能会忽视关键词。此外,它的效果依赖于向量嵌入的质量,并对非专业领域的术语较为敏感。

本文针对ElasticSearch中间件来实现整个外部知识库向量的存储和计算,在RAG技术架构中的混合检索进行探索和分析,结合我们自己的实际业务情况,如何通过底层的技术驱动,完善我们的产品设计,改善整个产品流程。

整篇文章主要包括:

  • 简介:简要概述ElasticSearch中间件以及在RAG技术架构的选型及实现
  • 算法理论:参数在混合检索过程中涉及的算法理论知识,面向的业务场景及选择方式
  • 召回Score分值计算:讲解ElasticSearch组件在召回计算过程中的Score分值规则及算法细节
  • TorchV产品驱动:技术推动我们TorchV产品的产品架构设计,如何影响产品流程
  • 结论:整篇的总结概述及参考文章

ElasticSearch简介

在介绍ElasticSearch的混合检索之前,我们需要先简单回顾ElasticSearch这个中间件如何在目前AI技术场景的落地情况

在目前的RAG大模型技术架构体系中,向量Vector技术已经作为大模型外挂知识库的非常重要的技术栈,向量的核心对于数据的表征(Embedding)然后执行相似度(Similarity)计算。2023年随着大模型技术的发布带火了非常多的向量数据库,特别是LangChainllama_index等LLM数据应用框架的发布,包括:MilvusQdrantPineconeChroma等等专业的向量数据库中间件。向量数据结构的存储与计算可以说是当前做大模型应用的基建产品了,就好像传统软件工程中的数据库一样。

而对于ElasticSearch而言也同样如此,对于之前使用ElasticSearch中间件的开发人员而言,可能对于向量数据的存储和计算是比较陌生的,在传统软件工程用ES来储存搜索主要还是基于关键词搜索技术(BM25、TF-IDF)等实现,本质还是基于文本的精确匹配。而在最近ES组件发布的版本来看,特别是ES 8.0版本发布对于KNN算法搜索的优化支持来看,AI大模型这场技术革命风暴,似乎也不想袖手旁观。

我们选择ElasticSearch作为TorchV的基础RAG架构组件也是出于以下几个方面考虑:

  • 开箱即用的语义搜索功能以及一流的相关性检索(BM25/TF-IDF)算法实现
  • 区别于其它向量数据库所不具备的特有功能,包括:聚合、过滤、集群、分布式等等特性。
  • 多年的技术沉淀和社区发展,不同编程语言的生态完善成熟度等

在ElasticSearch的目前的版本中,要使用向量实现存储和计算对于开发者使用上非常简单,开发者在定义ES的索引结构时,定义向量字段类型dense_vector,并且自定义向量维度dims(最大维度不超过4096(自8.x版本开始)),如下索引结构:

PUT test-001
{
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 3
},
"my_text" : {
"type" : "keyword"
}
}
}
}

在执行搜索时则可以通过k-最近邻(KNN)搜索找到与查询向量最近的K个向量结果值来获取结果,通过相似度值来衡量获取文档片段。

GET test-001/_knn_search
{
"knn": {
"field": "my_vector",
"query_vector": [0.3, 0.1, 1.2],
"k": 10,
"num_candidates": 100
},
"_source": ["name", "date"]
}

而我们在前面提到,混合检索(语义搜索+相关性搜索)是目前做RAG的非常重要文档召回技术手段,纯KNN搜索并不能完全满足业务的需求,因此在当前的RAG技术架构体系中,ES在保持传统相关性搜索的基础上,增加对语义搜索的技术支持就显得很有冲击力,毕竟在向量搜索火爆之前,ES作为搜索引擎的老大哥,在企业级的产品应用体系中,应用范围还是非常广泛的。

算法&业务场景

在做混合检索时,我们会接触到两类算法,需要对算法有一个基础了解,这有助于我们在应用产品的技术体系中做决策:

  • 语义检索:基于向量空间的KNN算法
  • 相关性检索:传统的文本精确匹配方法,包括BM25、TF-IDF

语义检索(knn)

KNN算法:k近邻算法,是机器学习算法中一种基本分类和回归方法。在给定的一个数据集中,对于新的数据实例,找到与该实例最邻近的k个实例,这k个实例的多数属于某个分类。

这就像你在一个陌生的城市,你可能会问周围的k个人哪家餐馆最好。如果大多数人都推荐同一家餐馆,那么你可能会选择去那家餐馆。

而我们在选择餐馆的过程中,每一个餐馆会有非常多的维度来描述这个餐馆的信息,包括:地理位置、菜系、价格、环境、口味等等,这一系列参数属性就是特征工程,目前的向量Embedding模型用来对一段文本进行Embedding,其实就是对于该文本内容的的特征信息进行提取描述。

这个时候,你会根据你自己的诉求,对于餐馆的不同特征要求,最终选择你要去哪家餐馆吃饭。

在Elasticsearch中,KNN搜索主要通过使用向量相似度(特征空间中的两个实例点间的距离可以反映出两点间的相似程度)进行度量,文档根据向量数据集与查询向量的相似度进行排名。每个文档的 _score 将从相似度中得出,以确保分数为正并且分数越高对应于越高的排名。

ES目前主要提供了三种度量的标准供我们选择(考虑到本文是基于es,因此也只对该三种度量标准做介绍,对于其它的向量计算距离的方式,开发者可以自行搜索了解)

  • L2_norm(欧式距离):这是最常用的距离度量方式,它计算的是两个向量在笛卡尔坐标系中的直线距离。文档的score计算方式为:1 / (1 + l2_norm(query, vector)^2)
  • dot_product(点积):点积是两个向量的对应元素相乘然后求和,文档 _score 计算为 (1 + dot_product(query, vector)) / 2
  • cosine(余弦相似度,default):计算两个向量余弦相似度,余弦相似性度量的是两个向量之间的角度,而不是距离。它的值范围是 -1 到 1,值越接近 1,表示两个向量越相似,文档 _score 计算为 (1 + cosine(query, vector)) / 2

我们在开发RAG的大模型应用产品中,通常会将外部的知识库通过chunk分段存储处理,对于用户的query,通过Embedding模型进行表征为向量后,与chunk片段的向量进行距离计算,此时作为距离度量的方式考虑,那么根据实际的业务场景,就可以考虑上面的三种类型中的一种。

一般默认选择cosine余弦相似度进行计算召回,主要考虑:

  • 长度不敏感:在文本数据中,文档的长度可能会有很大的差异,这会影响到向量的长度。余弦相似性只关注向量的方向,而不关注长度,因此它对尺度不敏感,适合处理这种情况(虽然我们在使用向量Embedding模型进行表征时,向量的维度都是固定的)。
  • 方向敏感:在问答系统中,我们通常关心的是文档的主题或者内容是否相似,而不是文档的长度。余弦相似性度量的是两个向量之间的角度,可以很好地反映出文档的主题或者内容是否相似。
  • 高维数据:向量Embedding模型表征的高维度(768/1024/1536…等等)向量,适合余弦相似性适合处理这种高维稀疏的数据。

而ES自8.0版本发布后,同样也提供了对KNN搜索的优化,主要提供了两种策略:

  • 近邻KNN搜索算法(ANN):数据结构基于HNSW算法索引实现,近似 kNN 提供较低的延迟,但代价是索引速度较慢且准确性不完善(这也为后来RAG架构中的文档检索结果做ReRank重排埋下伏笔,可以关注员外的这篇《Rerank——RAG中百尺竿头更进一步的神器,从原理到解决方案》)。
  • 精确、强力的 kNN搜索(暴力搜索):基于函数实现,这种方式能够保证结果的准确性,通过计算script_score 函数扫描每个匹配文档计算向量距离获取文档结果集,这会导致搜索速度缓慢(大数据集的应用场景下)。

开发者在选择KNN搜索的算法策略时,可以根据自己的实际业务需要进行抉择。

相关性检索(BM25/TF-IDF)

ES自5.0版本之后,针对文档的相关性评分机制默认采用了BM25相似度算法(之前是基于TF-IDF),BM25全称Okapi BM25。Okapi 是使用它的第一个系统的名称,即Okapi信息检索系统,于 20 世纪 80 年代和 1990 年代在伦敦城市大学实施。 BM则是best matching的缩写。

因此对于词的相关性检索方案,我们对于TF-IDF和BM25也需要有一个基础的了解。

TF-IDF(Term Frequency-Inverse Document Frequency):词频-逆文档频率是一个常用于信息检索和文本挖掘的权重计算方法,函数公式如下:

主要由两部分组成:

  • TF(Term Frequency,词频):衡量一个词在文档中出现的频率。假设某一词条在文本中出现的次数为n,文本的总词条数为m,则词频TF为n/m,也就是词频,比如一个单词:旅游在我们的一篇文档中出现了4次,而我们的文档总共包含的词条数量是100,那么词频的值就是4/100。词频越高,说明这个词在文档中越重要。
  • IDF(Inverse Document Frequency,逆文档频率):衡量一个词是否常见的度量。如果某个词在很多文档中都出现,那么它可能就不具有区分能力(比如常用词等)。它的计算公式是:log(文档总数(N) / 包含该词的文档数(df))。逆文档频率越大,说明这个词越不常见,可能就越重要。

TF-IDF就是将这两个值相乘,得到的结果就是一个词的权重,这个权重可以用来表示这个词对于文档的重要性,也可以用来比较不同文档的相似性。

TF-IDF在实践的发展中会存在一些问题:

  • 文档长度问题:在长文档中,某个词可能会因为文档本身的长度就有更高的出现次数,而不是因为这个词对于文档的主题更重要。这可能会导致TF-IDF对长文档中的词给予过高的权重,而忽视了短文档中的重要词
  • 词频不饱和:在实际的业务场景中,词的重要性并不总是随着它的出现次数线性增加的。例如,一个词在文档中出现100次可能并不意味着它比出现10次的词10倍重要,相反,对于IDF而言,如果一个词在文档集中出现的次数越少,那么它的IDF值就越高,被认为越重要,也并非一定符合实际业务场景。

这些问题都在BM25中得到了改进,BM25的词频部分使用了一个饱和函数,使得词频达到一定值后,增加词频对于最终得分的影响会变小。同时,BM25还考虑了文档长度的影响,通过一个归一化因子来调整不同长度文档中的词频。这使得BM25在处理词频未饱和和文档长度问题时,比TF-IDF有更好的性能。

BM25 的计算公式:

和TF-IDF的计算公式相比,BM25的公式着实有点吓人,不过其实我们关注几个核心的参数即可。

对于BM25算法在ElasticSearch中的应用公式和参数变量说明,可以参考这篇文章《BM25 算法及其变量》,这里我们只关心几个核心的参数

  • k1:控制非线性项频率归一化(词频饱和度)。默认值为 1.2 。较低的值导致较快的饱和,较高的值导致较慢的饱和。

  • b:该参数控制字段长度归一化的影响程度。b的值在0到1之间,当b为0时,表示完全不考虑字段长度的影响;当b为1时,表示完全考虑字段长度的影响。默认为 0.75 。这个参数值也是针对上面我们提到TF-IDF在文档长度未考虑的情况下一个加权计算,当然默认值0.75是官方基于大量的数据实验得到的一个值,在默认场景下都会有较好的效果,我们可以不用调整。如果我们的默认检索效果不佳,应该从其它方面来考虑优化,这个后面我们再说

Score分值计算&注意事项

在理解了算法、es中间件之后,结合实战+Score分值的计算使用过程,包括配合ES的Explain接口,讲清楚Score的计算规则,原理

在前面了解了ES的整个检索Score算法介绍之后,其实对于文本内容的检索召回Score分值计算,就比较清晰了,先说结论:

ElasticSearch在使用KNN+BM25检索的混合检索分值Score计算公式是:knn_score+bm25_score

使用ES混合检索的语法如下:

POST image-index/_search
{
"query": {
"match": {
"title": {
"query": "mountain lake",
"boost": 0.9
}
}
},
"knn": {
// 字段
"field": "image-vector",
// 输入向量
"query_vector": [54, 10, -2],
// k值
"k": 5,
// 每个分片要考虑的最近邻居候选数。不能超过 10,000
"num_candidates": 50,
// 加权参数值
"boost": 0.1,
// 档被视为匹配所需的最小相似度,配合filter使用,提高检索效率
"similarity": 0.7,
// 过滤条件
"filter": {
"term": {
"file-type": "png"
}
}
},
"size": 10
}

query部分的检索所代表的是BM25算法的Score计算分值召回,而knn部分的检索所代表的则是语义向量空间的距离Score分值,最终的结果值相加后倒排的一个文档列表结果集

score=match_score*0.9 + knn_score*0.1

BM25的Score

对于BM25算法的检索分值计算,开发者可以使用Explain API来查看整个Score的计算过程,整个计算过程就和BM25算法公式那样,如下图:

BM25算法会将用户输入的match参数,计算每一个分词的score分值,最终加起来,得到一个总的分值score数据,对于每一个分词,都可以通过该接口查看到完整的计算过程,是非常方便的开发者进行理解的。

在这里进行BM25计算时,我前面提到BM25算法可能存在检索不到最终我们说期望的文本,会有一些其它参数影响最终效果,并非需要更改算法中的k1和b这两个参数,主要考虑如下:

  • ES是一个分布式搜索和分析引擎,数据被分为多个分片(shards),每个分片可以在任何节点上存储。这使得数据可以在多个节点之间进行分布,从而提高系统的容量和性能,最终数据在存储构建索引的时候,es会均衡的进行分布存储,而在召回计算的过程中,数据也会从各个shards分片进行召回计算。开发者在创建索引(index)的时候,可以设置shards的分片为1或者3,来查看区别。
  • es默认提供了非常多的tokenizer分词器,而对于中文用户的使用者来说,哪些词该分,哪些词不该分,包括同义词的影响等等,都会影响整个Score分值的计算,在目前es的生态中,ik分词器可能是当下最成熟的一个plugin插件,ik提供了一个基础的词库,同时支持热更新,对于上层应用产品的设计融合,非常刚需。

KNN的Score

对于KNN的检索分值计算,就非常的简单了,开发者在构建用户索引的时候,选择具体的向量距离类型,es在计算knn的时候,就会根据其算法进行计算

PUT my-index-2
{
"mappings": {
"properties": {
"my_vector": {
"type": "dense_vector",
"dims": 1024,
// 选择类型,cosine、dot_product、l2_norm
"similarity": "cosine"
}
}
}
}

选择不同的类型, 就是单纯的向量距离计算了,按公式套用就可以了。

不过值得注意的是,对于使用最多的cosine的文档 _score 计算为 (1 + cosine(query, vector)) / 2

️注意事项

当我们使用混合检索的时候,有一些注意事项值得我们关注:

  • 开发者在使用Explain API接口进行调试的时候,由于KNN的分值是单独计算,所以在分析的时候,不能有KNN的部分
  • KNN检索的参数,可以配置多个knn的向量查询值,另外filter过滤参数会提高检索的效率,但是提高检索效率的同时,由于总是会计算召回文档进行相似度计算,所以可以配合similarity来一起使用。

TorchV产品驱动&总结

对于混合检索,我们在算法层面有了直接的了解后,最终在产品层面会影响一些设计。

1、混合检索的权重设置:在上面的score分值计算公式中,我们其实知道es最终是通过bm25*boost+knn*boost,那么这个boost则可以影响我们最终的内容,因为并不是所有的客户和业务场景都适合knn检索,可能在其他关键的场景中,关键词检索会更适合(比如一些利用大模型做一些异步的任务提取,报告输出等等业务场景),我们在产品设计中则可以根据不同的客户诉求以及业务诉求,就可以设置这个boost来影响最终的召回结果天平,从而改善我们的产品效果。

在我们的TorchV的产品设计中,我们设计了一个alpha参数值,取值范围在0-1之间,具体来说:

  • alpha = 1:完全基于向量的搜索,也就是KNN近邻搜索
  • alpha = 0:完全基于关键词的搜索,基于ES的BM25算法检索

2、在BM25算法的场景中,分词是非常重要的一个特性,对于不同的行业客户,词库的收集建立对于产品应用的提升肯定是会有质的提升,也是每个公司做RAG产品的核心竞争力。

3、持续运营能力的重要性,RAG问答检索功能在技术架构迭代优化上是一个方面,但是运营能力同样重要,哪怕是ChatGPT4,在针对特殊的数据文件,如果数据混乱,知识库质量不高,那么同样回答准确率不会很好的,这在我们和客户进行沟通交流的同时,虽然RAG可能会给客户眼前一亮的感觉,但是持续的提升他的能力,发挥更大的作用,产品的持续运营能力是必不可少的。

参考

好了,全文完. 如果你也在关注大模型、RAG检索增强生成技术,欢迎关注我(公众号‬:八一菜刀‬)

 

TorchV的RAG实践分享(二):基于ElasticSearch的混合检索实战&原理分析的更多相关文章

  1. 百度APP移动端网络深度优化实践分享(二):网络连接优化篇

    本文由百度技术团队“蔡锐”原创发表于“百度App技术”公众号,原题为<百度App网络深度优化系列<二>连接优化>,感谢原作者的无私分享. 一.前言 在<百度APP移动端网 ...

  2. 【课程分享】基于plusgantt的项目管理系统实战开发(Spring3+JDBC+RMI的架构、自己定义工作流)

    基于plusgantt的项目管理系统实战开发(Spring3+JDBC+RMI的架构.自己定义工作流) 课程讲师:张弘 课程分类:Java 适合人群:中级 课时数量:37课时 用到技术:Spring  ...

  3. 基于Elasticsearch进行地理检索,计算距离值

      实现步骤: 1.定义属性     [Serializable]     public class Coordinate     {         public double Lat { get; ...

  4. 基于web的IM软件通信原理分析

    关于IM(InstantMessaging)即时通信类软件(如微信,QQ),大多数都是桌面应用程序或者native应用较为流行,而网上关于原生IM或桌面IM软件类的通信原理介绍也较多,此处不再赘述.而 ...

  5. 百度APP移动端网络深度优化实践分享(一):DNS优化篇

    本文由百度技术团队“蔡锐”原创发表于“百度App技术”公众号,原题为<百度App网络深度优化系列<一>DNS优化>,感谢原作者的无私分享. 一.前言 网络优化是客户端几大技术方 ...

  6. 百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇

    本文由百度技术团队“蔡锐”原创发表于“百度App技术”公众号,原题为<百度App网络深度优化系列<三>弱网优化>,感谢原作者的无私分享. 一.前言 网络优化解决的核心问题有三个 ...

  7. 【Meetup回顾】Apache DolphinScheduler在联通的实践和二次开发经验分享

    在由 openLooKeng 社区主办,Apahce DolphinScheduler社区.Apache Pulsar 社区.示说网协办的联合 Meetup 上,来自联通数字科技的王兴杰老师分享了Do ...

  8. 基于Sql Server 2008的分布式数据库的实践(二)

    原文 基于Sql Server 2008的分布式数据库的实践(二) 从Win7连接Win2003的Sql Server 2008 1.新建链接服务器链接到Win2003的Sql Server 2008 ...

  9. DCOS实践分享(3):基于Mesos 和 Docker 企业级移动应用实践分享

    2016年1月24日 8:00—19:00 北京万豪酒店(东城区建国门南大街7号) @Container大会是由国内容器社区DockOne组织的专为一线开发者和运维工程师设计的顶级容器技术会议,会议强 ...

  10. DCOS实践分享(1):基于图形化模型设计的应用容器化实践

    2015年11月29日,Mesos Meetup 第三期 - 北京技术沙龙成功举行.本次活动由数人科技CTO 肖德时 和 Linker Networks 的 Sam Chen 一起组织发起. 在这次m ...

随机推荐

  1. 鸿蒙开发游戏(三)---大鱼吃小鱼(放置NPC)

    效果图 添加了一个NPC(小红鱼),玩家控制小黄鱼 鸿蒙开发游戏(一)---大鱼吃小鱼(界面部署) 鸿蒙开发游戏(二)---大鱼吃小鱼(摇杆控制) 鸿蒙开发游戏(三)---大鱼吃小鱼(放置NPC) 鸿 ...

  2. 机器学习基础01DAY

    数据的特征抽取 现实世界中多数特征都不是连续变量,比如分类.文字.图像等,为了对非连续变量做特征表述,需要对这些特征做数学化表述,因此就用到了特征提取. sklearn.feature_extract ...

  3. C语言,变长数组的用法

    在我的<C语言,结构体成员的地址>文章中,定义了一个demo_node结构体,其中用到变长数组char addr[0].本文以此为例,对C语言变长数组的基本用法展开介绍. typedef ...

  4. 全排列II

    全排列II 给定一个可包含重复数字的序列,返回所有不重复的全排列. 示例 输入: [1,1,2] 输出: [ [1,1,2], [1,2,1], [2,1,1] ] 题解 /** * @param { ...

  5. BUG管理系统(Mantis)迁移实战

    Mantis迁移实战 名词解释 Mantis:  开源的BUG管理平台Mantis,也做MantisBT.           同档次产品有EasyBUG,QC,BugFree,Bugzila. Xa ...

  6. Java并发编程实例--5.线程睡眠

    有时候我们需要让线程在一段时间内不做任何事.例如某线程每个一小时检测一下传感器,剩余的时间不做任何事. 我们可以使用sleep()方法使线程睡眠,此期间不占用计算机资源. 这个方法接受一个整数表示睡眠 ...

  7. 如何在 libevent 中读取超过 4096 字节的数据

    如何在 libevent 中读取超过 4096 字节的数据 bufferevent 是 libevent 中相对高层的封装,较 event 使用起来方便很多. 之前有一个需求,需要从服务端读取数据进行 ...

  8. django中update_or_create()

    update_or_create()方法中有一个defaults参数 模型字段会根据查询条件进行查询,如果查询到了,那么就用defaults对应的值去更新字段,如果没有查到就用defaults对应的值 ...

  9. git开发规范

  10. Java 数组查找

    1 //要找的数 - 数组中的第一个元素 / 最大的数 - 第一个元素 2 //数组的查找(线性查找 二分法查找) 3 //线性查找: 4 //equals 5 6 String dest = &qu ...