一、概述
密集检索(Dense Retrieval)是现代信息检索的核心技术,通过将文本编码为稠密向量,实现语义级别的相似度匹配。本文档详细介绍主流的密集检索模型、向量数据库以及相关优化技术。
二、密集检索基础
2.1 什么是密集检索
定义:使用神经网络将查询和文档编码为低维稠密向量,通过向量相似度进行检索。
与传统检索的对比:
| 特性 | 稀疏检索(BM25) | 密集检索(Dense Retrieval) |
|---|---|---|
| 表示方式 | 高维稀疏向量 | 低维稠密向量 |
| 匹配方式 | 词汇精确匹配 | 语义相似度匹配 |
| 同义词处理 | 较弱 | 强 |
| 计算效率 | 高(倒排索引) | 中等(需要向量搜索) |
| 可解释性 | 强 | 较弱 |
优势:
- 捕捉语义相似性
- 处理同义词和释义
- 支持跨语言检索
- 适合开放域问答
三、主流密集检索模型
3.1 双塔架构(Bi-Encoder)
3.1.1 基本原理
使用两个独立的编码器分别编码查询和文档,然后计算向量相似度。
# 伪代码
query_embedding = query_encoder(query) # [batch, hidden_dim]
doc_embedding = doc_encoder(document) # [batch, hidden_dim]
similarity = cosine_similarity(query_embedding, doc_embedding)
优点:
- 文档可以离线预编码
- 检索速度快
- 易于扩展到大规模数据
缺点:
- 查询和文档之间没有交互
- 表达能力相对较弱
3.2 DPR(Dense Passage Retrieval)
3.2.1 模型架构
论文:《Dense Passage Retrieval for Open-Domain Question Answering》(Facebook AI Research, 2020)
核心思想:使用BERT作为双塔编码器,通过对比学习训练。
训练目标:
对于每个问题q和正例段落p+,以及负例段落p-:
最大化:sim(q, p+)
最小化:sim(q, p-)
实现示例:
from transformers import DPRQuestionEncoder, DPRContextEncoder
import torch
# 加载模型
question_encoder = DPRQuestionEncoder.from_pretrained("facebook/dpr-question_encoder-single-nq-base")
context_encoder = DPRContextEncoder.from_pretrained("facebook/dpr-ctx_encoder-single-nq-base")
# 编码查询
question = "What is the capital of France?"
question_embedding = question_encoder(
**tokenizer(question, return_tensors="pt")
).pooler_output
# 编码文档
context = "Paris is the capital of France."
context_embedding = context_encoder(
**tokenizer(context, return_tensors="pt")
).pooler_output
# 计算相似度
similarity = torch.cosine_similarity(question_embedding, context_embedding)
关键技术:
- In-batch负采样
- 难负例挖掘
- 使用BM25检索的负例
3.3 Contriever
3.3.1 核心创新
论文:《Unsupervised Dense Information Retrieval with Contrastive Learning》(Meta AI, 2021)
特点:
- 无监督对比学习
- 不依赖标注数据
- 通过数据增强构造正负样本对
数据增强策略:
- 独立裁剪(Independent Cropping)
- 对抗攻击
- 回译(Back-translation)
训练框架:
# 对比学习框架(MoCo风格)
class Contriever(nn.Module):
def __init__(self, encoder):
super().__init__()
self.encoder = encoder
self.queue = [] # 动态负样本队列
def forward(self, query, doc_positive, doc_negatives):
# 编码
q_emb = self.encoder(query)
p_emb = self.encoder(doc_positive)
n_embs = [self.encoder(neg) for neg in doc_negatives]
# 对比损失
loss = contrastive_loss(q_emb, p_emb, n_embs)
return loss
应用场景:
- 零样本检索
- 领域迁移
- 低资源场景
3.4 ColBERT(Late Interaction)
3.4.1 后期交互机制
论文:《ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction》(Stanford, 2020)
核心思想:在Token级别进行细粒度交互。
架构特点:
Query: [q1, q2, q3] → BERT → [e_q1, e_q2, e_q3]
Doc: [d1, d2, d3, d4] → BERT → [e_d1, e_d2, e_d3, e_d4]
相似度 = Σ max_j (e_qi · e_dj)
优势:
- 保留Token级别的语义信息
- 比交叉编码器快
- 比双塔模型准确
实现示例:
from colbert.modeling.colbert import ColBERT
from colbert.infra import ColBERTConfig
# 加载模型
config = ColBERTConfig(
doc_maxlen=220,
nbits=2, # 量化位数
)
model = ColBERT.from_pretrained('colbert-ir/colbertv2.0', config=config)
# 编码和检索
query_embeddings = model.query_encoder(query_tokens)
doc_embeddings = model.doc_encoder(doc_tokens)
# MaxSim操作
scores = model.score(query_embeddings, doc_embeddings)
性能优化:
- 向量量化(2-bit, 4-bit)
- 倒排索引加速
- GPU批量处理
3.5 ANCE(Approximate Nearest Neighbor Negative Contrastive Learning)
核心创新:
- 使用ANN检索作为难负例挖掘
- 异步索引更新
- 动态负采样
训练流程:
1. 用当前模型编码所有文档
2. 构建ANN索引
3. 对每个查询,检索Top-K作为难负例
4. 更新模型
5. 重复1-4
四、向量数据库技术
4.1 FAISS(Facebook AI Similarity Search)
4.1.1 基本介绍
开发者:Meta AI Research
特点:
- 高效的相似度搜索
- 支持GPU加速
- 多种索引类型
- 十亿级向量检索
4.1.2 索引类型
1. Flat索引(精确搜索)
import faiss
import numpy as np
# 创建索引
dimension = 768
index = faiss.IndexFlatL2(dimension) # L2距离
# 或使用内积
index = faiss.IndexFlatIP(dimension) # 内积
# 添加向量
vectors = np.random.random((10000, dimension)).astype('float32')
index.add(vectors)
# 搜索
query = np.random.random((1, dimension)).astype('float32')
D, I = index.search(query, k=10) # 返回距离和索引
2. IVF索引(倒排文件)
# 聚类中心数量
nlist = 100
quantizer = faiss.IndexFlatL2(dimension)
index = faiss.IndexIVFFlat(quantizer, dimension, nlist)
# 训练索引(聚类)
index.train(vectors)
index.add(vectors)
# 搜索时探测的聚类数
index.nprobe = 10
D, I = index.search(query, k=10)
3. HNSW索引(层次化导航小世界图)
# M: 每层的最大连接数
# efConstruction: 构建时的搜索范围
index = faiss.IndexHNSWFlat(dimension, M=32)
index.hnsw.efConstruction = 40
index.add(vectors)
# 搜索时的探索范围
index.hnsw.efSearch = 16
D, I = index.search(query, k=10)
4. PQ索引(乘积量化)
# m: 子向量数量(dimension必须是m的倍数)
# nbits: 每个子向量的量化位数
m = 8
nbits = 8
index = faiss.IndexPQ(dimension, m, nbits)
index.train(vectors)
index.add(vectors)
D, I = index.search(query, k=10)
4.1.3 性能对比
| 索引类型 | 搜索速度 | 内存占用 | 准确率 | 适用场景 |
|---|---|---|---|---|
| Flat | 慢 | 高 | 100% | 小规模精确搜索 |
| IVF | 快 | 中 | 90-98% | 中等规模 |
| HNSW | 很快 | 高 | 95-99% | 需要高召回 |
| PQ | 很快 | 低 | 80-95% | 大规模内存受限 |
4.2 Milvus
4.2.1 特性
定义:开源向量数据库,专为AI应用设计。
核心优势:
- 分布式架构
- 多种索引支持
- GPU加速
- 实时更新
- 混合查询(向量+标量)
4.2.2 使用示例
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType
# 连接到Milvus
connections.connect("default", host="localhost", port="19530")
# 定义Schema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=1000),
]
schema = CollectionSchema(fields, description="Document collection")
# 创建集合
collection = Collection("documents", schema)
# 插入数据
entities = [
[1, 2, 3], # id
[[0.1]*768, [0.2]*768, [0.3]*768], # embedding
["text1", "text2", "text3"] # text
]
collection.insert(entities)
# 创建索引
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128}
}
collection.create_index("embedding", index_params)
# 搜索
search_params = {"metric_type": "L2", "params": {"nprobe": 10}}
results = collection.search(
data=[[0.1]*768],
anns_field="embedding",
param=search_params,
limit=10
)
4.2.3 混合查询
# 向量检索 + 标量过滤
expr = "id in [1, 2, 3, 4, 5]"
results = collection.search(
data=[[0.1]*768],
anns_field="embedding",
param=search_params,
limit=10,
expr=expr # 过滤条件
)
4.3 Weaviate
4.3.1 特性
定义:云原生向量数据库,支持GraphQL查询。
独特功能:
- 自动向量化(集成多种编码器)
- 语义搜索
- 混合搜索(向量+BM25)
- GraphQL API
4.3.2 使用示例
import weaviate
# 连接
client = weaviate.Client("http://localhost:8080")
# 定义Schema
schema = {
"classes": [{
"class": "Document",
"vectorizer": "text2vec-transformers",
"properties": [
{"name": "content", "dataType": ["text"]},
{"name": "title", "dataType": ["string"]},
]
}]
}
client.schema.create(schema)
# 插入数据(自动向量化)
client.data_object.create(
data_object={"content": "Paris is the capital of France.", "title": "France"},
class_name="Document"
)
# 语义搜索
result = (
client.query
.get("Document", ["content", "title"])
.with_near_text({"concepts": ["French capital"]})
.with_limit(5)
.do()
)
4.4 Qdrant
4.4.1 特性
定义:高性能向量搜索引擎,Rust实现。
优势:
- 高性能
- 支持Payload过滤
- 实时更新
- 云原生
4.4.2 使用示例
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
# 创建客户端
client = QdrantClient("localhost", port=6333)
# 创建集合
client.create_collection(
collection_name="documents",
vectors_config=VectorParams(size=768, distance=Distance.COSINE),
)
# 插入数据
points = [
PointStruct(
id=1,
vector=[0.1] * 768,
payload={"text": "Sample document", "category": "A"}
)
]
client.upsert(collection_name="documents", points=points)
# 搜索
search_result = client.search(
collection_name="documents",
query_vector=[0.1] * 768,
limit=5,
query_filter={"category": "A"} # Payload过滤
)
五、检索优化技术
5.1 重排序(Re-ranking)
5.1.1 交叉编码器(Cross-Encoder)
原理:将查询和文档拼接后输入BERT,直接预测相关性分数。
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
# 加载模型
model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)
# 重排序
def rerank(query, documents, top_k=10):
pairs = [[query, doc] for doc in documents]
inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors="pt")
with torch.no_grad():
scores = model(**inputs).logits.squeeze(-1)
# 按分数排序
sorted_indices = scores.argsort(descending=True)[:top_k]
return [documents[i] for i in sorted_indices]
两阶段检索:
阶段1:双塔模型快速检索Top-100
阶段2:交叉编码器精细重排序Top-10
5.2 混合检索(Hybrid Search)
思想:结合稀疏检索和密集检索的优势。
def hybrid_search(query, alpha=0.5):
# BM25检索
bm25_scores = bm25.get_scores(query)
# 密集检索
query_embedding = encoder.encode(query)
dense_scores = index.search(query_embedding)
# 分数融合(归一化后加权)
bm25_norm = normalize(bm25_scores)
dense_norm = normalize(dense_scores)
final_scores = alpha * bm25_norm + (1 - alpha) * dense_norm
return final_scores
5.3 查询扩展
方法:
- 伪相关反馈(PRF)
- 查询改写
- 多向量查询
# 查询扩展示例
def expand_query(query, retriever, top_k=3):
# 初次检索
initial_results = retriever.search(query, k=top_k)
# 提取关键词
expanded_terms = extract_keywords(initial_results)
# 构造扩展查询
expanded_query = query + " " + " ".join(expanded_terms)
# 最终检索
final_results = retriever.search(expanded_query)
return final_results
六、领域特定检索模型
6.1 多语言检索
6.1.1 LaBSE(Language-agnostic BERT Sentence Embedding)
特点:
- 支持109种语言
- 跨语言语义对齐
- 双语平行语料训练
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('sentence-transformers/LaBSE')
# 跨语言检索
query_en = "What is the capital of France?"
doc_zh = "巴黎是法国的首都。"
query_emb = model.encode(query_en)
doc_emb = model.encode(doc_zh)
similarity = cosine_similarity([query_emb], [doc_emb])
6.1.2 mBERT(Multilingual BERT)
特点:
- 在104种语言的维基百科上预训练
- 零样本跨语言迁移
- 适合微调
6.2 代码检索
模型:
- CodeBERT
- GraphCodeBERT
- UniXcoder
应用:
- 代码搜索
- 代码克隆检测
- 代码补全
七、实战案例:构建RAG检索系统
import faiss
from sentence_transformers import SentenceTransformer
from typing import List, Tuple
class RAGRetriever:
def __init__(self, model_name: str = "sentence-transformers/all-MiniLM-L6-v2"):
self.encoder = SentenceTransformer(model_name)
self.index = None
self.documents = []
def build_index(self, documents: List[str]):
"""构建向量索引"""
self.documents = documents
# 编码文档
embeddings = self.encoder.encode(documents, show_progress_bar=True)
# 创建FAISS索引
dimension = embeddings.shape[1]
self.index = faiss.IndexFlatIP(dimension) # 内积
# 归一化(用于余弦相似度)
faiss.normalize_L2(embeddings)
self.index.add(embeddings)
def search(self, query: str, k: int = 5) -> List[Tuple[str, float]]:
"""检索相关文档"""
# 编码查询
query_embedding = self.encoder.encode([query])
faiss.normalize_L2(query_embedding)
# 搜索
scores, indices = self.index.search(query_embedding, k)
# 返回文档和分数
results = [
(self.documents[idx], score)
for idx, score in zip(indices[0], scores[0])
]
return results
# 使用示例
retriever = RAGRetriever()
documents = [
"Paris is the capital of France.",
"Berlin is the capital of Germany.",
"Madrid is the capital of Spain.",
]
retriever.build_index(documents)
results = retriever.search("French capital", k=2)
for doc, score in results:
print(f"Score: {score:.4f} | Doc: {doc}")
八、性能优化建议
8.1 索引优化
-
选择合适的索引类型
- < 10万文档:Flat
- 10万-100万:IVF
-
100万:HNSW或IVF+PQ
-
调整超参数
- IVF: nlist, nprobe
- HNSW: M, efConstruction, efSearch
- PQ: m, nbits
8.2 编码优化
- 批量编码:减少GPU调用开销
- 模型量化:INT8/FP16推理
- 模型蒸馏:使用小模型
- 缓存策略:缓存热门查询
8.3 分布式部署
# 使用Ray进行分布式检索
import ray
@ray.remote
class DistributedRetriever:
def __init__(self, shard_id):
self.retriever = RAGRetriever()
# 加载对应分片
def search(self, query, k):
return self.retriever.search(query, k)
# 创建多个检索器
retrievers = [DistributedRetriever.remote(i) for i in range(4)]
# 并行检索
results = ray.get([r.search.remote(query, k) for r in retrievers])
九、总结
密集检索技术是现代AI应用的基础设施:
- 模型选择:根据场景选择双塔、ColBERT或交叉编码器
- 向量数据库:FAISS适合离线、Milvus适合生产、Weaviate适合快速原型
- 性能优化:两阶段检索、混合搜索、查询扩展
- 领域适配:使用领域特定模型或在领域数据上微调
最佳实践:
- 离线:构建高质量索引
- 在线:快速检索+精细重排
- 监控:持续评估检索质量
- 迭代:根据用户反馈优化