本地 RAG 检索器:加载 FAISS 索引并实现语义搜索
【学习记录】本地 RAG 检索器:加载 FAISS 索引并实现语义搜索
在前一篇文章中,我们构建了 PDF → 文本 → 向量 → FAISS 索引的完整流水线。本文展示如何使用该索引进行语义检索:加载已保存的 FAISS 索引和 LlamaIndex 存储上下文,创建一个不依赖 LLM 的检索器(Retriever),接受用户查询,返回最相似的文本片段及其相关度分数。代码完全独立,适合集成到问答系统或进一步分析检索结果。
📌 目录
- 功能概述
- 环境配置与依赖
- 核心原理解析
- 完整代码(检索脚本)
- 执行方法
- 运行效果示例
- 注意事项
- 总结
一、功能概述
本脚本实现了以下功能:
- 加载 Embedding 模型:使用与构建索引时相同的 HuggingFace 中文嵌入模型
BAAI/bge-small-zh-v1.5,确保向量空间一致。 - 读取 FAISS 索引:从磁盘加载之前保存的 FAISS 索引文件
vector_store.faiss。 - 恢复 StorageContext:利用 LlamaIndex 的持久化机制,恢复文档存储(docstore)和索引结构。
- 创建检索器:基于索引构建一个检索器(
retriever),支持top_k参数。 - 交互式检索:循环接受用户输入查询,输出最相关的文本片段及其相关度分数(L2 距离转换后的相似度)。
适用场景:在 RAG 系统中,可以先使用此脚本验证检索效果,调试chunk_size、top_k等参数,再接入 LLM 生成最终答案。
二、环境配置与依赖
Python 库依赖
pipinstallllama-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.json、index_store.json等 LlamaIndex 元数据文件。
三、核心原理解析
3.1 Embedding 模型一致性
检索时使用的嵌入模型必须与构建索引时完全相同(包括模型名称、维度)。本脚本显式设置了相同的EMBED_MODEL和device="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.json和index_store.json,恢复文档节点和索引结构。
3.4 创建检索器
retriever=index.as_retriever(similarity_top_k=TOP_K)- 检索器只执行相似度搜索,不调用 LLM 生成答案。
- 返回的每个节点带有
score属性(对于 L2 距离索引,score是负的欧氏距离,越接近 0 表示越相似;实际使用中可转换为余弦相似度或直接使用)。
3.5 检索流程
- 用户输入查询文本
query。 - 脚本调用
Settings.embed_model.get_text_embedding(query)将查询转换为向量(内部自动完成)。 - FAISS 索引执行
search操作,返回top_k个最相似的向量索引及其距离。 - 通过
storage_context将索引映射回原始文本节点,连同元数据一起返回。
四、完整代码(检索脚本)
创建文件search_index.py,内容如下:
importosimportsysfromllama_index.coreimport(StorageContext,load_index_from_storage,Settings)fromllama_index.embeddings.huggingfaceimportHuggingFaceEmbeddingfromllama_index.vector_stores.faissimportFaissVectorStoreimportfaiss# ==================================================# 配置(与构建脚本保持一致)# ==================================================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")ifnotos.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("索引加载成功")exceptExceptionase:print(f"索引加载失败:{e}")sys.exit(1)# 获取文档节点数量doc_count=len(index.docstore.docs)ifhasattr(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)whileTrue:query=input("\n请输入检索查询:").strip()ifquery.lower()in['exit','quit','q']:print("退出程序。")breakifnotquery:continueprint("\n检索中...")nodes_with_scores=retriever.retrieve(query)print("\n【检索到的文档片段】")ifnodes_with_scores:foridx,node_with_scoreinenumerate(nodes_with_scores,1):score=node_with_score.scoreifhasattr(node_with_score,'score')else'N/A'node=node_with_score.nodeprint(f"\n片段{idx}(相关度:{score:.4f})")snippet=node.text[:800]+"..."iflen(node.text)>800elsenode.textprint(f"文本:\n{snippet}")ifnode.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.py5.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 索引可能占用大量内存。 | 使用faiss的read_index时,可设置mmap模式(但 LlamaIndex 适配器可能需要全量加载)。 |
| 分数解释 | score不是标准的余弦相似度,而是 FAISS 返回的距离转换值。 | 可忽略具体数值,仅用于排序。 |
| 无检索结果 | 可能查询与文档内容完全不相关,或索引中文本过少。 | 尝试更换查询词,或增加chunk_size重新构建索引。 |
八、总结
本文提供了一个即用型语义检索脚本,实现了:
- ✅ 加载 FAISS 索引和 LlamaIndex 存储。
- ✅ 创建不依赖 LLM 的检索器。
- ✅ 交互式查询,展示相似文本片段及相关度分数。
- ✅ 可直接集成到 RAG 系统的检索环节,或用于调试分块策略。
通过这个脚本,你可以:
- 验证索引质量:检查检索结果是否符合预期。
- 调整
TOP_K和CHUNK_SIZE参数,观察召回效果变化。 - 将检索到的节点传递给 LLM,构建完整的问答系统。
下一步可以结合上一篇文章的构建脚本,形成一套完整的本地 RAG 知识库预处理与检索工具链。
