RRF算法原理与RAG中的应用

倒数排名融合算法(Reciprocal Rank Fusion,简称RRF)是一种在元搜索或混合搜索中常用的相关性评分算法。元搜索或混合搜索是指从多个搜索引擎或搜索源获取结果,并将这些结果融合到一个结果集中的过程。在RAG中,这种算法能够提高你的文档召回率,准率的文档召回才能使得RAG系统出色的完成了第一步。

RRF算法原理

RRF的基本思想是,每个搜索源返回的结果都有其自身的排名。这些排名可能基于各种因素,如 relevancy score、page rank、click-through rate 等。RRF算法将这些排名转化为倒数,然后将这些倒数相加,从而得到一个融合的排名分数。

RRF的具体公式如下 (i一般作为经验值取60,根据实际情况调整):

$$ RRF(k) = \frac{1}{k + i} $$

其中 k 表示一个搜索结果在其原始搜索源中的排名。这个公式的意义在于,排名越高(即 k 越小),其倒数越大,因此其在融合结果中的排名也应该越高。

RRF的优点在于其简单易用,且不需要知道原始搜索源的排名算法的具体细节。只需要知道每个搜索结果在其原始搜索源中的排名,就可以计算出其在融合结果中的排名。

然而,RRF也有其局限性。它假设所有的搜索源都同样重要,但实际上,不同的搜索源可能有不同的权重。此外,它也没有考虑到搜索结果的相关性评分,只考虑了排名。因此,如果两个搜索结果的排名相同,但一个的相关性评分远高于另一个,RRF无法区分这两个结果。

现在以大白话来解释RRF算法,保证你一看就懂:

假设有两个搜索引擎A和B,用来搜索用户提出的关于水果的问题,它们分别返回了以下两个搜索结果列表:

搜索引擎A排名列表:[apple, banana, cherry, date]

搜索引擎B排名列表:[banana, cherry, apple, date]

现在我们想要使用倒数排名融合(RRF)来融合这两个排名列表。首先,我们计算每个项目在各个排名列表中的倒数排名:

对于搜索引擎A:

  • apple在A中的倒数排名为1
  • banana在A中的倒数排名为2
  • cherry在A中的倒数排名为3
  • date在A中的倒数排名为4

对于搜索引擎B:

  • banana在B中的倒数排名为1
  • cherry在B中的倒数排名为2
  • apple在B中的倒数排名为3
  • date在B中的倒数排名为4

然后,我们将这些倒数排名进行加权平均,得到新的融合排名列表:

  • apple: (1/1 + 1/3) / 2 = 0.67
  • banana: (1/2 + 1/1) / 2 = 0.75
  • cherry: (1/3 + 1/2) / 2 = 0.42
  • date: (1/4 + 1/4) / 2 = 0.25

最终的融合排名列表为:[banana, apple, cherry, date],这就是通过倒数排名融合得到的综合排名结果。

RRF算法实战

Elasticsearch在8.12版本中支持了Vector Search。如果你的Embedding后的向量是浮点类型的向量,要么采用类似Milvus的向量数据库进行检索,现在也可以采用Elasticsearch进行检索了;如果你的Embedding后是二进制类型目前Elasticsearch也是支持的。在8.12版本中支持了HNSW算法,自动将 float32 值量化为 int8 字节值。虽然这会使磁盘使用量增加,但是HNSW 搜索所需的内存减少了 75%,大幅减少密集向量搜索所需的资源开销,在这一点上Milvus和Elasticsearch同时支持。

关于Elasticsearch 8.12的环境参考:

Elasticsearch 的 kNN 搜索功能可以找到与查询向量最相似的 k 个向量。这种搜索技术在自然语言处理、产品推荐、图像或视频的相似性搜索等领域有广泛应用。

要使用 kNN 搜索,首先需要将数据转换为有意义的向量值,并将它们作为 dense_vector 字段值添加到文档中。查询也以同样维度的向量形式表示。设计时需确保文档向量与查询向量越接近,其匹配度就越高。

Elasticsearch 支持两种 kNN 方法:一是使用 knn 搜索选项或 knn 查询进行近似 kNN 搜索;二是通过带有 vector 函数的 script_score 查询进行精确但暴力计算的 kNN 搜索。通常情况下,建议使用近似方法,因为它提供了较低延迟和可接受准确性之间平衡,尽管索引速度会慢些。而精确方法虽能保证结果准确无误,但对于大型数据集来说并不易扩展。

运行一个近似 kNN 搜索时需要特别注意资源配置,并且所有矢量数据必须适合节点页面缓存以保证效率。此外,在映射中明确定义至少一个启用索引功能的 dense_vector 字段,并设置相应参数如 similarity 值(默认为余弦相似性)。还可以调整 num_candidates 参数来权衡搜索速度和结果准确性:增加该值可获得更精准结果但降低搜索速度;反之则可能牺牲一定准确性以换取更快响应时间。

现在以QA问答式数据为例,来演示如何使用RRF算法:

1elasticsearch==8.12.1
2openai==1.12.1

插入使用BM25算法的文本匹配表数据:

 1from datetime import datetime
 2from elasticsearch import Elasticsearch
 3import json
 4
 5es = Elasticsearch(
 6    ['http://demo.com:9200'],
 7    basic_auth=('username', 'password'),
 8)
 9
10with open('pc_qa_library.json', 'r', encoding='utf-8') as f:
11    data = json.load(f)
12    for item in data:
13        res = es.index(index="qa-pc-doc", id=item['id'], body={
14            'qa': item['qa'],
15            'timestamp': datetime.now()
16        })

测试一下检索:

 1res = es.search(index="qa-pc-doc", body={
 2    "size": 5,  # 限定最终的Top-k的数量
 3    "query": {
 4        "match": {
 5            'qa': 'Linux支持吗?'
 6        }
 7    }
 8})
 9
10# print(res)
11print("总共%d条结果:" % res['hits']['total']['value'])
12for hit in res['hits']['hits']:
13    print(hit['_score'], "---------- %(qa)s" % hit["_source"])

OK,插入和查询数据都验证没问题!

插入使用向量检索的数据:

 1from elasticsearch import Elasticsearch
 2
 3es = Elasticsearch(
 4    ['http://demo.com:9200'],
 5    basic_auth=('elastic', 'password'),
 6)
 7
 8# 创建索引
 9create_index_body = {
10    "mappings": {
11        "properties": {
12            "embedding": {
13                "type": "dense_vector",
14                "dims": 1536,  # 这个维度目前是OpenAI text-embedding-3-small 的维度
15                # "similarity": "l2_norm"  # sqrt((1 / _score) - 1)
16                "similarity": "cosine"  # (2 * _score) - 1
17            },
18            "context": {
19                "type": "keyword"
20            }
21        }
22    }
23}
24
25es.indices.create(index="qa-knn", body=create_index_body)
26
27
28openai.api_key = 'sk-xxx'
29
30# 读取JSON文件
31with open('pc_qa_library.json', 'r', encoding='utf-8') as f:
32    data = json.load(f)
33    count = 0
34    for item in data:
35        resp = openai.embeddings.create(input=[item['qa']], model="text-embedding-3-small")
36        res = es.index(index="qa-pc-knn", id=item['id'], body={
37            'embedding': resp.data[0].embedding,
38            'qa': item['qa'],
39            'timestamp': datetime.now()
40        })
41        count += 1
42        print("Inserted %d documents into Elasticsearch." % count)

向量检索的方式查询试试:

 1question = 'Linux支持吗?'
 2q_vector = openai.embeddings.create(input=[question], model="text-embedding-3-small").data[0].embedding
 3
 4print(q_vector)
 5
 6search_body = {
 7  "knn": {
 8    "field": "embedding",
 9    "query_vector": q_vector,
10    "k": 5,  # 最终的Top-k的数量
11    "num_candidates": 350
12  },
13  "fields": ["qa"]
14}
15
16res = es.search(index="qa-pc-knn", body=search_body)
17
18print("总共%d条结果:" % res['hits']['total']['value'])
19for hit in res['hits']['hits']:
20    print(hit['_score'], "---------- %(qa)s" % hit["_source"])

现在将两种搜索方式的结果结合起来,然后采用RRF算法的到最终的结果:

 1import openai
 2from elasticsearch import Elasticsearch
 3
 4
 5es = Elasticsearch(
 6    ['http://nas.zouchanglin.cn:9200'],
 7    basic_auth=('elastic', 'lhl123456an'),
 8)
 9
10openai.api_key = 'sk-xxx'
11
12question = '你这里的电脑Linux能跑吗?'
13top_k = 5
14q_vector = openai.embeddings.create(input=[question], model="text-embedding-3-small").data[0].embedding
15
16search_body = {
17    "size": top_k,  # 限定最终的Top-k的数量
18    "knn": {
19        "field": "embedding",
20        "query_vector": q_vector,
21        "k": top_k,  # 最终的Top-k的数量
22        "num_candidates": 512
23    },
24    "fields": ["context"]
25}
26
27res = es.search(index="qa-pc-knn", body=search_body)
28group_knn = []
29print("总共%d条结果:" % res['hits']['total']['value'])
30for hit in res['hits']['hits']:
31    # print(hit['_id'], '---------', hit['_score'], "---------- %(context)s" % hit["_source"])
32    print(hit['_id'], '---------', hit['_score'])
33    group_knn.append(hit['_id'])
34
35
36res = es.search(index="qa-pc-doc", body={
37    "size": top_k,  # 限定最终的Top-k的数量
38    "query": {
39        "match": {
40            'qa': question
41        }
42    }
43})
44
45group_bm25 = []
46print("总共%d条结果:" % res['hits']['total']['value'])
47for hit in res['hits']['hits']:
48    # print(hit['_id'], '---------', hit['_score'], "---------- %(question)s: %(answer)s" % hit["_source"])
49    print(hit['_id'], '---------', hit['_score'])
50    group_bm25.append(hit['_id'])
51
52
53# 倒数排序融合(Reciprocal Rank Fusion,RRF)
54# 1/(k+rank) k取60
55group_knn_rank = []
56for i in range(1, len(group_knn)+1):
57    group_knn_rank.append({
58        group_knn[i-1]: 1/(60+i)
59    })
60print(group_knn_rank)
61
62group_bm25_rank = []
63for i in range(1, len(group_bm25)+1):
64    group_bm25_rank.append({
65        group_bm25[i-1]: 1/(60+i)
66    })
67print(group_bm25_rank)
68
69# fusion
70group_fusion = {}
71for i in range(len(group_knn_rank)):
72    for k, v in group_knn_rank[i].items():
73        if k in group_fusion:
74            group_fusion[k] += v
75        else:
76            group_fusion[k] = v
77for i in range(len(group_bm25_rank)):
78    for k, v in group_bm25_rank[i].items():
79        if k in group_fusion:
80            group_fusion[k] += v
81        else:
82            group_fusion[k] = v
83# rank
84group_fusion = sorted(group_fusion.items(), key=lambda x: x[1], reverse=True)
85print('---------------最终结果-------------------')
86for k, v in group_fusion:
87    # print(k, v)
88    res = es.get(index="qa-pc-doc", id=k)
89    content: str = res['_source']['qa']
90    content = content.replace('\n', '')
91    print(f'id={k}, score={v}, content={content}')
92    print('---------------------------------')

可以看到排序结果还是非常令人满意的:

相似问题优化

RAG中常用的一个的优化方式:有时候存在用户提问是一个非常模糊的问题,此时通常情况下我们会生成3-5个类似问题,同时进行检索:

 1client = OpenAI(
 2    api_key=os.environ.get("OPENAI_API_KEY")
 3)
 4
 5completion = client.chat.completions.create(
 6    model="gpt-4-0125-preview",
 7    messages=[
 8        {
 9            "role": "system",
10            "content": f'''Generate 3 similar questions based on existing questions to ensure \
11                that the semantics remain unchanged and must be in Chinese.
12            ## Problem
13            {question}
14            ## Return JSON Format
15            ["", "", ""]
16            '''
17        }
18    ],
19    temperature=0.2,
20)
21
22questions = json.loads(completion.choices[0].message.content.strip())
23print(questions)

最后贴出50条QA Demo数据 pc_qa_library.json:

  1[
  2  {
  3    "id": 0,
  4    "qa": "问:我应该选择固态硬盘还是机械硬盘? 答:如果您追求更快的速度和更好的性能,建议选择固态硬盘。"
  5  },
  6  {
  7    "id": 1,
  8    "qa": "问:购买电脑时,哪些因素需要考虑? 答:您需要考虑处理器性能、内存容量、硬盘类型、显卡性能等因素。"
  9  },
 10  {
 11    "id": 2,
 12    "qa": "问:我可以在购买电脑时升级内存吗? 答:通常可以,在购买时选择较低配置,后期再单独购买内存进行升级。"
 13  },
 14  {
 15    "id": 3,
 16    "qa": "问:购买电脑时有哪些推荐的品牌? 答:常见的电脑品牌有惠普、戴尔、华硕、联想等,选择时可以根据个人喜好和需求来决定。"
 17  },
 18  {
 19    "id": 4,
 20    "qa": "问:购买电脑时有什么保修服务? 答:大多数电脑品牌会提供一定时期的免费保修服务,可以根据实际情况选择延长保修期。"
 21  },
 22  {
 23    "id": 5,
 24    "qa": "问:如何选择合适的显示器? 答:选择显示器时需要考虑分辨率、屏幕大小、刷新率等因素,根据自己的需求来选择。"
 25  },
 26  {
 27    "id": 6,
 28    "qa": "问:购买电脑时可以选择哪些支付方式? 答:通常支持支付宝、微信支付、银行转账等多种支付方式,具体以商家支持的方式为准。"
 29  },
 30  {
 31    "id": 7,
 32    "qa": "问:购买电脑后配送时间需要多久? 答:配送时间会根据您所在地区和配送方式而有所不同,一般在1-7个工作日内送达。"
 33  },
 34  {
 35    "id": 8,
 36    "qa": "问:我可以在电脑上安装Linux系统吗? 答:可以,在购买时选择兼容Linux系统的硬件配置,安装Linux系统通常不会有太大问题。"
 37  },
 38  {
 39    "id": 9,
 40    "qa": "问:购买电脑需要注意什么? 答:购买电脑时需要注意配置是否满足自己的需求、售后服务是否完善、价格是否合理等方面。"
 41  },
 42  {
 43    "id": 10,
 44    "qa": "问:如何选择合适的处理器? 答:选择处理器时可以考虑性能、核心数量、功耗等因素,根据自己的需求来选择合适的处理器。"
 45  },
 46  {
 47    "id": 11,
 48    "qa": "问:购买电脑时可以选择哪些配送方式? 答:通常支持快递配送、自提等多种配送方式,具体以商家提供的配送方式为准。"
 49  },
 50  {
 51    "id": 12,
 52    "qa": "问:购买电脑时有哪些性能指标需要关注? 答:您可以关注处理器型号、内存容量、显卡性能、硬盘类型等性能指标。"
 53  },
 54  {
 55    "id": 13,
 56    "qa": "问:购买电脑时可以选择哪些保修方案? 答:通常有标准保修、延长保修、意外保修等多种保修方案可供选择。"
 57  },
 58  {
 59    "id": 14,
 60    "qa": "问:如何选择合适的显卡? 答:选择显卡时可以考虑显存大小、核心频率、功耗等因素,根据自己的需求来选择合适的显卡。"
 61  },
 62  {
 63    "id": 15,
 64    "qa": "问:购买电脑时有哪些常见的配件? 答:常见的配件有键盘、鼠标、显示器、音箱等,可以根据实际需求选择购买。"
 65  },
 66  {
 67    "id": 16,
 68    "qa": "问:购买电脑时可以选择哪些操作系统? 答:常见的操作系统有Windows、Mac OS、Linux等,可以根据个人喜好选择。"
 69  },
 70  {
 71    "id": 17,
 72    "qa": "问:如何选择合适的内存? 答:选择内存时可以考虑容量、频率、延迟等因素,根据主板支持的规格来选择合适的内存。"
 73  },
 74  {
 75    "id": 18,
 76    "qa": "问:购买电脑时有哪些网络连接方式? 答:通常支持有线连接、无线连接等多种网络连接方式,可以根据需求选择。"
 77  },
 78  {
 79    "id": 19,
 80    "qa": "问:购买电脑时如何选择合适的机箱? 答:选择机箱时需要考虑散热性能、扩展性、外观设计等因素,根据自己的需求来选择。"
 81  },
 82  {
 83    "id": 20,
 84    "qa": "问:购买电脑时有哪些常见的接口? 答:常见的接口有USB接口、HDMI接口、网口、音频接口等,可以根据外设需求选择。"
 85  },
 86  {
 87    "id": 21,
 88    "qa": "问:购买电脑时可以选择哪些储存设备? 答:常见的储存设备有固态硬盘、机械硬盘、光驱等,可以根据需求选择。"
 89  },
 90  {
 91    "id": 22,
 92    "qa": "问:如何选择合适的键盘? 答:选择键盘时可以考虑键盘类型、按键手感、背光等因素,根据个人习惯来选择合适的键盘。"
 93  },
 94  {
 95    "id": 23,
 96    "qa": "问:购买电脑时可以选择哪些音频设备? 答:常见的音频设备有耳机、音箱、麦克风等,可以根据需求选择适合的音频设备。"
 97  },
 98  {
 99    "id": 24,
100    "qa": "问:购买电脑时如何选择合适的散热系统? 答:选择散热系统时可以考虑散热效率、噪音、尺寸等因素,根据电脑配置来选择合适的散热系统。"
101  },
102  {
103    "id": 25,
104    "qa": "问:购买电脑时可以选择哪些外设? 答:常见的外设有打印机、扫描仪、摄像头等,可以根据实际需求选择适合的外设。"
105  },
106  {
107    "id": 26,
108    "qa": "问:如何选择合适的电源? 答:选择电源时可以考虑功率、效率、稳定性等因素,根据电脑配置来选择合适的电源。"
109  },
110  {
111    "id": 27,
112    "qa": "问:购买电脑时可以选择哪些电池? 答:如果是笔记本电脑,可以选择标准电池、大容量电池等不同容量的电池。"
113  },
114  {
115    "id": 28,
116    "qa": "问:购买电脑时如何选择合适的摄像头? 答:选择摄像头时可以考虑像素、拍摄效果、适配性等因素,根据需求选择合适的摄像头。"
117  },
118  {
119    "id": 29,
120    "qa": "问:如何选择合适的无线网络设备? 答:选择无线网络设备时可以考虑信号覆盖范围、传输速度、稳定性等因素,根据需求选择合适的设备。"
121  },
122  {
123    "id": 30,
124    "qa": "问:购买电脑时可以选择哪些耗材? 答:常见的耗材有墨盒、硒鼓、纸张等,可以根据打印需求选择适合的耗材。"
125  },
126  {
127    "id": 31,
128    "qa": "问:购买电脑时如何选择合适的投影仪? 答:选择投影仪时可以考虑分辨率、亮度、投影距离等因素,根据使用场景选择合适的投影仪。"
129  },
130  {
131    "id": 32,
132    "qa": "问:如何选择合适的键鼠套装? 答:选择键鼠套装时可以考虑连接方式、手感、耐用性等因素,根据个人习惯选择合适的键鼠套装。"
133  },
134  {
135    "id": 33,
136    "qa": "问:购买电脑时可以选择哪些办公软件? 答:常见的办公软件有Office套件、金山WPS等,可以根据需求选择适合的办公软件。"
137  },
138  {
139    "id": 34,
140    "qa": "问:购买电脑时如何选择合适的打印机? 答:选择打印机时可以考虑打印速度、打印质量、耗材成本等因素,根据需求选择合适的打印机。"
141  },
142  {
143    "id": 35,
144    "qa": "问:如何选择合适的扫描仪? 答:选择扫描仪时可以考虑扫描速度、扫描精度、连接方式等因素,根据需求选择合适的扫描仪。"
145  },
146  {
147    "id": 36,
148    "qa": "问:购买电脑时可以选择哪些办公设备? 答:常见的办公设备有复印机、传真机、装订机等,可以根据需求选择适合的办公设备。"
149  },
150  {
151    "id": 37,
152    "qa": "问:购买电脑时如何选择合适的耳机? 答:选择耳机时可以考虑音质、佩戴舒适度、阻抗等因素,根据需求选择合适的耳机。"
153  },
154  {
155    "id": 38,
156    "qa": "问:如何选择合适的音箱? 答:选择音箱时可以考虑音质、功率、连接方式等因素,根据需求选择合适的音箱。"
157  },
158  {
159    "id": 39,
160    "qa": "问:购买电脑时可以选择哪些网络设备? 答:常见的网络设备有路由器、交换机、网卡等,可以根据需求选择适合的网络设备。"
161  },
162  {
163    "id": 40,
164    "qa": "问:购买电脑时如何选择合适的显示器支架? 答:选择显示器支架时可以考虑承重能力、调节功能、稳定性等因素,根据显示器尺寸选择合适的支架。"
165  },
166  {
167    "id": 41,
168    "qa": "问:如何选择合适的鼠标垫? 答:选择鼠标垫时可以考虑材质、尺寸、防滑性等因素,根据鼠标类型选择合适的鼠标垫。"
169  },
170  {
171    "id": 42,
172    "qa": "问:购买电脑时可以选择哪些电脑桌椅? 答:常见的电脑桌椅有电脑椅、电脑桌、桌面升降架等,可以根据使用习惯选择合适的电脑桌椅。"
173  },
174  {
175    "id": 43,
176    "qa": "问:购买电脑时如何选择合适的电脑包? 答:选择电脑包时可以考虑尺寸、质地、舒适度等因素,根据电脑尺寸选择合适的电脑包。"
177  },
178  {
179    "id": 44,
180    "qa": "问:如何选择合适的电脑桌面壁纸? 答:选择桌面壁纸时可以根据个人喜好选择风格、颜色等,也可以自定义壁纸。"
181  },
182  {
183    "id": 45,
184    "qa": "问:购买电脑时可以选择哪些办公软件? 答:常见的办公软件有Office套件、金山WPS等,可以根据需求选择适合的办公软件。"
185  },
186  {
187    "id": 46,
188    "qa": "问:购买电脑时如何选择合适的打印机? 答:选择打印机时可以考虑打印速度、打印质量、耗材成本等因素,根据需求选择合适的打印机。"
189  },
190  {
191    "id": 47,
192    "qa": "问:如何选择合适的扫描仪? 答:选择扫描仪时可以考虑扫描速度、扫描精度、连接方式等因素,根据需求选择合适的扫描仪。"
193  },
194  {
195    "id": 48,
196    "qa": "问:购买电脑时可以选择哪些办公设备? 答:常见的办公设备有复印机、传真机、装订机等,可以根据需求选择适合的办公设备。"
197  },
198  {
199    "id": 49,
200    "qa": "问:购买电脑时如何选择合适的耳机? 答:选择耳机时可以考虑音质、佩戴舒适度、阻抗等因素,根据需求选择合适的耳机。"
201  }
202]