为什么需要语义检索
传统搜索靠关键词匹配:你搜「怎么请假」,它只找包含「请假」两个字的文档。但用户真实想问的可能是「年假申请流程」,文档里一个「请假」字都没有,传统搜索就找不到。语义检索解决的就是这个问题——它理解你问的意思,找的是语义上相关的内容,而不是字面一样的内容。
实现方式是把所有文档和你的问题都用 Embedding 模型变成向量,然后在向量空间里找距离最近的那些。语义相近的句子,向量在空间里也靠近。这就是向量数据库存在的意义:它专门存这些高维向量,并提供快速的最近邻搜索。
语义检索不是「搜关键词」,而是「找意思相近的内容」。Embedding 是桥梁,向量数据库是仓库,重排是精筛。
- 语义检索用向量相似度代替关键词匹配,理解用户真实意图。
- Embedding把文本变成固定长度的向量,是语义检索的基础。
- 向量索引用 HNSW 等结构加速最近邻搜索,从线性扫描降到毫秒级。
- 重排召回粗筛之后用更精细的模型重新排序,提升结果质量。
Embedding 模型选型
Embedding 模型决定了语义检索的天花板。再好的索引和重排,如果 Embedding 把意思完全不同的句子映到一起,后面都救不回来。选模型时要看几件事:中文效果好不好、向量维度多少、推理速度多快、有没有开源版本可以本地部署。
| 模型 | 特点 | 适用场景 |
|---|---|---|
| 通用开源 Embedding | 中英文效果均衡,可本地部署 | 大多数业务场景 |
| API Embedding | 效果好但要联网,按量付费 | 快速验证、数据量小 |
| 领域微调 Embedding | 用业务数据微调过,垂直场景准 | 专业领域知识库 |
# 用开源 Embedding 模型的最小示例
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-zh")
# 把文档和问题都编码成向量
doc_vectors = model.encode(["年假怎么请", "报销流程是什么"])
query_vector = model.encode("休假申请步骤")
# 计算相似度
from numpy import dot
similarity = dot(query_vector, doc_vectors[0])
换 Embedding 模型是提升检索效果最直接的手段。如果召回结果总是不对,先别着急调索引参数,换个更好的 Embedding 模型试试。
向量索引与相似度度量
向量数据库存了成千上万条向量,每条可能有几百维。直接和每条算距离(暴力搜索)在数据量大的时候太慢。所以向量数据库都用近似最近邻(ANN)索引来加速,常见的是 HNSW——一种分层图结构,搜索时从顶层快速定位再逐层细化。
相似度度量也有讲究:余弦相似度衡量方向是否一致,适合文本;欧氏距离衡量绝对距离,适合图像。大多数文本语义检索默认用余弦相似度。
# 常见的向量数据库选型
# Chroma:轻量嵌入式,适合原型和小项目
# FAISS:Meta 开源的高性能向量库,适合研究和大项目
# Qdrant / Milvus:生产级向量数据库,支持过滤、分布式
# 用 Chroma 的最小示例
import chromadb
client = chromadb.Client()
collection = client.create_collection("docs")
collection.add(
ids=["doc1", "doc2"],
documents=["年假怎么请", "报销流程"],
embeddings=[emb1, emb2]
)
results = collection.query(query_embeddings=[q_vec], n_results=5)
召回策略与 ANN 加速
召回就是从向量库里捞出最可能相关的一批候选。ANN 是「近似」最近邻,意味着它牺牲一点精度换速度——可能漏掉极个别真正最近的,但绝大多数情况下够用。你可以通过调整索引参数(比如 HNSW 的搜索宽度)来在精度和速度之间权衡。
实践中通常召回 20 到 50 个候选,再交给重排环节精选。召回阶段不求精准,只求「该找的都别漏掉」;精排阶段再从这批候选里挑出最相关的几个。
- 宁多勿漏召回量设大一点,把候选池放宽,交给重排去筛。
- 混合检索向量召回加关键词召回两路并行,互补提升覆盖率。
- 加过滤条件按文档类型、时间范围预过滤,缩小搜索空间。
重排:从粗排到精排
向量召回是粗排,它用的是通用 Embedding,不一定贴合你的业务。重排就是拿召回的这批候选,用一个更精细的模型(比如 Cross-Encoder)重新给每个候选和问题的匹配程度打分,然后按新分数排序。
# 重排的典型流程
# 1. 向量召回 50 个候选
candidates = vector_db.search(query, top_k=50)
# 2. Cross-Encoder 精排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM")
scores = reranker.predict([(query, c.text) for c in candidates])
# 3. 按精排分数取前 5
top5 = sorted(zip(candidates, scores), key=lambda x: -x[1])[:5]
重排是把召回质量从「差不多」拉到「很准」的关键一步。它比纯向量检索慢,但因为只对几十个候选打分,总耗时仍然可控。
常见问题与优化
语义检索上线后常见的问题:召回不到该找的内容——多半是 Embedding 不行或者文档切分不合理;召回了一堆不相关内容——要加重排或者调相似度阈值;新文档加进去搜不到——检查索引更新流程是不是有延迟。
优化的优先级是:先换更好的 Embedding 模型,再调整文档切分粒度,然后加混合检索(向量加关键词),最后加重排。顺序不要搞反——在 Embedding 不行的时候去调索引参数,收益非常有限。
从实验到生产
从实验到生产,向量检索系统要补上几件事:索引的增量更新机制(新文档及时进库)、检索质量的持续评测(定期跑评测集看召回率有没有下降)、以及性能监控(查询延迟、QPS、索引大小)。
向量检索系统的成熟标志,不是「能搜到」,而是你能说出每天召回了多少条、精排准确率多少、新文档加进去多久能被搜到。