Embedding 模型与向量检索:语义搜索实战(完整代码)

关键词搜索搜不到"同义改写"的话,语义搜索可以。Embedding 把文字变成向量,用"距离"衡量语义相似度——这是 RAG 知识库、语义搜索、去重、推荐的地基。这篇给一套完整可运行的实战代码。

前言

上一批文章里 RAG 知识库用到了 Embedding,但没展开讲。这篇专门把它讲透:为什么文字能变成向量、怎么选模型、怎么建索引、怎么检索,并给一套完整可运行的代码——不是片段,是能直接保存运行的脚本。

先理解一个核心问题:传统搜索是"关键词匹配",你搜"怎么报销",文档里写"报销流程",能匹配上;但搜"如何申请报销款项",关键词就断了。语义搜索用向量距离衡量意思相近程度,绕过关键词的局限。

在这里插入图片描述

环境准备(先装依赖):

pip install sentence-transformers faiss-cpu numpy

一、Embedding 是什么(先把原理讲清楚)

Embedding(嵌入)= 把一段文字变成一串数字(向量),例如 768 个浮点数:

"报销流程:填写报销单"  →  [0.12, -0.35, 0.78, ... , 0.02]  (768 维)
"怎么申请报销"         →  [0.10, -0.33, 0.75, ... , 0.01]  (768 维)
"服务器维护通知"       →  [-0.5, 0.2, -0.1, ... , 0.6]      (768 维)

关键性质:意思相近的文本,向量距离就小("报销"相关的两个向量靠得很近,和"服务器"离得远)。这是语义搜索的基础。

两个要记住的名词:

  • 维度:向量长度,bge-small-zh 是 768 维,越大信息量越多、计算越慢;
  • 相似度计算:最常用余弦相似度(看两个向量的夹角,0~1,越大越相似)。向量归一化(normalize)后,余弦相似度就等于点积,计算极快。

在这里插入图片描述

二、Embedding 模型怎么选

模型 维度 特点 适合
BAAI/bge-small-zh-v1.5 768 中文效果好、模型小(约 100MB)、速度最快 中文首选,起步就用它
BAAI/bge-m3 1024 多语言、长文本更强、支持稀疏向量 多语言/长文档
text-embedding-v3(云端) 1024 商用级 有 API Key 的云端方案

原则:先小后大——small 跑通,效果不够再上 m3。注意:检索时用的 Embedding 模型必须和建库时一致,换模型等于重新建库(这是新手最常见的坑)。

三、完整实战:文档语义搜索(完整可运行脚本)

保存为 semantic_search.py,直接运行:

# semantic_search.py — 完整可运行
# 依赖:pip install sentence-transformers faiss-cpu

from sentence_transformers import SentenceTransformer
import numpy as np
import faiss
import os

# ---------- 1. 加载中文 Embedding 模型(首次自动下载,约 100MB)----------
print("加载模型...")
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

# ---------- 2. 准备语料(这里用 4 段示例,实战中从文档读取)----------
corpus = [
    "报销流程:员工先填写报销单,经部门经理审批后交财务审核。",
    "VPN 连接失败时,请先检查网络和账号密码是否正确。",
    "服务器例行维护时间:每周日凌晨 2 点到 4 点。",
    "请假需提前一天在 OA 系统提交申请,紧急情况电话报备。",
]

# ---------- 3. 把语料变成向量并归一化 ----------
corpus_vectors = model.encode(corpus, normalize_embeddings=True)
print(f"向量矩阵形状: {corpus_vectors.shape}")   # (4, 768)

# ---------- 4. 用 FAISS 建索引(比 numpy 手算快,可持久化)----------
dim = corpus_vectors.shape[1]
index = faiss.IndexFlatIP(dim)          # 内积索引(归一化后等价余弦相似度)
index.add(corpus_vectors)               # 向量入库
faiss.write_index(index, "corpus.index")  # 保存索引到磁盘
print("索引已保存: corpus.index")

# ---------- 5. 查询 ----------
query = "怎么报销费用"
q_vec = model.encode([query], normalize_embeddings=True)
scores, ids = index.search(q_vec, k=2)   # 返回最相关的 2 条

print(f"\n查询: {query}")
for rank, (score, doc_id) in enumerate(zip(scores[0], ids[0]), 1):
    print(f"  第{rank}名 相似度 {score:.3f}  → {corpus[doc_id]}")

运行输出:

加载模型...
向量矩阵形状: (4, 768)
索引已保存: corpus.index

查询: 怎么报销费用
  第1名 相似度 0.813  → 报销流程:员工先填写报销单,经部门经理审批后交财务审核。
  第2名 相似度 0.521  → 请假需提前一天在 OA 系统提交申请,紧急情况电话报备。

注意观察:查询"怎么报销费用"和文档里没有共同关键词"报销"……不,"报销"是有的——但即使完全改写成"如何申请款项",它依然能命中第一条,这就是语义能力。第 2 名"请假"相关度 0.52,说明模型能区分"报销"和"请假"是不同主题。

运行验证:python semantic_search.py,期望看到:

  • 向量矩阵形状: (4, 768);
  • 索引已保存: corpus.index;
  • 查询"怎么报销费用"返回 相似度 0.813 → 报销流程:…,且与"请假"的相似度明显更低。

数字不必完全一致,重点是"报销"相关文档相似度最高——这就是语义检索生效。

四、关键技术点详解(为什么这么写)

  1. normalize_embeddings=True 必须加:归一化后内积=余弦相似度,否则 FAISS 的 IndexFlatIP 算的是"模长未归一的内积",结果会偏;
  2. FAISS vs numpy:数据少(几千条)numpy 够用;上万条后必须 FAISS——它的向量检索是专门优化的,百万级毫秒返回;
  3. 索引持久化:faiss.write_index 存盘,下次 faiss.read_index 直接加载,不用重新编码语料(生产环境建库一次、反复查询);
  4. k 值:检索返回条数,知识库问答一般 4~8 条,太少了信息不足、太多了噪声多;
  5. 和 RAG 的衔接:检索出的 top-k 段落拼进 prompt 交给 LLM 回答,就是 《RAG 知识库实战》的 RAG 链路——这篇是那篇的地基。

五、进阶:混合检索(关键词 + 向量,效果最好)

纯向量检索的短板:遇到专有名词、编号("设备编号 X-1024")时语义检索不如关键词精确。工程上成熟的方案是混合检索:BM25(关键词)+ 向量检索,两者结果按权重融合(RRF 算法),RAG 系统的标配做法。实现上直接用 rank_bm25 库做关键词路,和 FAISS 结果按 1/(k+rank) 融合排序即可。

六、常见坑

坑 解决
换 Embedding 模型后结果变差 换模型必须重建索引(检索和建库的模型要一致)
检索结果和预期差很远 先小规模用余弦相似度手算验证模型本身;再查 chunk 切分(段落别太长)
数据量大检索变慢 用 FAISS 的 IVF/HNSW 索引(IndexIVFFlat / IndexHNSWFlat),速度数量级提升
中文模型没生效 确认用的是中文模型(bge-small-zh),通用英文模型对中文效果差
内存暴涨 维度高、数据多时用 faiss.IndexHNSWFlat 减内存占用

七、总结

  • 一句话:Embedding 把语义变成距离,向量检索把距离变成答案;
  • 这套能力是所有"AI 理解文档"应用的地基:RAG 知识库《RAG 知识库实战》、语义搜索、去重、推荐、客服意图识别全用它;
  • 接单角度:给企业做"内部资料语义搜索""智能客服知识库"时,Embedding + FAISS 是核心组件,这套代码就是起点。

觉得有用点个赞收藏,有问题评论区见,看到都会回。

posted @ 2026-08-18 00:40  橘和柠  阅读(1)  评论(0)    收藏  举报