第 6 节 / 共 8 节

RAG 开发(四):混合检索与排序策略

🎯 RRF、TF-IDF/BM25、加权排名等检索策略。
📖 笔记

EduRAG 笔记(day06)

EduRAG项目

【了解】今日内容介绍

  • 日志格式设定
  • BM25算法
  • Redis
  • MySQL问答

【理解】日志格式设定

概述

日志(Logging)是程序运行时记录关键信息的一种方式,例如操作成功、错误发生或调试信息。它在开发和维护中非常重要,因为:

  • 调试:帮助找到代码中的问题。

  • 监控:记录程序的运行状态。

  • 审计:追踪用户或系统的行为。

Python的logging模块是一个内置工具,提供灵活的日志记录功能,比简单的print语句更强大。

核心概念

  1. 日志级别:表示日志的重要性,常见级别从低到高:
    - DEBUG:调试信息(最低)。
    - INFO:一般信息。
    - WARNING:警告,可能有问题。
    - ERROR:错误,已影响程序。
    - CRITICAL:严重错误(最高)。
  2. 日志处理器(Handler):决定日志输出到哪里(如控制台或文件)。
  3. 日志格式(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)

公式运算步骤总结

  1. 对每个检索器,第 i 个结果中排名为 ranki(d) 的文档 d,计算分数 1/(k+ranki(d)+1)。
  2. 若文档未出现在某检索器结果中,则贡献分数为0。
  3. 将所有检索器对应的分数相加,得到文档 d 的总 RRF 分数。
  4. 根据总分高低对所有文档重新排序。
📖 笔记

专题: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 原理和步骤

  1. 收集搜索得分
    从不同的向量检索路径分别收集返回的结果及其得分(例如 score_1、score_2)。

  2. 得分归一化
    不同路径的得分范围通常不同(如内积 IP 可能是 [-∞,+∞],欧氏距离 L2 是 [0,+∞]),直接比较不合适。
    通过对分数使用如函数等数学变换,将各得分统一映射到 区间,方便统一秩序比较。

  3. 分配权重
    根据不同向量字段的重要性,为每一路径的归一化得分分配权重 wi*,权重值在0-1之间。
    例如图像特征权重 0.6,文本特征权重 0.4。

  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 适合多模态或多特征联合搜索,能体现不同特征的重要性和贡献度。
  • 归一化步骤很重要,确保不同度量标准的得分能统一比较。
  • 权重由用户设定,灵活调整各检测路径的优先级。
  • 最终按加权得分降序排序,得到综合考虑所有搜索路径结果的最优排序。