# 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
1

GPU 版本要依赖 CUDA 环境,部署复杂度更高:

pip install -U faiss-gpu
1

多数 RAG 服务从 CPU 版本开始就够了。GPU 版本更适合大规模离线建库、批量检索评估、图像向量检索等高吞吐场景。

LangChain 当前的 FAISS 封装位于:

from langchain_community.vectorstores import FAISS
1

这个包路径很重要。不要再把旧材料里的导入方式当成唯一标准,新项目应以当前 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)
1
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() 做了几件事:

  1. 调用 Embedding 模型,把文本转成向量。
  2. 创建 FAISS index。
  3. 保存文本内容到 docstore。
  4. 建立 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,
)
1
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,
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

生产里一定要认真设计 metadata。RAG 检索通常不只是语义相似,还要受来源、租户、权限、文档状态、时间范围、业务类型约束。

# 检索接口

LangChain 为 VectorStore 抽象了统一检索接口,FAISS 也遵循这些方法。

返回最相似的文档:

docs = vector_store.similarity_search(
    "我养了一只猫,叫笨笨",
    k=3,
)
1
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)
1
2
3
4
5
6
7

这里的 score 不一定是“越接近 1 越相关”的相关性分数。对默认 L2 距离来说,它更接近距离值,通常越小越近。

# similarity_search_with_relevance_scores

返回归一化相关性分数:

results = vector_store.similarity_search_with_relevance_scores(
    "我养了一只猫,叫笨笨",
    k=3,
)
1
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("猫咪相关的内容")
1
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"},
)
1
2
3
4
5

也可以传函数:

docs = vector_store.similarity_search_with_score(
    "猫咪相关的内容",
    k=2,
    filter=lambda metadata: metadata.get("page", 0) > 5,
)
1
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"},
)
1
2
3
4
5
6

如果权限过滤是强约束,不建议只依赖检索后过滤。更稳的做法是按租户、数据域或权限边界拆分索引,至少把不可越权的数据放到不同 collection/index 里。

# 删除数据

FAISS 封装支持按 id 删除:

ids = vector_store.add_texts(
    texts=["待删除的文档"],
    ids=["doc-001"],
)

vector_store.delete(ids=["doc-001"])
1
2
3
4
5
6

需要注意,向量数据库里的“删除”不等于业务数据库里的“更新”。如果文档内容变了,生产里更常见的做法是:

  1. 给新文档生成新的 chunk 和 embedding。
  2. 写入新索引版本。
  3. 切换检索流量。
  4. 下线旧索引。

对于小型 FAISS 索引,可以直接 delete + add;对于重要业务索引,建议把重建索引当成可回滚发布流程。

# 保存与加载

FAISS 可以本地持久化:

vector_store.save_local("./vector-store/faiss-index")
1

加载:

new_store = FAISS.load_local(
    "./vector-store/faiss-index",
    embeddings,
    allow_dangerous_deserialization=True,
)
1
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
1
2
3

如果使用 cosine similarity,常见做法是先把向量归一化,再用内积或 L2 的等价关系处理。很多 Embedding 模型的示例会设置:

encode_kwargs={"normalize_embeddings": True}
1

生产里不要混着来:

  • 建库时是否 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={},
)
1
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
1
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
}
1
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"},
    )
1
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 权限边界、安全加载、评估集、发布和回滚。

参考: