# FAISS 向量数据库实践:从本地检索到 LangChain 生产封装
FAISS 经常出现在 RAG demo 里,因为它轻、快、好接入。
但如果只把它理解成“一个向量数据库”,很容易在生产里踩坑。更准确地说,FAISS 是一个高性能向量相似度搜索库。它擅长做向量索引和近邻检索,但不负责完整数据库系统里的权限、多租户、分布式副本、事务、审计、在线 schema 迁移和托管运维。
所以使用 FAISS 前,先把定位说清楚:
- 它适合本地开发、离线评估、小中规模知识库、单机内网检索。
- 它适合做向量检索内核,而不是完整的业务数据治理平台。
- 它可以通过 LangChain 快速接入 RAG 链路,但 LangChain 封装不等于生产能力完整。
# 向量数据库的几种形态
按照部署方式,可以把常见向量存储分成三类。
| 类型 | 代表 | 特点 |
|---|---|---|
| 本地文件型 | FAISS | 部署轻、速度快、适合单机和离线任务 |
| 本地服务型 | Milvus、Qdrant、Weaviate | 能作为独立服务运行,功能更完整 |
| 云端托管型 | Pinecone、TCVectorDB、MongoDB Atlas Vector Search | 运维成本低,权限、弹性、监控和 SLA 更完整 |
FAISS 属于第一类。它像一个高性能检索引擎库,而不是一个开箱即用的云数据库。
如果只是做 Python 项目里的本地知识库、离线评估集、个人检索工具,FAISS 很合适。如果要做多租户企业知识库、权限过滤复杂、数据频繁更新、需要高可用和多副本,FAISS 通常需要额外工程层兜住。
# 安装与基本依赖
CPU 版本:
pip install -U faiss-cpu langchain-community langchain-openai
GPU 版本要依赖 CUDA 环境,部署复杂度更高:
pip install -U faiss-gpu
多数 RAG 服务从 CPU 版本开始就够了。GPU 版本更适合大规模离线建库、批量检索评估、图像向量检索等高吞吐场景。
LangChain 当前的 FAISS 封装位于:
from langchain_community.vectorstores import FAISS
这个包路径很重要。不要再把旧材料里的导入方式当成唯一标准,新项目应以当前 API 参考为准。
# 最小可用示例
先看一个最小例子:把文本列表写入 FAISS,然后做相似度检索。
import os
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
api_key=os.environ["OPENAI_API_KEY"],
)
texts = [
"笨笨是一只很喜欢睡觉的猫。",
"猫咪在窗台上打盹,看起来非常可爱。",
"学习新技能是每个人都应该追求的目标。",
"我的手机突然关机了,让我有些焦虑。",
]
vector_store = FAISS.from_texts(texts, embeddings)
docs = vector_store.similarity_search(
"我养了一只猫,叫笨笨",
k=2,
)
for doc in docs:
print(doc.page_content)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FAISS.from_texts() 做了几件事:
- 调用 Embedding 模型,把文本转成向量。
- 创建 FAISS index。
- 保存文本内容到 docstore。
- 建立 FAISS index 行号到 docstore id 的映射。
这就是为什么 LangChain 的 FAISS 对象里不只有 FAISS index,还有 docstore 和 index_to_docstore_id。
# from_texts 和 from_documents
如果只有纯文本,用 from_texts() 即可。
vector_store = FAISS.from_texts(
texts=texts,
embedding=embeddings,
)
2
3
4
如果有 metadata,推荐使用 Document 和 from_documents():
from langchain_core.documents import Document
documents = [
Document(
page_content="笨笨是一只很喜欢睡觉的猫。",
metadata={"source": "pet.md", "page": 1, "tenant_id": "t1"},
),
Document(
page_content="学习新技能是每个人都应该追求的目标。",
metadata={"source": "study.md", "page": 2, "tenant_id": "t1"},
),
]
vector_store = FAISS.from_documents(
documents=documents,
embedding=embeddings,
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
生产里一定要认真设计 metadata。RAG 检索通常不只是语义相似,还要受来源、租户、权限、文档状态、时间范围、业务类型约束。
# 检索接口
LangChain 为 VectorStore 抽象了统一检索接口,FAISS 也遵循这些方法。
# similarity_search
返回最相似的文档:
docs = vector_store.similarity_search(
"我养了一只猫,叫笨笨",
k=3,
)
2
3
4
# similarity_search_with_score
返回文档和原始距离分数:
results = vector_store.similarity_search_with_score(
"我养了一只猫,叫笨笨",
k=3,
)
for doc, score in results:
print(score, doc.page_content)
2
3
4
5
6
7
这里的 score 不一定是“越接近 1 越相关”的相关性分数。对默认 L2 距离来说,它更接近距离值,通常越小越近。
# similarity_search_with_relevance_scores
返回归一化相关性分数:
results = vector_store.similarity_search_with_relevance_scores(
"我养了一只猫,叫笨笨",
k=3,
)
2
3
4
生产里要谨慎依赖这个分数。不同 Embedding 模型、距离策略、是否归一化,都会影响分数分布。不要随手设一个全局阈值就上线。
# as_retriever
把 FAISS 包装成 Retriever:
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={
"k": 4,
"fetch_k": 20,
"lambda_mult": 0.5,
},
)
docs = retriever.invoke("猫咪相关的内容")
2
3
4
5
6
7
8
9
10
mmr 可以降低返回结果之间的重复度,适合文档 chunk 内容相似度很高的知识库。
# metadata filter 的真实边界
LangChain 的 FAISS 支持 filter:
docs = vector_store.similarity_search(
"猫咪相关的内容",
k=2,
filter={"tenant_id": "t1"},
)
2
3
4
5
也可以传函数:
docs = vector_store.similarity_search_with_score(
"猫咪相关的内容",
k=2,
filter=lambda metadata: metadata.get("page", 0) > 5,
)
2
3
4
5
但要理解边界:FAISS 本体并不是 metadata 数据库。LangChain 的过滤通常是在候选结果上做处理,因此会涉及 fetch_k。
比如你要返回 k=5 条结果,但过滤条件很严格,如果默认只先取 fetch_k=20,过滤后可能不足 5 条。生产里需要根据过滤条件调整:
docs = vector_store.similarity_search(
"报销流程",
k=5,
fetch_k=100,
filter={"tenant_id": "enterprise-a", "status": "published"},
)
2
3
4
5
6
如果权限过滤是强约束,不建议只依赖检索后过滤。更稳的做法是按租户、数据域或权限边界拆分索引,至少把不可越权的数据放到不同 collection/index 里。
# 删除数据
FAISS 封装支持按 id 删除:
ids = vector_store.add_texts(
texts=["待删除的文档"],
ids=["doc-001"],
)
vector_store.delete(ids=["doc-001"])
2
3
4
5
6
需要注意,向量数据库里的“删除”不等于业务数据库里的“更新”。如果文档内容变了,生产里更常见的做法是:
- 给新文档生成新的 chunk 和 embedding。
- 写入新索引版本。
- 切换检索流量。
- 下线旧索引。
对于小型 FAISS 索引,可以直接 delete + add;对于重要业务索引,建议把重建索引当成可回滚发布流程。
# 保存与加载
FAISS 可以本地持久化:
vector_store.save_local("./vector-store/faiss-index")
加载:
new_store = FAISS.load_local(
"./vector-store/faiss-index",
embeddings,
allow_dangerous_deserialization=True,
)
2
3
4
5
这里的 allow_dangerous_deserialization=True 很关键。LangChain 加载本地 FAISS 时可能涉及 pickle 反序列化,反序列化不可信文件有安全风险。
生产建议:
- 只加载自己构建并校验过的索引文件。
- 索引文件不要允许用户上传后直接加载。
- 对索引目录做 checksum 或签名校验。
- 把 FAISS index、docstore、embedding model、维度、chunk 策略版本一起记录。
- 线上发布时走制品仓库或对象存储,不要手工拷贝目录。
# 分数与距离策略
FAISS 里常见的是 L2 距离和内积检索。LangChain 的 FAISS 初始化参数里也有:
distance_strategy
normalize_L2
relevance_score_fn
2
3
如果使用 cosine similarity,常见做法是先把向量归一化,再用内积或 L2 的等价关系处理。很多 Embedding 模型的示例会设置:
encode_kwargs={"normalize_embeddings": True}
生产里不要混着来:
- 建库时是否 normalize。
- 查询时是否 normalize。
- FAISS index 类型是什么。
- LangChain distance strategy 是什么。
- 业务阈值怎么解释。
这些必须一致,否则召回排序会变得很难解释。
# 手动构建 FAISS Index
如果你想明确控制 index 类型,可以手动创建:
import faiss
from langchain_community.docstore.in_memory import InMemoryDocstore
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
dimension = len(embeddings.embed_query("hello world"))
index = faiss.IndexFlatL2(dimension)
vector_store = FAISS(
embedding_function=embeddings,
index=index,
docstore=InMemoryDocstore(),
index_to_docstore_id={},
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
IndexFlatL2 是精确检索,适合小中规模数据和效果验证。数据量更大时,可以研究 IVF、PQ、HNSW 等索引结构,在速度、内存和召回率之间做取舍。
# FAISS 适不适合生产
答案是:适合一部分生产场景,但不是所有。
适合:
- 单机知识库。
- 内网工具。
- 离线评估。
- 数据量中小、更新不频繁。
- 希望完全自管索引文件。
- 对云数据库依赖敏感。
不太适合直接裸用:
- 多租户权限复杂。
- 数据更新频繁。
- 需要高可用、多副本、自动扩缩容。
- 需要实时 metadata 过滤和复杂查询。
- 需要统一审计、备份、监控、控制台。
这时更适合 Milvus、Qdrant、Weaviate、PGVector、Pinecone、TCVectorDB 等完整向量数据库,或者在 FAISS 外层自己补工程平台能力。
# 生产封装建议
不要让业务代码到处直接调用 FAISS.from_texts()。建议封装一个索引构建服务:
from dataclasses import dataclass
from pathlib import Path
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
@dataclass(frozen=True)
class FaissIndexConfig:
index_name: str
index_version: str
persist_dir: Path
embedding_model: str
embedding_dimension: int
chunk_version: str
def build_faiss_index(
documents: list[Document],
embeddings,
config: FaissIndexConfig,
) -> FAISS:
vector_store = FAISS.from_documents(
documents=documents,
embedding=embeddings,
)
output_dir = config.persist_dir / config.index_name / config.index_version
vector_store.save_local(str(output_dir))
return vector_store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
同时保存一份 manifest:
{
"index_name": "help-center",
"index_version": "2026-08-12-001",
"embedding_model": "text-embedding-3-small",
"embedding_dimension": 1536,
"chunk_version": "chunk-v3",
"document_count": 1200,
"chunk_count": 8400
}
2
3
4
5
6
7
8
9
索引文件和 manifest 要一起发布。没有 manifest,后续排查“为什么召回变差”会很痛苦。
# 上线前检查清单
- Embedding 模型和向量维度是否固定。
- 文档 chunk 策略是否版本化。
- metadata 是否包含租户、权限、来源、状态、时间。
- 是否有固定评估集验证 Recall@K 和 MRR。
save_local产物是否进入制品管理。load_local是否只加载可信文件。- 是否记录 index version 和 embedding version。
- 是否有索引回滚方案。
- metadata filter 是否会因为
fetch_k太小漏召回。 - 是否知道 FAISS 的删除、更新和高可用边界。
# 问题
FAISS 最容易被误用的地方有几个。
第一,把 FAISS 当成完整数据库。它不是,它主要解决向量近邻检索问题。
第二,忽略 metadata filter 的边界。过滤是在候选结果上做,权限强约束要在索引设计层解决。
第三,随意使用 relevance score。不同距离策略下分数含义不同,不要直接拿来做统一阈值。
第四,加载不可信本地索引。allow_dangerous_deserialization=True 不是随便加的参数,它意味着你要对文件来源负责。
第五,文档更新时直接在旧索引上修修补补。重要业务更适合索引版本化和灰度切换。
# 拓展
FAISS 可以继续往三个方向深入。
第一,索引结构。Flat、IVF、PQ、HNSW 的取舍会直接影响召回、内存和延迟。
第二,混合检索。FAISS 负责 dense vector,BM25 负责关键词,再加 reranker 做重排。
第三,索引平台化。把构建、评估、发布、回滚、监控做成流水线,FAISS 只是其中的检索内核。
# 实际生产是否使用
会使用。
FAISS 在生产里常见于本地化部署、离线评估、小中规模知识库、推荐召回、图片检索、向量实验平台。它的性能和生态都很成熟。
但生产里通常不会只靠一个 faiss.index 文件解决所有问题。业务侧还需要文档库、metadata 存储、权限系统、索引版本、构建任务、评估任务、发布流程和监控告警。
# 现在是否抛弃
没有抛弃。
FAISS 本身仍然是非常重要的向量检索库。LangChain 当前也仍然保留 langchain_community.vectorstores.FAISS 集成,并支持 add、delete、similarity search、MMR、save/load 等能力。
需要更新的是认知:FAISS 不是“过时的向量数据库”,也不是“生产万能方案”。它更适合作为高性能本地向量检索内核。
# 最新生产如何实现
当前更推荐这样落地:
第一,使用 langchain_community.vectorstores.FAISS 接入 LangChain。
第二,Embedding 模型、维度、chunk 策略、索引版本写入 manifest。
第三,构建索引和在线查询分离。构建任务离线生成 FAISS 目录,在线服务只加载已发布版本。
第四,加载索引时只允许可信来源,并校验 checksum。
第五,权限强隔离场景按租户或数据域拆索引,不依赖检索后过滤兜底。
第六,固定评估集验证召回质量,通过后再灰度切换。
最小生产查询结构可以这样写:
from langchain_community.vectorstores import FAISS
def load_vector_store(index_dir: str, embeddings):
return FAISS.load_local(
index_dir,
embeddings,
allow_dangerous_deserialization=True,
)
def search_knowledge(vector_store: FAISS, query: str, tenant_id: str):
return vector_store.similarity_search(
query,
k=5,
fetch_k=50,
filter={"tenant_id": tenant_id, "status": "published"},
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
真正上线时,还要把 index_dir 绑定到索引版本,把 tenant_id 从认证态里取出,把每次检索的 k、fetch_k、耗时、命中文档、score 写入 trace。
# 总结
FAISS 是 Python RAG 工程里非常好用的向量检索基础设施。它轻量、快速、可本地持久化,适合开发、评估和一部分生产场景。
但越接近生产,越不能只关注 from_texts() 和 similarity_search()。真正关键的是索引版本、Embedding 版本、metadata 权限边界、安全加载、评估集、发布和回滚。
参考: