【学习记录】本地 RAG 检索器:加载 FAISS 索引并实现语义搜索

在前一篇文章中,我们构建了 PDF → 文本 → 向量 → FAISS 索引的完整流水线。本文展示如何使用该索引进行语义检索:加载已保存的 FAISS 索引和 LlamaIndex 存储上下文,创建一个不依赖 LLM 的检索器(Retriever),接受用户查询,返回最相似的文本片段及其相关度分数。代码完全独立,适合集成到问答系统或进一步分析检索结果。


📌 目录

  1. 功能概述
  2. 环境配置与依赖
  3. 核心原理解析
  4. 完整代码(检索脚本)
  5. 执行方法
  6. 运行效果示例
  7. 注意事项
  8. 总结

一、功能概述

本脚本实现了以下功能:

  1. 加载 Embedding 模型:使用与构建索引时相同的 HuggingFace 中文嵌入模型 BAAI/bge-small-zh-v1.5,确保向量空间一致。
  2. 读取 FAISS 索引:从磁盘加载之前保存的 FAISS 索引文件 vector_store.faiss
  3. 恢复 StorageContext:利用 LlamaIndex 的持久化机制,恢复文档存储(docstore)和索引结构。
  4. 创建检索器:基于索引构建一个检索器(retriever),支持 top_k 参数。
  5. 交互式检索:循环接受用户输入查询,输出最相关的文本片段及其相关度分数(L2 距离转换后的相似度)。

适用场景:在 RAG 系统中,可以先使用此脚本验证检索效果,调试 chunk_sizetop_k 等参数,再接入 LLM 生成最终答案。


二、环境配置与依赖

Python 库依赖

pip install llama-index-core llama-index-embeddings-huggingface llama-index-vector-stores-faiss faiss-cpu sentence-transformers
作用
llama-index-core LlamaIndex 核心,提供索引加载、检索器等功能
llama-index-embeddings-huggingface HuggingFace 嵌入模型适配器
llama-index-vector-stores-faiss FAISS 向量存储适配器
faiss-cpu FAISS 库(CPU 版本)
sentence-transformers 嵌入模型依赖(自动安装)

前置要求

  • 已经运行过上一篇文章中的构建脚本,生成了 ./storage/faiss_index 目录,其中包含:
    • vector_store.faiss:FAISS 索引文件。
    • docstore.jsonindex_store.json 等 LlamaIndex 元数据文件。

三、核心原理解析

3.1 Embedding 模型一致性

检索时使用的嵌入模型必须与构建索引时完全相同(包括模型名称、维度)。本脚本显式设置了相同的 EMBED_MODELdevice="cpu",保证查询向量与索引向量在同一空间。

3.2 加载 FAISS 索引

faiss_index = faiss.read_index(faiss_path)
  • faiss.read_index 直接读取原生 FAISS 索引文件,获得一个 faiss.Index 对象。
  • 通过 faiss_index.ntotal 可以查看索引中的向量总数。

3.3 恢复 StorageContext

vector_store = FaissVectorStore(faiss_index=faiss_index)
storage_context = StorageContext.from_defaults(persist_dir=INDEX_DIR, vector_store=vector_store)
  • 由于我们已经手动加载了 FAISS 索引,需要将其包装成 FaissVectorStore 对象。
  • StorageContext.from_defaults 再从 persist_dir 中读取 docstore.jsonindex_store.json,恢复文档节点和索引结构。

3.4 创建检索器

retriever = index.as_retriever(similarity_top_k=TOP_K)
  • 检索器只执行相似度搜索,不调用 LLM 生成答案。
  • 返回的每个节点带有 score 属性(对于 L2 距离索引,score 是负的欧氏距离,越接近 0 表示越相似;实际使用中可转换为余弦相似度或直接使用)。

3.5 检索流程

  1. 用户输入查询文本 query
  2. 脚本调用 Settings.embed_model.get_text_embedding(query) 将查询转换为向量(内部自动完成)。
  3. FAISS 索引执行 search 操作,返回 top_k 个最相似的向量索引及其距离。
  4. 通过 storage_context 将索引映射回原始文本节点,连同元数据一起返回。

四、完整代码(检索脚本)

创建文件 search_index.py,内容如下:

import os
import sys
from llama_index.core import (
    StorageContext,
    load_index_from_storage,
    Settings
)
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.faiss import FaissVectorStore
import faiss

# ==================================================
# 配置(与构建脚本保持一致)
# ==================================================
INDEX_DIR = "./storage/faiss_index"
EMBED_MODEL = "BAAI/bge-small-zh-v1.5"
TOP_K = 5  # 检索 top-k 相似片段

# ==================================================
# 初始化 Embedding 模型
# ==================================================
print("加载 Embedding 模型...")
Settings.embed_model = HuggingFaceEmbedding(
    model_name=EMBED_MODEL,
    device="cpu"
)

# ==================================================
# 加载 FAISS 索引
# ==================================================
faiss_path = os.path.join(INDEX_DIR, "vector_store.faiss")
if not os.path.exists(faiss_path):
    print(f"错误:索引文件不存在 - {faiss_path}")
    sys.exit(1)

print("读取 FAISS 索引...")
faiss_index = faiss.read_index(faiss_path)
print(f"FAISS 索引维度:{faiss_index.d}, 向量数量:{faiss_index.ntotal}")

vector_store = FaissVectorStore(faiss_index=faiss_index)

print("加载 StorageContext...")
storage_context = StorageContext.from_defaults(
    persist_dir=INDEX_DIR,
    vector_store=vector_store
)

print("加载索引...")
try:
    index = load_index_from_storage(storage_context)
    print("索引加载成功")
except Exception as e:
    print(f"索引加载失败:{e}")
    sys.exit(1)

# 获取文档节点数量
doc_count = len(index.docstore.docs) if hasattr(index, 'docstore') else "未知"
print(f"索引中的文档节点数:{doc_count}")

# ==================================================
# 创建检索器(不包含 LLM 生成)
# ==================================================
retriever = index.as_retriever(similarity_top_k=TOP_K)

# ==================================================
# 交互检索循环
# ==================================================
print("\n" + "="*60)
print("索引加载成功!现在仅进行检索(不调用 LLM)。")
print("输入 'exit' 或 'quit' 退出程序。")
print("="*60)

while True:
    query = input("\n请输入检索查询:").strip()
    if query.lower() in ['exit', 'quit', 'q']:
        print("退出程序。")
        break
    if not query:
        continue

    print("\n检索中...")
    nodes_with_scores = retriever.retrieve(query)

    print("\n【检索到的文档片段】")
    if nodes_with_scores:
        for idx, node_with_score in enumerate(nodes_with_scores, 1):
            score = node_with_score.score if hasattr(node_with_score, 'score') else 'N/A'
            node = node_with_score.node
            print(f"\n片段 {idx} (相关度: {score:.4f})")
            snippet = node.text[:800] + "..." if len(node.text) > 800 else node.text
            print(f"文本:\n{snippet}")
            if node.metadata:
                print(f"元数据: {node.metadata}")
            print("-" * 40)
    else:
        print("未检索到任何片段(可能索引为空或查询无匹配)。")

    print("\n" + "="*60)

五、执行方法

5.1 确保索引已存在

首先运行上一篇文章中的构建脚本(build_index.py),生成 ./storage/faiss_index 目录。

5.2 运行检索脚本

在终端中执行:

python search_index.py

5.3 交互示例

加载 Embedding 模型...
读取 FAISS 索引...
FAISS 索引维度:512, 向量数量:126
加载 StorageContext...
加载索引...
索引加载成功
索引中的文档节点数:126

============================================================
索引加载成功!现在仅进行检索(不调用 LLM)。
输入 'exit' 或 'quit' 退出程序。
============================================================

请输入检索查询:医疗器械分类规则

检索中...

【检索到的文档片段】

片段 1 (相关度: 0.8234)
文本:
医疗器械按照风险程度分为三类:第一类是风险较低,第二类是中度风险,第三类是较高风险...
元数据: {'source': 'YY/T0664-2020'}
----------------------------------------

片段 2 (相关度: 0.7651)
...

六、运行效果示例

假设索引中包含某医疗器械标准文档的内容,查询“分类规则”会返回相关段落及其相关度分数。score 对于 L2 距离索引,实际是负的欧氏距离(越大表示越相似)。用户可以根据分数阈值过滤低相关片段。


七、注意事项

问题 说明 解决方案
模型不一致 检索时使用的嵌入模型与构建时不同,会导致向量空间不匹配,检索结果完全错误。 确保 EMBED_MODEL 与构建脚本完全一致。
索引路径错误 脚本中 INDEX_DIR 必须指向正确的目录。 使用绝对路径或确认相对路径正确。
内存不足 加载大型 FAISS 索引可能占用大量内存。 使用 faissread_index 时,可设置 mmap 模式(但 LlamaIndex 适配器可能需要全量加载)。
分数解释 score 不是标准的余弦相似度,而是 FAISS 返回的距离转换值。 可忽略具体数值,仅用于排序。
无检索结果 可能查询与文档内容完全不相关,或索引中文本过少。 尝试更换查询词,或增加 chunk_size 重新构建索引。

八、总结

本文提供了一个即用型语义检索脚本,实现了:

  • ✅ 加载 FAISS 索引和 LlamaIndex 存储。
  • ✅ 创建不依赖 LLM 的检索器。
  • ✅ 交互式查询,展示相似文本片段及相关度分数。
  • ✅ 可直接集成到 RAG 系统的检索环节,或用于调试分块策略。

通过这个脚本,你可以:

  • 验证索引质量:检查检索结果是否符合预期。
  • 调整 TOP_KCHUNK_SIZE 参数,观察召回效果变化。
  • 将检索到的节点传递给 LLM,构建完整的问答系统。

下一步可以结合上一篇文章的构建脚本,形成一套完整的本地 RAG 知识库预处理与检索工具链。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐