学习资源站

23-第三节 索引构建与检索优化

23-第三节 索引构建与检索优化

第三节 索引构建与检索优化

flowchart LR
    %% 索引构建与检索优化流程
    INPUT[📦 接收文本块chunks] --> INDEX_CHECK{📂 检查索引缓存}
    INDEX_CHECK -->|存在| LOAD_INDEX[⚡ 加载已保存索引]
    INDEX_CHECK -->|不存在| BUILD_INDEX[🔨 构建新索引]

    BUILD_INDEX --> EMBED[🤖 BGE嵌入模型]
    EMBED --> FAISS[📊 FAISS向量索引]
    FAISS --> SAVE[💾 保存索引]

    LOAD_INDEX --> SETUP[🔧 设置检索器]
    SAVE --> SETUP

    SETUP --> QUERY[❓ 用户查询]
    QUERY --> HYBRID[🔍 RRF混合检索]

    %% 混合检索详细流程
    subgraph HybridProcess [RRF混合检索过程]
        H1[📊 向量检索语义相似度]
        H2[🔤 BM25检索关键词匹配]
        H3[⚡ RRF重排融合]
        H1 --> H3
        H2 --> H3
    end

    %% 索引构建详细流程
    subgraph IndexProcess [索引构建过程]
        I1[📝 文本向量化]
        I2[🗂️ 构建FAISS索引]
        I3[💾 索引持久化]
        I1 --> I2 --> I3
    end

    %% 检索器设置流程
    subgraph SetupProcess [检索器设置过程]
        S1[🔍 向量检索器设置]
        S2[📋 BM25检索器设置]
        S1 --> S2
    end

    HYBRID --> RESULT[📖 检索结果]

    %% 连接子流程
    BUILD_INDEX -.-> IndexProcess
    HYBRID -.-> HybridProcess
    SETUP -.-> SetupProcess

    %% 样式定义
    classDef index fill:#fff3e0,stroke:#e65100,stroke-width:2px
    classDef retrieval fill:#e8f5e8,stroke:#1b5e20,stroke-width:2px
    classDef cache fill:#fff8e1,stroke:#f57c00,stroke-width:2px
    classDef subprocess fill:#f1f8e9,stroke:#33691e,stroke-width:2px
    classDef output fill:#e1f5fe,stroke:#0277bd,stroke-width:2px

    %% 应用样式
    class BUILD_INDEX,EMBED,FAISS,SAVE index
    class SETUP,QUERY,HYBRID retrieval
    class INDEX_CHECK,LOAD_INDEX cache
    class IndexProcess,HybridProcess,SetupProcess subprocess
    class INPUT,RESULT output

一、核心设计

1.1 索引构建

索引构建模块的核心任务是将文本块转换为向量表示,并构建高效的检索索引。这里选择之前一直使用的BGE-small-zh-v1.5作为嵌入模型,并使用FAISS作为向量数据库来存储和检索向量。为了提升系统启动速度,实现索引缓存机制。首次构建后会将FAISS索引保存到本地,后续启动时直接加载已有索引,可以将启动时间从几分钟缩短到几秒钟。

1.2 混合检索

检索优化模块实现了多种检索策略的组合。采用双路检索的方式:向量检索基于语义相似度,擅长理解查询意图;BM25检索基于关键词匹配,擅长精确匹配。为了综合两种检索方式的优势,我们使用RRF(Reciprocal Rank Fusion)算法来融合检索结果。这个算法会综合考虑两种检索结果的排名信息,避免过度依赖单一检索方式。

RRF 可能并不是效果最好的重排方式,但是够用🫠。如果想使用 ColBERT、RankLLM 等更先进的重排方法可以自行尝试。

此外,系统还支持基于元数据的智能过滤,可以按菜品分类、难度等级等条件进行筛选检索。

二、索引构建模块

index_construction.py完整代码

2.1 类结构设计

class IndexConstructionModule:
    """索引构建模块 - 负责向量化和索引构建"""

    def __init__(self, model_name: str = "BAAI/bge-small-zh-v1.5",
                 index_save_path: str = "./vector_index"):
        self.model_name = model_name
        self.index_save_path = index_save_path
        self.embeddings = None
        self.vectorstore = None
        self.setup_embeddings()
  • index_save_path: 索引保存路径
  • embeddings: HuggingFace嵌入模型实例
  • vectorstore: FAISS向量存储实例

2.2 嵌入模型初始化

def setup_embeddings(self):
    """初始化嵌入模型"""
    self.embeddings = HuggingFaceEmbeddings(
        model_name=self.model_name,
        model_kwargs={'device': 'cpu'},
        encode_kwargs={'normalize_embeddings': True}
    )

2.3 向量索引构建

def build_vector_index(self, chunks: List[Document]) -> FAISS:
    """构建向量索引"""
    if not chunks:
        raise ValueError("文档块列表不能为空")

    # 提取文本内容
    texts = [chunk.page_content for chunk in chunks]
    metadatas = [chunk.metadata for chunk in chunks]

    # 构建FAISS向量索引
    self.vectorstore = FAISS.from_texts(
        texts=texts,
        embedding=self.embeddings,
        metadatas=metadatas
    )

    return self.vectorstore

使用FAISS作为向量数据库,它的检索速度很快,同时保存了文本内容和元数据信息,支持大规模向量的高效检索。

2.4 索引缓存机制

def save_index(self):
    """保存向量索引到配置的路径"""
    if not self.vectorstore:
        raise ValueError("请先构建向量索引")

    # 确保保存目录存在
    Path(self.index_save_path).mkdir(parents=True, exist_ok=True)

    self.vectorstore.save_local(self.index_save_path)

def load_index(self):
    """从配置的路径加载向量索引"""
    if not self.embeddings:
        self.setup_embeddings()

    if not Path(self.index_save_path).exists():
        return None

    self.vectorstore = FAISS.load_local(
        self.index_save_path, 
        self.embeddings,
        allow_dangerous_deserialization=True
    )
    return self.vectorstore

索引缓存的效果很明显:首次运行时构建索引需要几分钟,但后续运行时加载索引只需几秒钟。索引文件通常只有几十MB,存储效率很高。

三、检索优化模块

retrieval_optimization.py完整代码

3.1 类结构设计

class RetrievalOptimizationModule:
    """检索优化模块 - 负责混合检索和过滤"""

    def __init__(self, vectorstore: FAISS, chunks: List[Document]):
        self.vectorstore = vectorstore
        self.chunks = chunks
        self.setup_retrievers()
  • vectorstore: FAISS向量存储实例
  • chunks: 文档块列表,用于BM25检索

3.2 检索器设置

def setup_retrievers(self):
    """设置向量检索器和BM25检索器"""
    # 向量检索器
    self.vector_retriever = self.vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 5}
    )

    # BM25检索器
    self.bm25_retriever = BM25Retriever.from_documents(
        self.chunks,
        k=5
    )

3.3 RRF混合检索

def hybrid_search(self, query: str, top_k: int = 3) -> List[Document]:
    """混合检索 - 结合向量检索和BM25检索,使用RRF重排"""
    # 分别获取向量检索和BM25检索结果
    vector_docs = self.vector_retriever.get_relevant_documents(query)
    bm25_docs = self.bm25_retriever.get_relevant_documents(query)

    # 使用RRF重排
    reranked_docs = self._rrf_rerank(vector_docs, bm25_docs)
    return reranked_docs[:top_k]

def _rrf_rerank(self, vector_results: List[Document], bm25_results: List[Document]) -> List[Document]:
    """RRF (Reciprocal Rank Fusion) 重排"""

    # RRF融合算法
    rrf_scores = {}
    k = 60  # RRF参数

    # 计算向量检索的RRF分数
    for rank, doc in enumerate(vector_results):
        doc_id = id(doc)
        rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # 计算BM25检索的RRF分数
    for rank, doc in enumerate(bm25_results):
        doc_id = id(doc)
        rrf_scores[doc_id] = rrf_scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # 合并所有文档并按RRF分数排序
    all_docs = {id(doc): doc for doc in vector_results + bm25_results}
    sorted_docs = sorted(all_docs.items(),
                        key=lambda x: rrf_scores.get(x[0], 0),
                        reverse=True)

    return [doc for _, doc in sorted_docs]

在当前系统中,两种检索方式各有优势:

向量检索的优势: - 理解语义相似性,如"简单易做的菜"能匹配到标记为"简单"的菜谱 - 处理同义词和近义词,如"制作方法"和"做法"、"烹饪步骤" - 理解用户意图,如"适合新手"能找到难度较低的菜谱

BM25检索的优势: - 精确匹配菜名,如"宫保鸡丁"能准确找到对应菜谱 - 匹配具体食材,如"土豆丝"、"西红柿"等关键词 - 处理专业术语,如"爆炒"、"红烧"等烹饪手法

RRF算法能综合两种检索方式的排名信息,既保证了语义理解的准确性,又确保了关键词匹配的精确性。当然还可以用路由的方式,根据查询类型智能选择使用向量检索还是BM25检索。这种方法针对性强,能为不同类型的查询选择最优的检索方式;不足是路由规则的设计和维护比较复杂,边界情况难以处理,而且通常需要调用LLM来判断查询类型,会增加延迟和成本。

3.4 元数据过滤检索

def metadata_filtered_search(self, query: str, filters: Dict[str, Any],
                           top_k: int = 5) -> List[Document]:
    """基于元数据过滤的检索"""
    # 先进行向量检索
    vector_retriever = self.vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": top_k * 3, "filter": filters}  # 扩大检索范围
    )

    results = vector_retriever.invoke(query)
    return results[:top_k]

过滤检索应用场景: - 用户询问"推荐几道素菜"时,可以按菜品分类过滤,只检索素菜相关的内容 - 新手用户问"有什么简单的菜谱"时,可以按难度等级过滤,只返回标记为"简单"的菜谱 - 想做汤品时询问"今天喝什么汤",可以按分类过滤出所有汤品菜谱