密集检索与向量数据库技术详解

0 阅读9分钟

一、概述

密集检索(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)

特点

  • 无监督对比学习
  • 不依赖标注数据
  • 通过数据增强构造正负样本对

数据增强策略

  1. 独立裁剪(Independent Cropping)
  2. 对抗攻击
  3. 回译(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 性能对比

索引类型搜索速度内存占用准确率适用场景
Flat100%小规模精确搜索
IVF90-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 查询扩展

方法

  1. 伪相关反馈(PRF)
  2. 查询改写
  3. 多向量查询
# 查询扩展示例
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 索引优化

  1. 选择合适的索引类型

    • < 10万文档:Flat
    • 10万-100万:IVF
    • 100万:HNSW或IVF+PQ

  2. 调整超参数

    • IVF: nlist, nprobe
    • HNSW: M, efConstruction, efSearch
    • PQ: m, nbits

8.2 编码优化

  1. 批量编码:减少GPU调用开销
  2. 模型量化:INT8/FP16推理
  3. 模型蒸馏:使用小模型
  4. 缓存策略:缓存热门查询

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应用的基础设施:

  1. 模型选择:根据场景选择双塔、ColBERT或交叉编码器
  2. 向量数据库:FAISS适合离线、Milvus适合生产、Weaviate适合快速原型
  3. 性能优化:两阶段检索、混合搜索、查询扩展
  4. 领域适配:使用领域特定模型或在领域数据上微调

最佳实践

  • 离线:构建高质量索引
  • 在线:快速检索+精细重排
  • 监控:持续评估检索质量
  • 迭代:根据用户反馈优化