混合检索(BM25 + 向量)
为什么纯向量检索不够
纯向量检索(语义检索),它有一个弱点:
查询:"Python 3.11 有什么新特性" 向量检索可能返回:"编程语言的演化历史"(语义相关但没提 3.11) BM25 检索会返回:包含 "Python 3.11" 关键词的文档(精确匹配)检索方式对比
| 检索方式 | 优势 | 劣势 |
|---|---|---|
| 向量检索 | 擅长语义相近但用词不同的内容 | 精确关键词匹配弱 |
| BM25(关键词) | 擅长精确词匹配 | 不理解语义,同义词失效 |
| 混合检索 | 两者兼得 | 需要分数融合 |
混合检索原理
查询 ├── 向量检索 → [doc_A: 0.92, doc_C: 0.85, doc_E: 0.81] └── BM25检索 → [doc_B: 15.3, doc_A: 12.1, doc_D: 9.8] ↓ RRF 融合(Reciprocal Rank Fusion) ↓ 最终排序: [doc_A, doc_B, doc_C, ...]RRF 公式
RRF 公式(记住思路就行,不用背):
score(doc) = Σ 1 / (k + rank_i)- rank_i:该文档在第 i 个检索结果里的名次(第 1 名 rank=1,第 2 名 rank=2…)
- k:常取 60,用来压低高名次的优势、抬高低名次的贡献,避免「只要某一路排第一就碾压一切」
- Σ:对每个检索器分别算一项,再相加
例如:向量:A > C > E;BM25:B > A > D;融合时,通常不看原始分数,只看排名。
| 文档 | 向量项 | BM25 项 | 总分(约) |
|---|---|---|---|
| doc_A | 1/(60+1) ≈ 0.0164 | 1/(60+2) ≈ 0.0161 | ≈ 0.0325 |
| doc_B | — | 1/(60+1) ≈ 0.0164 | ≈ 0.0164 |
| doc_C | 1/(60+2) ≈ 0.0161 | — | ≈ 0.0161 |
| doc_D | — | 1/(60+3) ≈ 0.0159 | ≈ 0.0159 |
| doc_E | 1/(60+3) ≈ 0.0159 | — | ≈ 0.0159 |
最终排序:doc_A > doc_B > doc_C > doc_D ≈ doc_E
前置概念
rank_bm25 是什么(了解即可)
pipinstallrank_bm25BM25 是一个关键词打分算法,rank_bm25 是这个算法的 Python 实现包,BM25Retriever 在底层调用了它,故使用BM25Retriever要安装rank_bm25
jieba 是什么
BM25 的工作方式是把句子拆成词再匹配:
英文: "machine learning" → ["machine", "learning"] ← 空格天然分词 中文: "机器学习" → ??? ← 没有空格,不能直接拆字jieba 是中文分词库,专门解决这个问题:
importjieba jieba.lcut("机器学习是人工智能的子领域")# → ['机器', '学习', '是', '人工智能', '的', '子', '领域']不加 jieba,中文会被逐字拆开(“机”、“器”、“学”、“习”),匹配效果很差。
LangChain 的实现方式
LangChain 提供了 EnsembleRetriever,直接组合多个检索器:
importjiebafromlangchain.retrieversimportEnsembleRetrieverfromlangchain_community.retrieversimportBM25Retrieverfromlangchain_qdrantimportQdrantVectorStore# BM25 检索器(基于原始文档)bm25_retriever=BM25Retriever.from_documents(chunks,k=3,preprocess_func=jieba.lcut)# bm25_retriever.k = 3# 向量检索器## 方式一:从文档直接创建(内部会建 collection 并写入)vector_store=QdrantVectorStore.from_documents(documents=chunks,embedding=embeddings,location=":memory:",# 或 url="http://localhost:6333"collection_name="my_docs",)# 方式二:连接已有 Qdrant 服务# client = QdrantClient(url="http://localhost:6333")# vector_store = QdrantVectorStore(# client=client,# collection_name="my_docs",# embedding=embeddings,# )# vector_store.add_documents(chunks) # ← 在这里:embed + upsert# 或# vector_store.add_texts(["文本1", "文本2"]) # 只传纯文本也行vector_retriever=vector_store.as_retriever(search_kwargs={"k":3})# 混合ensemble=EnsembleRetriever(retrievers=[bm25_retriever,vector_retriever],weights=[0.5,0.5],# BM25 和向量各占 50%)results=ensemble.invoke("你的查询")混合检索输出:list[Document]
ensemble_retriever().invoke(query) 返回的是 LangChain 的 Document 对象列表。
每个 Document 的结构:
Document(page_content="文档正文片段...",metadata={"source":"xxx.pdf",# 来源文件"chunk_id":"chunk_001",# 分块 ID})LangChain 的 Document(page_content + metadata)会被存成 Qdrant 的 point:
对应向量库中的每一条point的结构:
{"id":"uuid-xxx",# 唯一 ID"vector":[0.12,-0.34,...],# embedding 向量(scroll 时 with_vectors=False 不返回)"payload":{# 业务数据,任意 JSON"page_content":"...","metadata":{"source":"xxx...sample.md",# 固定,metadata其他属性可手动添加"doc_type":"pdf","chunk_id":"sample.pdf_p0_c0","page":0}}}QdrantClient 与 QdrantVectorStore
二者区别
| QdrantClient | QdrantVectorStore | |
|---|---|---|
| 来源 | qdrant_client(Qdrant 官方) | langchain_qdrant(LangChain 生态) |
| 层级 | 数据库 API 层 | RAG 业务抽象层 |
| 职责 | 连接 Qdrant,直接操作 collection / point | 把「文本 + Embedding + 检索」串成 LangChain 接口 |
| 是否管 Embedding | 否,只存/取向量 | 是,内置 embedding 模型 |
QdrantClient
QdrantClient 是连接 Qdrant 向量数据库的原生客户端,相当于 SQL 里的 database driver。
它负责基础设施级操作,例如:
- 检查:collection_exists()
- 建表:create_collection(),指定向量维度、距离度量(COSINE)
- 原始数据读取:scroll() 分页拉取 point 的 payload
不走向量检索,只导出文本做 BM25 索引
QdrantVectorStore
QdrantVectorStore 是 LangChain 的 VectorStore 实现,内部持有 QdrantClient,并额外绑定 Embedding 模型:
它提供面向 RAG 的高层 API:
| 方法 | 作用 |
|---|---|
| add_documents(docs) | 自动 embed 文本 → 写入 Qdrant |
| similarity_search(query) | 自动 embed query → 向量检索 |
| as_retriever() | 转成 LangChain Retriever,供 RAG 链路使用 |