第 6 节 / 共 8 节
RAG 开发(四):混合检索与排序策略
EduRAG 笔记(day06)
EduRAG项目
【了解】今日内容介绍
- 日志格式设定
- BM25算法
- Redis
- MySQL问答
【理解】日志格式设定
概述
日志(Logging)是程序运行时记录关键信息的一种方式,例如操作成功、错误发生或调试信息。它在开发和维护中非常重要,因为:
-
调试:帮助找到代码中的问题。
-
监控:记录程序的运行状态。
- 审计:追踪用户或系统的行为。
Python的logging模块是一个内置工具,提供灵活的日志记录功能,比简单的print语句更强大。
核心概念
- 日志级别:表示日志的重要性,常见级别从低到高:
-DEBUG:调试信息(最低)。
-INFO:一般信息。
-WARNING:警告,可能有问题。
-ERROR:错误,已影响程序。
-CRITICAL:严重错误(最高)。 - 日志处理器(Handler):决定日志输出到哪里(如控制台或文件)。
- 日志格式(Formatter):定义日志的显示样式(如时间、级别、消息)。
使用
基础日志纪录
import logging
# 配置基本的日志设置
logging.basicConfig(level=logging.INFO)
# 获取日志记录器
logger = logging.getLogger("Example1")
# 记录不同级别的日志
logger.debug("这是调试信息,通常用于开发")
logger.info("程序运行正常")
logger.warning("注意,可能有小问题")
logger.error("发生错误")
logger.critical("严重错误,程序可能崩溃")
自定义日志格式
import logging
# 配置日志格式
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# 获取日志记录器
logger = logging.getLogger("Example2")
# 记录日志
logger.debug("调试模式已开启")
logger.info("正在处理数据")
logger.error("数据处理失败")
将日志存储到文件
import logging
# 配置日志,输出到文件
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
filename='app.log', # 日志文件路径
filemode='a' # 'a'表示追加,'w'表示覆盖
)
# 获取日志记录器
logger = logging.getLogger("Example3")
# 记录日志
logger.info("程序启动")
logger.warning("内存使用率较高")
logger.error("无法连接数据库")
同时输出控制台和文件
import logging
# 创建日志记录器
logger = logging.getLogger("Example4")
logger.setLevel(logging.DEBUG) # 设置记录器级别
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) # 控制台显示INFO及以上级别
# 创建文件处理器
file_handler = logging.FileHandler('app.log', mode='a')
file_handler.setLevel(logging.DEBUG) # 文件记录DEBUG及以上级别
# 定义日志格式
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# 为处理器设置格式
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# 将处理器添加到记录器
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 记录日志
logger.debug("调试信息,仅写入文件")
logger.info("程序运行正常")
logger.error("发生错误")
【理解】BM25算法
概述
BM25(Best Matching 25)是一种信息检索领域的排名算法,用于计算查询(Query)与文档(Document)之间的相关性得分。它改进了传统的TF-IDF算法,引入文档长度归一化和词频饱和机制,使检索结果更准确。
代码实现
from retrieval.bm25_search import BM25Search
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main():
# 示例文档集合
documents = ["我喜欢编程", "编程很有趣"]
# 初始化BM25检索器
bm25_search = BM25Search(documents)
# 示例查询
query = "他喜欢编程"
# 执行检索
result, score = bm25_search.search(query)
if result:
logger.info(f"查询结果: {result}, 得分: {score}")
else:
logger.info("未找到匹配结果")
if __name__ == "__main__":
main()
【掌握】Redis
概述
Redis(Remote Dictionary Server)是一个高性能的键值对数据库,常用于缓存、会话管理等场景。它支持多种数据结构(如字符串、哈希、列表等),并提供快速的内存操作。
特性
- 高性能:数据存储在内存中,读写速度极快。
- 持久化:支持 RDB 和 AOF 两种持久化方式。
- 灵活性:支持多种数据类型和丰富命令。
- 简单易用:提供直观的 API,易于集成。
应用场景
- 缓存查询结果以减少数据库压力。
- 存储用户会话信息。
- 实现排行榜或计数器功能。
代码实现
from redis_client import RedisClient
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def main():
# 初始化 Redis 客户端
redis_client = RedisClient()
# 示例数据
key = "user:1"
value = {"name": "Alice", "age": 25}
# 存储数据
redis_client.set_data(key, value)
# 获取数据
result = redis_client.get_data(key)
if result:
logger.info(f"查询结果: {result}")
else:
logger.info("未找到数据")
# 示例查询缓存
query = "test_query"
answer = redis_client.get_answer(query)
if answer:
logger.info(f"缓存答案: {answer}")
else:
logger.info("未找到缓存答案")
if __name__ == "__main__":
main()
【掌握】MySQL问答
概述
本系统从MySQL数据库检索问答对,使用BM25算法计算相似度,并通过Softmax归一化将得分转换为概率值,阈值0.85判断答案可靠性。Redis仅缓存高可靠性结果(相似度>0.85且有答案)。若MySQL无可靠答案,则调用RAG系统检索。
流程
数据存储:MySQL存储FQA高频问答对数据。
问题检索:BM25计算相似度,Softmax归一化后判断阈值0.85。
缓存管理:Redis仅存储相似度>0.85且有答案的数据。
答案返回:
- 若MySQL返回可靠答案,直接返回。
- 否则,调用RAG系统检索。
代码实现
略。
【扩展】
format日志格式大全
| 占位符 | 含义 |
|---|---|
%(name)s |
当前日志记录器的名称(比如你用 logger = logging.getLogger("myLogger")) |
%(filename)s |
当前运行的脚本文件名(不包含路径) |
%(pathname)s |
当前运行脚本的完整路径 |
%(funcName)s |
打印日志的函数名 |
%(lineno)d |
打印日志所在的代码行号 |
%(module)s |
脚本模块名(去掉 .py 后缀的文件名) |
%(process)d |
当前进程的ID |
%(thread)d |
当前线程的ID |
TF-IDF和BM25的区别和联系
背景场景
假设我们有以下三份文档(Corpus):
Doc1: 我 喜欢 吃 苹果
Doc2: 我 今天 吃 了 一个 苹果
Doc3: 苹果 是 一种 水果 啊
现在你搜索的关键词是:
Query: 吃 苹果
我们要用 TF-IDF 和 BM25 分别计算每个文档与查询的相关性打分。
TF-IDF 原理+示例
- TF(Term Frequency):某词在文档中出现得越多,该词对该文档越重要。
- IDF(Inverse Document Frequency):某词在所有文档中越少见,区分度越高,重要性越高。
公示如下:
$$
TF\text{-}IDF(t, d) = TF(t, d) \times \log\left(\frac{N}{DF(t)} + 1\right)
$$
其中:
- t:词语
- d:文档
- N:文档总数
- TF(t,d):词项 t 在文档 d 中出现的频率(Term Frequency)
- DF(t):包含词 t的文档数
示例:计算“吃 苹果”在每个文档中的得分
第一步:统计词频(TF)
| 词语 | Doc1 | Doc2 | Doc3 |
|---|---|---|---|
| 吃 | 1 | 1 | 0 |
| 苹果 | 1 | 1 | 1 |
第二步:计算 DF 和 IDF
| 词语 | DF(包含它的文档数) | IDF |
|---|---|---|
| 吃 | 2 | log(3/2 + 1) ≈ 0.405 |
| 苹果 | 3 | log(3/3 + 1) = log(2) ≈ 0.693 |
第三步:TF-IDF 得分(简单相加)
| 文档 | 吃的TF-IDF | 苹果的TF-IDF | 总分 |
|---|---|---|---|
| Doc1 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc2 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc3 | 0 × 0.405 | 1 × 0.693 | 0.693 |
BM25 原理+示例
比 TF-IDF 更复杂,考虑了文档长度、词频饱和、查询词的重要性。用于计算查询(Query)与文档(Document)之间的相关性得分。
公式如下:
$$
BM25(D, Q) = \sum_{i=1}^{n} \frac{(k_1 + 1) \cdot f_i}{f_i + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{avgdl}\right)} \cdot \log\left(\frac{N - n_i + 0.5}{n_i + 0.5}\right)
$$
默认参数:
- D:文档
-
Q:查询
-
fi 是词项 qi在文档 D中的出现次数(TF),
- N是文档总数,
- ni是包含词项 qi的文档数,
- ∣D∣是文档 D的长度,
- avgdl 是所有文档的平均长度,
- k1 和 b是调整参数,通常设置为 k1=1.2和 b=0.75 。
示例参数
- Doc1 长度 = 4
- Doc2 长度 = 6
- Doc3 长度 = 5
- 平均长度 avgdl = (4+6+5)/3 = 5
用 BM25 公式代入计算(略去繁琐步骤,给出简化结果):
| 文档 | BM25 得分(吃 + 苹果) |
|---|---|
| Doc1 | ≈ 1.45 |
| Doc2 | ≈ 1.52 ✅(最高相关) |
| Doc3 | ≈ 0.68 |
由于 Doc2 更长、包含更多上下文信息,它在 BM25 中被认为更相关。
总结对比
| 特性 | TF-IDF | BM25(改进版) |
|---|---|---|
| 是否考虑文档长度 | ❌ 不考虑 | ✅ 考虑(更公平) |
| 词频处理 | 线性累加 | 非线性饱和处理(防止刷词) |
| 效果 | 中等,快速 | 更精确,现代搜索引擎常用 |
| 是否使用参数 | 无 | 有 k1,b参数可调节 |
MySQL
数据库有很多种,向量数据库、缓存数据库、关系型数据库、图数据库、宽列数据库等。
向量数据库:Milvus
缓存数据库:Redis(了解即可)
关系型数据库:MySQL(Oracle、SQLServer、PostgreSQL,用的非常广泛)
这里,重点说说关系型数据库。
先明确一点:既然是个数据库,就一定是增删改查的操作。
因此,学习MySQL,就是学习MySQL的增删改查操作。
这些操作,可以是纯SQL,也可以是代码(Java,Python等)。
SQL:
#0.前提
MySQL和Milvus差不多,都是有数据库(database),也有数据表(table)
新建库:
create database 库名
新建表:
create table 表名(字段)
# create table t1(
# id int primary key auto_increment,
# name varchar(20)
# )
#1.增加
insert into 表名 values (列)
insert into t1 values (null, 'yangguo');
#2.修改
update 表名 set 字段名=字段值.... where 条件
update t1 set name='yangkang' where id=6;
#3.删除
delete from 表名 where 条件;
delete from t1 where id=6;
#4.查询
select * from 表名;
select 列名1,列名2 ... from 表名;
select * from t1;
Python:
略。
专题:RRF 的理解
RRF的理解
倒数排序融合(Reciprocal Rank Fusion,简称 RRF)是一种融合多个独立排名列表的方法,目的是合并多个检索器(搜索系统)返回的结果,使最终结果能较好地反映所有检索器的优势,提高整体相关性和鲁棒性。
RRF 的核心思想和公式
RRF 利用排名位置的倒数加权累加来给每个文档打分
关键点:
- 排名越靠前,倒数越大,贡献分数越高。
- k 加上排名,有平滑作用,避免前几名分数差距过大。
- 出现在多个排名列表且位置靠前的文档,累积得分更高,最终排名优先。
通俗举例说明
假设有2个检索器,它们分别给出以下两个排名结果列表(排名从1开始):
| 文档ID | 检索器1排名 | 检索器2排名 |
|---|---|---|
| D1 | 1 | 4 |
| D2 | 2 | 1 |
| D3 | 3 | 6 |
| D4 | — (无排名) | 2 |
| D5 | 4 | — (无排名) |
取 k=60
| 文档ID | 检索器1得分 | 检索器2得分 | RRF总分 |
|---|---|---|---|
| D1 | 1/(60+1+1)= 0.01639 | 1/(60+4+1)= 0.01563 | 0.03202 |
| D2 | 1/(60+2+1)= 0.01613 | 1(60+1+1)= 0.01639 | 0.03252 |
| D3 | 1/(60+3+1)= 0.01587 | 1(60+6+1)= 0.01538 | 0.03125 |
| D4 | 0 (无排名,因此得分0) | 1(60+2+1)= 0.01613 | 0.01613 |
| D5 | 1/(60+4+1)= 0.01563 | 0 | 0.01563 |
最终根据 RRF 总分排序,得到的融合排名是:
D2(0.03252)>D1(0.03202)>D3(0.03125)>D4(0.01613)>D5(0.01563)
公式运算步骤总结
- 对每个检索器,第 i 个结果中排名为 ranki(d) 的文档 d,计算分数 1/(k+ranki(d)+1)。
- 若文档未出现在某检索器结果中,则贡献分数为0。
- 将所有检索器对应的分数相加,得到文档 d 的总 RRF 分数。
- 根据总分高低对所有文档重新排序。
专题:TF-IDF 和 BM25 的区别和联系
TF-IDF和BM25的区别和联系
🌟背景场景:
假设我们有以下三份文档(Corpus):
Doc1: 我 喜欢 吃 苹果
Doc2: 我 今天 吃 了 一个 苹果
Doc3: 苹果 是 一种 水果 啊
现在你搜索的关键词是:
Query: 吃 苹果
我们要用 TF-IDF 和 BM25 分别计算每个文档与查询的相关性打分。
1️⃣ TF-IDF 原理 + 示例
TF-IDF 核心思想:
- TF(Term Frequency):某词在文档中出现得越多,该词对该文档越重要。
- IDF(Inverse Document Frequency):某词在所有文档中越少见,区分度越高,重要性越高。
公式:
$$
TF-IDF(t, d) = TF * IDF = \frac{词t在文档d中的出现次数}{文档d的总词数} * log(\frac{1 + N}{1 + df(t)}) + 1
$$
其中:
- t:词语
- d:文档
- N:文档总数(3)
- DF(t):包含词 t的文档数
示例:计算“吃 苹果”在每个文档中的得分
➤ 第一步:统计词频(TF)
| 词语 | Doc1 | Doc2 | Doc3 |
|---|---|---|---|
| 吃 | 1 | 1 | 0 |
| 苹果 | 1 | 1 | 1 |
➤ 第二步:计算 DF 和 IDF
| 词语 | DF(包含它的文档数) | IDF |
|---|---|---|
| 吃 | 2 | log(3/2 + 1) ≈ 0.405 |
| 苹果 | 3 | log(3/3 + 1) = log(2) ≈ 0.693 |
➤ 第三步:TF-IDF 得分(简单相加)
| 文档 | 吃的TF-IDF | 苹果的TF-IDF | 总分 |
|---|---|---|---|
| Doc1 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc2 | 1 × 0.405 | 1 × 0.693 | 1.098 |
| Doc3 | 0 × 0.405 | 1 × 0.693 | 0.693 |
2️⃣ BM25 原理 + 示例
BM25 核心思想:
比 TF-IDF 更复杂,考虑了文档长度、词频饱和、查询词的重要性。用于计算查询(Query)与文档(Document)之间的相关性得分
公式
$$
BM25(D, Q) = \sum_{i=1}^{n} \frac{(k_1 + 1) \cdot f_i}{f_i + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{avgdl}\right)} \cdot \log\left(\frac{N - n_i + 0.5}{n_i + 0.5}\right)
$$
默认参数:
- DD:文档
-
QQ:查询(包含词项 q1,…,qnq1,…,qn)
-
fi 是词项 qi在文档 D中的出现次数(TF),
- N是文档总数,
- ni是包含词项 qi的文档数,
- ∣D∣是文档 D的长度,
- avgdl 是所有文档的平均长度,
- k1 和 b是调整参数,通常设置为 k1=1.2和 b=0.75 。
➤ 示例参数:
- Doc1 长度 = 4
- Doc2 长度 = 6
- Doc3 长度 = 5
- 平均长度 avgdl = (4+6+5)/3 = 5
用 BM25 公式代入计算(略去繁琐步骤,给出简化结果):
| 文档 | BM25 得分(吃 + 苹果) |
|---|---|
| Doc1 | ≈ 1.45 |
| Doc2 | ≈ 1.52 ✅(最高相关) |
| Doc3 | ≈ 0.68 |
由于 Doc2 更长、包含更多上下文信息,它在 BM25 中被认为更相关。
✅总结对比:
| 特性 | TF-IDF | BM25(改进版) |
|---|---|---|
| 是否考虑文档长度 | ❌ 不考虑 | ✅ 考虑(更公平) |
| 词频处理 | 线性累加 | 非线性饱和处理(防止刷词) |
| 效果 | 中等,快速 | 更精确,现代搜索引擎常用 |
| 是否使用参数 | 无 | 有 k1,b参数可调节 |
专题:加权排名策略
加权排名策略(WeightedRanker)
加权排名策略(WeightedRanker)是一种融合多个向量检索结果评分(score)的方法,核心是根据不同向量字段的重要性,为各路径搜索结果的得分分配权重,再将这些加权得分融合,得出最终排序。
WeightedRanker 原理和步骤
-
收集搜索得分
从不同的向量检索路径分别收集返回的结果及其得分(例如 score_1、score_2)。 -
得分归一化
不同路径的得分范围通常不同(如内积 IP 可能是 [-∞,+∞],欧氏距离 L2 是 [0,+∞]),直接比较不合适。
通过对分数使用如函数等数学变换,将各得分统一映射到 区间,方便统一秩序比较。 -
分配权重
根据不同向量字段的重要性,为每一路径的归一化得分分配权重 wi*,权重值在0-1之间。
例如图像特征权重 0.6,文本特征权重 0.4。 -
融合得分
按权重加权计算得分:
\(score^{final}\)=\(∑w^{i} *score^{i}\)
最后根据加权融合的得分从高到低排序,得到最终搜索结果的优先级。
举个具体例子(多模态搜索)
假设有两路检索:
| 文档ID | 图像检索得分 | 文本检索得分 |
|---|---|---|
| 101 | 0.92 | 0.87 |
| 203 | 0.88 | N/A |
| 150 | 0.85 | N/A |
| 198 | 0.83 | 0.91 |
| 175 | 0.80 | 0.82 |
| 110 | N/A | 0.85 |
| 250 | N/A | 0.78 |
假设权重为图像 0.6,文本 0.4,则加权得分计算为:
| 文档ID | 最终得分 = 图像得分×0.6 + 文本得分×0.4 |
|---|---|
| 101 | 0.92×0.6 + 0.87×0.4 = 0.552 + 0.348 = 0.90 |
| 203 | 0.88×0.6 + 0 = 0.528 |
| 150 | 0.85×0.6 + 0 = 0.51 |
| 198 | 0.83×0.6 + 0.91×0.4 = 0.498 + 0.364 = 0.86 |
| 175 | 0.80×0.6 + 0.82×0.4 = 0.48 + 0.328 = 0.81 |
| 110 | 0 + 0.85×0.4 = 0.34 |
| 250 | 0 + 0.78×0.4 = 0.312 |
最终排序就是:
101 (0.90) > 198 (0.86) > 175 (0.81) > 203 (0.528) > 150 (0.51) > 110 (0.34)> 250(0.312)
关键点总结
- WeightedRanker 适合多模态或多特征联合搜索,能体现不同特征的重要性和贡献度。
- 归一化步骤很重要,确保不同度量标准的得分能统一比较。
- 权重由用户设定,灵活调整各检测路径的优先级。
- 最终按加权得分降序排序,得到综合考虑所有搜索路径结果的最优排序。