为什么需要语义检索

传统搜索靠关键词匹配:你搜「怎么请假」,它只找包含「请假」两个字的文档。但用户真实想问的可能是「年假申请流程」,文档里一个「请假」字都没有,传统搜索就找不到。语义检索解决的就是这个问题——它理解你问的意思,找的是语义上相关的内容,而不是字面一样的内容。

实现方式是把所有文档和你的问题都用 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]

重排是把召回质量从「差不多」拉到「很准」的关键一步。它比纯向量检索慢,但因为只对几十个候选打分,总耗时仍然可控。