语义搜索
本文深入浅出地讲解语义检索的基本原理,从文本向量化到相似度匹配,对比传统关键词检索,并结合实际案例说明语义检索如何解决多义词、意图理解等痛点,适合对AI检索感兴趣的技术读者。
核心洞察
这篇文章最有意思的点是,语义检索基于语义相似度而非关键词匹配的检索技术,能够理解查询意图和文档含义。听起来高深,拆开看就是给文字找语义上的邻居。传统搜索像查字典,语义检索像找人聊天,话没说完对方已经懂你意思了。不过别高兴太早,这种聪明是用钱和算力堆出来的,后面几篇会讲到它贵在哪、坑在哪。
核心结论
- 语义检索通过把文本编码为高维向量并按语义相似度匹配,能够结合上下文区分多义词(如“苹果”指水果还是公司)并召回同义表达(如“薪资”与“工资”),而传统关键词检索做不到这一点。
- 分布式词嵌入将词语映射到低维向量空间的技术,使语义相似的词在向量空间中距离相近,包括Word2Vec、GloVe等方法。将语义映射到低维连续向量(通常100到768维),使得语义相近的词在向量空间中距离更近;在STS-B语义相似度基准上,Sentence-BERTBERT的变体,专门优化句子级语义相似度计算和嵌入生成,常用于信息检索和聚类。的Pearson相关系数达到0.85,明显高于平均词向量的0.75。
- 实际语义检索系统普遍采用两阶段架构:先用Bi-Encoder从百万级文档中快速粗选出Top 1000候选,再用Cross-Encoder精排产生Top 10结果,兼顾响应速度与排序精度。
- 语义检索的规模化依赖向量数据库:FAISS适合本地百万级数据的快速近似检索;当数据量到达千万级且需要分布式管理、元数据过滤时,需使用Milvus、Pinecone等专用向量数据库。
- 语义检索存在明确代价:需要模型编码和向量存储开销,结果是概率性输出、可解释性差,因此生产系统常将传统关键词检索与语义检索混合使用,以保留前者的可解释性和效率优势。
语义检索技术:让AI更懂自然语言
一、引言:从“关键询匹配”到“语义理解”的革命
1.1 一个真实的痛点场景
先还原一个场景。你在搜索框里敲下「苹果的价格走势」,传统搜索引擎会返回什么?大概率是这几类结果:
- 苹果公司的股价波动
- iPhone 新机降价的消息
- 水果市场批发报价,占比还特别小
问题出在哪?传统检索靠关键词匹配,分不清「苹果」指的是公司、手机还是水果,也理解不了「价格走势」背后的真实意图。你可能只是想知道水果摊上的苹果贵不贵,搜出来全是股票。
换成语义检索,系统会先分析「苹果」在这个语境里更接近水果,再搞懂「价格走势」是在问时间序列上的变化,最后把「今年夏季苹果批发价预测」这类结果推到最前面。
1.2 什么是语义检索?
语义检索(Semantic Retrieval)是一种基于自然语言语义理解的信息检索技术。它把文本变成机器能算的语义表示,最常见的形式是向量,然后按语义相似度去捞信息,不死磕关键词。
整个流程一句话就能讲完:你的查询先被编码成向量,再去向量数据库里找语义上最接近的文档,最后按相关度排序返回。
1.3 语义检索 vs 传统检索:本质区别
把两种检索方式摆在一起,差别不小。
匹配逻辑不一样。传统检索做字面匹配,你搜什么词,它就找包含这些词的文档。语义检索做意思匹配。你说「怎么让电脑不卡」,它返回的是「优化系统性能的方法」。
多义词的处理是另一个分水岭。传统检索遇到「苹果」这种词,只能靠词频和权重猜,经常猜错。语义检索会结合上下文判断。同一个词放在不同句子里,生成的向量不同,准确率自然高出一截。
同义表达同样不用怕。传统检索里「薪资」和「工资」是两个词,搜一个漏一个。语义检索里这俩向量距离很近,都能被召回。
代价也有。语义检索需要模型做编码,需要向量库做存储和检索,成本比关键词检索高。而且它是个概率系统,结果不太好解释,出错了不容易说清原因。
两种技术各有各的用处。传统检索快、省、可解释。语义检索聪明、灵活,能处理复杂说法。实际产品里,很多系统是混着用的。
这篇先聊到这,下一篇我们讲文本到底怎么变成向量。
这套差异具体落在四个维度上。
| 维度 | 传统检索 | 语义检索 |
|---|---|---|
| 理解方式 | 关键词匹配,看字面意思 | 语义表示,抓深层意图 |
| 多义性处理 | 分不清"苹果"是水果还是手机 | 结合上下文消歧 |
| 相似性衡量 | 词频统计,比如TF-IDF | 向量空间相似性,比如余弦相似度一种衡量两个向量方向相似程度的度量方法,值域为[-1, 1],常用于文本嵌入向量的语义相似度计算。 |
| 效果依赖 | 用户输入得准不准 | 模型对语义理解得深不深 |
四行看完,问题就来了:文字在计算机里到底怎么存,机器才能直接比较"意思"?
二、语义检索的核心原理:从"文本"到"语义向量"的魔法
2.1 基础:语义表示,语言的"数学指纹"
语义检索的第一步,是把词、句子、文档统一变成高维向量,也就是Embedding。这类向量有两个特点。
语义相近的文本,向量距离就近。"猫"和"狗"的向量,一定比"猫"和"汽车"近得多。
语义相关的文本,向量方向就一致。"吃苹果"和"啃水果",两个句子的向量夹角很小。
2.1.1 词嵌入:从one-hot到分布式表示
最早的词表示是one-hot向量。"猫"是[1,0,0,0],"狗"是[0,1,0,0]。这种表示法有个问题:任意两个词的余弦相似度都是0。猫和狗明明都是动物,在计算机眼里却是两个孤岛。
分布式表示把每个词映射到一个低维连续向量,通常是100到768维。每一维承载一种语义特征,比如"有没有生命""是不是动物"。有了这些维度,"猫"和"狗"的向量自然就靠拢了。
Word2VecA technique for learning vector representations of words from large text corpora, capturing semantic relationships.是2013年Google发布的词嵌入模型,核心思想一句话:词的语义由上下文决定。具体有两种做法,CBOW用上下文预测目标词,Skip-gram反过来,用目标词预测上下文。
拿CBOW举例。输入是上下文词的one-hot向量,"我""喜欢""吃"三个词,过一层权重矩阵W,取平均得到隐藏向量h,再过一层权重矩阵W',输出一个词表大小的概率分布。概率最高的那个词,就是模型猜的"苹果"。训练用交叉熵损失,一轮一轮跑下来,W矩阵的每一行就沉淀为对应词的嵌入向量。
训练完的向量能看出语义关系。"猫"的嵌入可能是[0.8, -0.2, 0.5, ...],"狗"是[0.7, -0.1, 0.6, ...],两者的余弦相似度能到0.9左右。
2.1.2 句/文档嵌入:从词向量到文本向量
词嵌入只管单个词。语义检索要处理的是句子、段落、整篇文档,得把一句话压成一个固定长度的向量,还得保住整体语义。
BERT是2019年Google发布的Transformer预训练模型,抓上下文语义的能力很强。但直接拿BERT的[CLS]向量当句嵌入用,效果并不理想。BERT本来是为分类、问答设计的,句子相似度不是它的专长。
Sentence-BERT是2020年提出的改进,思路换成对比学习。训练的时候喂成对的句子,语义相近的当正例,"我喜欢吃苹果"和"苹果是我最喜欢的水果";语义无关的当负例,"我喜欢吃苹果"和"今天天气很好"。损失函数用三元组损失,目标很直白:正例对的距离尽量小,负例对的距离尽量大。
效果提升直接反映在数字上。语义相似度基准STS-B上,平均词向量的Pearson相关系数是0.75,Sentence-BERT直接拉到0.85。
2.2 关键:语义匹配,怎么衡量相似性
向量都有了,接下来就是算距离、做排序。
2.2.1 常用相似性度量
常用的有三种。
余弦相似度,算两个向量的夹角余弦,范围[-1, 1],越大越相似。公式是cosine(u,v) = (u·v) / (||u|| ||v||)。查询"如何学习Python"和文档"Python入门教程"算出来0.9,基本就是同一话题。
点积,直接算u·v,范围从负无穷到正无穷,也是越大越相似。省了模长计算,速度更快。但向量长度差异大的时候,结果容易失真。
欧氏距离,算两个向量的直线距离,范围[0, +∞],反过来,越小越相似。对异常值敏感,实际用得不多。
2.2.2 两种匹配策略:Bi-Encoder vs Cross-Encoder
Bi-Encoder的思路是各算各的。查询和文档分别过一遍编码器,得到两个向量,再用余弦相似度打分。优点快,文档向量可以提前算好存起来,查询来了只做一次向量计算。缺点也在这一步,查询和文档没有直接交互,精度上不去。Sentence-BERT、LaBSE都是这个路子。
Cross-Encoder反过来,把查询和文档拼成一个序列,中间加[SEP]分隔,整个丢进Transformer,直接输出0到1的匹配概率。精度确实高,查询里的每个词都能和文档里的每个词做注意力交互。代价是慢,每条查询都得跟所有文档拼接算一遍,文档一多,算力扛不住。
实际项目里通常混合着来。Bi-Encoder先从百万级文档里粗筛出Top 1000,Cross-Encoder再精排,从1000篇里挑出Top 10。速度不慢,精度不丢。
2.3 支撑:向量数据库,语义检索的索引引擎
文档量到百万甚至亿级,挨个算查询向量和所有文档向量的相似度,复杂度O(N),谁也等不起。向量数据库靠索引技术把复杂度降下来。
2.3.1 向量数据库的核心功能
向量数据库管四件事:
- 存向量,把文档的嵌入向量持久化存储。
- 建索引,用KD-Tree、IVF、HNSW这类算法,把向量组织成高效的检索结构。
- 相似性查询,给定查询向量,快速返回Top K个最相似的文档。
- 元数据过滤,支持带上文档类型、时间、作者这些条件一起筛,比如"只要2024年发布的Python教程"。
2.3.2 主流向量数据库对比
市面上的选择大致分三类。Milvus、Weaviate、Qdrant、Pinecone这类专门的向量数据库,把存储、索引、查询做成开箱即用的服务,功能齐全,部署和运维成本也高。Elasticsearch的向量检索插件、PostgreSQL的pgvector扩展,算是给老系统加装向量能力的折中方案,迁移成本低,但检索性能和专门的向量数据库有差距。FAISS这种开源相似性搜索库,严格说不算数据库,只负责高效算最近邻,适合技术团队自己搭检索服务。
选型看数据量。几百万条以内,pgvector或ES插件够用。到了千万级,对延迟又有要求,就得看Milvus这类专门方案了。
| 数据库 | 类型 | 优点 | 缺点 |
|---|---|---|---|
| Faiss | 本地库(C++/Python) | 速度极快(支持GPU加速),内存占用低 | 无分布式支持,无元数据管理 |
| Milvus | 分布式(Go/Python) | 支持分布式存储,元数据管理,多语言SDK | 部署复杂,资源占用高 |
| Pinecone | 云服务 | 完全托管,弹性扩展,支持实时检索 | 成本高(按向量数量收费) |
| Weaviate | 开源(Go) | 支持知识图谱融合,多模态检索 | 社区较小,文档不够完善 |
选型的时候可以这么考虑:数据量小,本地折腾着玩,Faiss 够了。数据上了百万级,要长期维护,Milvus 虽然部署麻烦点,但值得。要是团队没有运维人手,直接买 Pinecone 的服务,省心,就是烧钱。Weaviate 适合需要把知识图谱和向量检索揉在一起的场景,不过得忍受它的文档和社区现状。
三、语义检索系统架构:从“数据”到“结果”的全流程
前面聊了原理,现在看实际跑起来长什么样。整个系统就是一条流水线,左边是文档入库,右边是用户查询,中间全靠向量把两边串起来。
3.1 系统架构图(Mermaid)
graph TD
A[数据采集] --> B[数据预处理]
B --> C[语义嵌入生成]
C --> D[向量数据库存储]
E[用户查询] --> F[查询预处理]
F --> G[查询嵌入生成]
G --> H[向量数据库检索(粗排)]
H --> I[Cross-Encoder精排]
I --> J[结果返回]
看到这个图,脑子里要有个印象:数据准备和查询处理是两条并行的线,最后在向量检索那里汇合。粗排负责快,精排负责准。
3.2 各模块详细说明
3.2.1 数据采集
来源有几种:网页爬取,比如用 Scrapy 抓站点内容;数据库导出,MySQL 里存的老数据直接拉出来;还有文件上传,PDF 和 Word 文档丢进去。举个例子,你要采集 Wikipedia 上所有跟 Python 相关的文档,大概十万条,这量级用本地 Faiss 也能扛住。
3.2.2 数据预处理
采集到的原始数据不能直接用,得先过三关:
第一步,清洗。HTML 标签剥掉,特殊字符删掉,重复内容去重。爬下来的网页经常一堆导航栏、广告位,这些留着只会让向量变得更脏。
第二步,分词。中文用 jieba,比如“如何学习Python”切出来是“如何/学习/Python”。英文用 NLTK,“Learn Python”切成“Learn/Python”。分词结果直接影响后续嵌入质量,不过用像 Sentence-BERT 这类模型时,它内部自带 tokenizer,通常你只需要把文本喂进去就行,不一定非要单独分成词。
第三步,截断和分段。模型对输入长度有限制,比如 512 tokens。文档一长就得切。常见做法是滑动窗口,每段 512 tokens,相邻段重叠 100 tokens。这样能避免把一句话拦腰截断,也能让前后文信息不丢太多。
3.2.3 语义嵌入生成
用 Hugging Face 的 Transformers 加载预训练模型就行。代码长这样:
from sentence_transformers import SentenceTransformer
# 加载预训练模型(支持多语言)
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# 输入文本(文档列表)
documents = [
"Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。",
"Python的设计哲学强调代码的可读性和简洁性。",
"学习Python的最佳方式是动手做项目。"
]
# 生成嵌入向量(shape: [3, 768])
embeddings = model.encode(documents, convert_to_tensor=True)
# 打印第一个文档的嵌入向量(前5维)
print(embeddings[0][:5])
输出长这样:
tensor([-0.0321, 0.0456, -0.0123, 0.0789, -0.0234])
768 维的向量,每个维度值都代表文本在某个语义方向上的响应。人没法直接看懂,但计算机拿它算距离就很好使。
3.2.4 向量数据库存储
嵌入向量得存起来。数据量不大就用 Faiss,本地文件直接写盘。代码示例:
import faiss
import numpy as np
# 将Tensor转换为numpy数组(Faiss要求输入为float32)
embeddings_np = embeddings.cpu().numpy().astype('float32')
# 构建Faiss索引(IVF_FLAT,适合百万级数据)
index = faiss.IndexIVFFlat(
d=embeddings_np.shape[1], # 向量维度(768)
nlist=100, # 聚类中心数量(根据数据量调整)
metric=faiss.METRIC_COSINE # 相似性度量(余弦相似度)
)
# 训练索引(需要随机样本)
index.train(embeddings_np)
# 添加向量到索引
index.add(embeddings_np)
# 保存索引到本地
faiss.write_index(index, 'python_docs.index')
IVF_FLAT 这种索引是给百万级数据准备的。它先做聚类,检索时只在一个小范围内找,速度就上来了。nlist 设 100,意思是把数据先分成 100 个簇,具体数值要根据数据量调。
3.2.5 查询处理与检索
用户输入一句查询,系统走四步:
查询预处理,跟文档预处理一样,清洗、分词、截断。
查询嵌入生成,用同一个 Sentence-BERT 模型把查询转成向量。注意必须跟文档用的是同一套模型,不然向量空间都不一样,算相似度没意义。
粗检索,拿查询向量去 Faiss 索引里快速搜出 Top 1000 篇候选文档。这一步非常快,代价是精度不够。
精排序,用 Cross-Encoder 把查询和每篇候选文档拼在一起,算一个匹配分。这个模型比 Sentence-BERT 慢很多,但只在 1000 篇候选中跑,可以接受。最后按分数取 Top 10。
代码长这样:
from sentence_transformers import CrossEncoder
# 加载Cross-Encoder模型(用于精排)
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# 用户查询
query = "如何学习Python"
# 生成查询嵌入
query_embedding = model.encode(query, convert_to_tensor=True).cpu().numpy().astype('float32')
# 粗检索:从Faiss索引中找到Top 1000文档(参数nprobe=10,提高检索精度)
k = 1000
distances, indices = index.search(query_embedding.reshape(1, -1), k)
# 获取粗检索的文档(假设documents列表与索引顺序一致)
candidate_documents = [documents[i] for i in indices[0]]
# 精排序:用Cross-Encoder计算查询与候选文档的匹配得分
cross_encoder_input = [[query, doc] for doc in candidate_documents]
scores = cross_encoder.predict(cross_encoder_input)
# 按得分降序排序,取Top 10
top_10_indices = scores.argsort()[::-1][:10]
top_10_documents = [candidate_documents[i] for i in top_10_indices]
# 打印结果
print("Top 10相关文档:")
for i, doc in enumerate(top_10_documents):
print(f"{i+1}. {doc}")
3.2.6 结果返回
最后把结果打包成 JSON,带上文档内容、相似度得分和元数据。元数据可以放来源、时间、作者这些信息,方便前端展示和后续过滤。
{
"query": "如何学习Python",
"results": [
{
"document": "学习Python的最佳方式是动手做项目。",
"score": 0.98,
"metadata": {
"source": "Wikipedia",
"timestamp": "2024-05-01"
}
},
{
"document": "Python的设计哲学强调代码的可读性和简洁性。",
"score": 0.85,
"metadata": {
"source": "Wikipedia",
"timestamp": "2024-05-01"
}
}
]
}
四、项目实战:搭建一个智能知识库语义检索系统
理论说再多,不如动手搭一个。接下来全程实操,用一套完整的技术栈做个能跑的知识库系统。
4.1 项目目标
做一个基于语义检索的智能知识库,用户可以用自然语言提问,系统从产品手册、技术文档里找出最相关的内容返回。比如你放进去几份 Python SDK 文档,用户问“怎么用这个 SDK 发请求”,系统能直接把讲请求的段落捞出来。
4.2 技术栈
后端用 FastAPI,负责起 RESTful 接口。语义嵌入用 Sentence-BERT,之前已经见过,多语言支持。向量数据库用 Milvus,这里跟前面的 Faiss 不一样,因为 Milvus 支持分布式,后续数据量大了不用推倒重来。前端用 Streamlit,写个简单界面很快。数据就用 PDF 格式的产品手册,比如“Python SDK 使用指南”。
4.3 步骤1:环境搭建
先装依赖:
pip install fastapi uvicorn sentence-transformers pymilvus streamlit pdfplumber
Milvus 用 Docker 启动,直接把官方提供的 standalone 配置拉下来:
wget https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml
docker-compose up -d
等容器跑起来,Milvus 就在本地 19530 端口等着了。
4.4 步骤2:数据预处理(PDF转文本)
PDF 里面的文字得先提出来。用 pdfplumber 这个库,按页提取再拼起来:
import pdfplumber
def extract_text_from_pdf(pdf_path):
text = ""
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
text += page.extract_text() + "\n"
return text
# 提取产品手册内容
pdf_path = "python_sdk_guide.pdf"
document_text = extract_text_from_pdf(pdf_path)
# 分段(每段512 tokens)
from sentence_transformers import util
sentences = util.split_into_sentences(document_text, language='zh')
chunks = []
current_chunk = []
current_length = 0
max_length = 512
for sentence in sentences:
sentence_length = len(sentence.split())
if current_length + sentence_length > max_length:
chunks.append(" ".join(current_chunk))
current_chunk = [sentence]
current_length = sentence_length
else:
current_chunk.append(sentence)
current_length += sentence_length
if current_chunk:
chunks.append(" ".join(current_chunk))
print(f"共提取{len(chunks)}段文档")
这里先按句子拆,再攒句子拼成不超过 512 token 的块。split_into_sentences 这个工具函数能处理中英文的句号、问号、感叹号,挺方便。分块时注意别把一个完整的技术步骤切断,尽量让每块保持独立语义。
4.5 步骤3:生成嵌入并存入Milvus
PDF 变文本,文本分块,然后就是生成向量,存入 Milvus:
from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection
# 连接Milvus
connections.connect("default", host="localhost", port="19530")
连接之后还要建 collection,定义字段。一个 id 字段,一个向量字段,再加一个原始文本字段。字段类型要提前想好,之后不好改。向量维度取 768,对应 Sentence-BERT 的输出维度。建完 collection,把前面生成的 chunks 逐个变成向量插进去。插入时注意一次性批量插入,不要一条条 insert,速度差很多。
具体插入代码和后面的查询接口,下一部分接着写。到这里整个流程已经打通:文档进来,预处理,变成向量,存进 Milvus。接下来就是用户提问,走查询链路,从库里捞结果。
好,接着上一部分继续。前面我们把 Milvus 在本地跑起来了,也理解了集合、分区这些基本概念。现在得动真格的了,把这套东西接到实际的知识库流程里。
先说一个常见误区。很多人拿到 Milvus 第一步就去调索引参数,其实顺序搞反了。最稳妥的做法是先把数据灌进去,跑通一个最简单的检索,然后再回头优化索引。索引只是加速手段,它不改变召回结果的上限。所以下面的步骤也是按这个思路来的,先建集合,再插数据,最后才是建索引。
4.5 定义集合 Schema 并写入数据
Schema 这个东西,说白了就是告诉 Milvus 你存的数据长什么样。我们用四个字段:id 是主键,embedding 存向量,text 存原始文档内容,metadata 放一些结构化信息,比如文档来源、页码这些。
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=384),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=4096),
FieldSchema(name="metadata", dtype=DataType.JSON)
]
schema = CollectionSchema(fields=fields, description="产品手册知识库")
collection_name = "product_manuals"
collection = Collection(name=collection_name, schema=schema)
注意这里的 embedding 维度设成了 384。很多人会直接照抄 768 或者 1024,但维度必须跟你选的嵌入模型输出对齐。我们用 SentenceTransformer 的 paraphrase-multilingual-MiniLM-L12-v2,它输出的就是 384 维。这个模型对中文支持还行,关键是模型小,本地 CPU 也能跑。
生成向量的时候有个细节容易踩坑。model.encode() 默认返回的是 numpy 数组,但如果你设了 convert_to_tensor=True,拿到的就是 PyTorch 张量。Milvus 只认 float32 的 numpy 数组,所以得记得转一下:
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
embeddings = model.encode(chunks, convert_to_tensor=True).cpu().numpy().astype('float32')
astype('float32') 这步别省。有些模型默认输出 float64,直接塞给 Milvus 会报类型不匹配。
接下来是插入数据。这里有一个容易懵的点:collection.insert() 接收的是一个 list,list 里的每个元素对应一个字段的数据。注意是按字段顺序排列,不是按文档排列。
data = [
embeddings, # embedding字段
chunks, # text字段
[{"source": "python_sdk_guide.pdf", "page": i+1} for i in range(len(chunks))]
]
collection.insert(data)
数据插入之后,需要建索引。这一步可能让你觉得奇怪,刚才不是说最后才搞索引吗?那是因为查询之前必须得有索引。Milvus 不会自动帮你建,你得手动指定。
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "COSINE",
"params": {"nlist": 100}
}
collection.create_index(field_name="embedding", index_params=index_params)
collection.load()
nlist 设多少合适?这个参数的意思是索引构建时把向量空间分成多少个桶。一般建议是 4 * sqrt(n),n 是数据量。但如果你只有几百条测试数据,设 100 完全够用,设太大反而浪费。
最后的 load() 是把集合加载进内存。这一步很多人会忘,忘了的话查询直接报错。而且要注意,每次程序重启之后,都需要重新执行 load()。
4.6 步骤4:构建 FastAPI 接口
数据准备好了,接下来就是对外提供服务。用 FastAPI 封装一个检索接口,让前端或者其他服务通过 HTTP 调用。
from fastapi import FastAPI, Query
from pymilvus import Collection
app = FastAPI(title="智能知识库语义检索接口")
collection = Collection(name="product_manuals")
collection.load()
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
@app.get("/search")
async def search(query: str = Query(..., description="用户查询"), top_k: int = Query(10, ge=1, le=100)):
query_embedding = model.encode(query, convert_to_tensor=True).cpu().numpy().astype('float32')
search_params = {
"metric_type": "COSINE",
"params": {"nprobe": 10}
}
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param=search_params,
limit=top_k,
output_fields=["text", "metadata"]
)
search_results = []
for hit in results[0]:
search_results.append({
"text": hit.entity.get("text"),
"score": hit.score,
"metadata": hit.entity.get("metadata")
})
return {"query": query, "results": search_results}
这里有个细节值得说。nprobe 是查询时搜索的桶数量。它跟 nlist 是对应的,nprobe 越大,搜索越精确,但耗时也越高。一个经验值是从 nlist / 10 开始调,看效果再决定往哪个方向动。
查询结果的 score 是余弦相似度,范围在 -1 到 1 之间。实际使用的时候,你会发现一个现象:不管查什么,第一条的 score 都特别高。这大概率是数据量太少导致的。几百条数据、384 维向量,向量空间非常稀疏,最近邻的距离天然就大。这不是 bug,是维度灾难的表现。等数据量上来之后,score 的区分度会逐渐合理。
4.7 步骤5:构建 Streamlit 前端
接口有了,但总不能让人直接拿 curl 去敲吧。最后一步用 Streamlit 搭一个简单的可视化界面,让非技术的同事也能用。
import streamlit as st
import requests
st.title("智能知识库语义检索系统")
query = st.text_input("请输入查询")
top_k = st.slider("返回结果数量", min_value=1, max_value=10, value=5)
if st.button("检索"):
if not query:
st.warning("请输入查询内容")
else:
response = requests.get(
"http://localhost:8000/search",
params={"query": query, "top_k": top_k}
)
results = response.json()["results"]
for i, r in enumerate(results, 1):
st.markdown(f"**Top {i}**")
st.write(r["text"])
st.caption(f"score: {r['score']:.4f}")
st.divider()
Streamlit 的启动方式和 FastAPI 不太一样,不加 __name__ 判断,直接 streamlit run app.py 就行。如果你把 FastAPI 和 Streamlit 跑在同一台机器上,记得 FastAPI 的启动要加 --host 0.0.0.0,不然别人访问不到。Streamlit 默认绑定 localhost,要开放给局域网的话,启动时加 --server.address 0.0.0.0。
到这里,一个最小可用的语义检索系统就跑起来了。接下来用户输入问题,前端发请求给 FastAPI,FastAPI 把问题编码成向量去 Milvus 里找相似的内容,拿到结果返回给前端展示。链路的每一环都有独立的出错可能,建议先从 FastAPI 接口开始测,接口通了再调前端。还有最后一部分,聊一下数据量大了之后,这个架构会遇到什么问题,以及怎么应对。
检索通了,但结果还停留在后端返回的数据里,人没法直接看。接下来把结果渲染到页面上。Streamlit 写这种小页面很快,核心代码就这几行:
st.subheader("检索结果:")
for i, result in enumerate(results):
st.write(f"**{i+1}. 相似度:{result['score']:.2f}**")
st.write(f"内容:{result['text'][:200]}...") # 截断显示前200字
st.write(f"元数据:{result['metadata']}")
st.divider()
相似度分数保留两位小数,内容先截前 200 个字符再显示。元数据原样打出来,出问题的时候方便排查。
4.8 运行效果
后端和前端分开起。一个终端跑 uvicorn main:app --reload,另一个终端跑 streamlit run frontend.py。
浏览器打开 http://localhost:8501,输入“如何使用Python SDK上传文件?”,相关的文档片段就会列出来。
五、语义检索的实际应用场景
语义检索实际用起来的场景已经不少了,挑几个典型的说说。
5.1 智能客服:快速定位知识库
用户问“我的订单为什么还没发货?”,传统客服得人工去翻知识库里的“订单发货规则”。换成语义检索,系统直接匹配到“订单发货时间:付款后48小时内”这种文档,自动回复当场就能拼出来。
5.2 推荐系统:个性化内容推荐
用户刚看完“Python数据分析教程”,基于关键词的传统推荐大概率推“Python编程入门”。语义检索理解“数据分析”和“数据处理”之间的关联,推出来的是“Pandas数据处理实战”,明显更对路。
5.3 代码检索:根据自然语言找代码
想找“用Python实现快速排序”的代码,传统代码库得搜“快速排序 Python”才行。语义检索直接理解“实现快速排序”的意图,把 GitHub 上 star 最多的那个实现给你捞出来。
5.4 学术文献检索:精准找到研究论文
研究者想找“基于Transformer的语义检索”方向的论文,传统学术搜索引擎只能做关键词匹配。语义检索能抓住这句话的语义,返回 ACL、SIGIR 这些顶会里的相关工作。
六、语义检索的挑战与未来趋势
6.1 当前挑战
嵌入模型的泛化能力是第一个坎。Sentence-BERT 这类预训练模型在通用领域表现不错,进了专业领域就露馅。“心肌梗死”在通用语料里和“心脏病”算近义词,到了医疗场景,这两个词差远了。
数据量一上来,实时性就成了问题。亿级数据量下,向量数据库的检索延迟很可能超过 1 秒。推荐、客服这类场景,超过一秒用户就明显感觉卡了。
模态也是个限制。现在的语义检索主要处理文本,图像、语音都还没打通。用户说“找一张猫玩球的图片”,目前这套链路直接抓瞎。
隐私问题容易被忽略。用户查询本身可能带着个人信息,嵌入向量落到数据库里,等于把这些信息永久留了下来。联邦学习、同态加密这类技术还在早期阶段,离大规模使用还有距离。
6.2 未来趋势
针对专业领域的模型会越来越多。BioBERT、LegalBERT 这类模型就是先在通用语料上预训练,再用医疗、法律领域的语料继续训练,出来的嵌入比通用模型靠谱得多。
检索算法也在往快里走。HNSW 的优化版、GPU 加速的向量检索,目标都是把亿级数据上的延迟压到毫秒级。
CLIP 已经把图像和文本映射到同一个向量空间,多模态语义检索的基础算是有了。将来“拿文字找图片”“拿图片找文字”都会变成常规操作。
知识图谱融合解决的是歧义问题。“苹果”带上“水果”的语义关系,和“苹果手机”里的“苹果”是两个意思。把这类结构化知识揉进嵌入,语义理解的准确率还能再上一个台阶。
SimCSE、ELECTRA 这些自监督方法在句嵌入上的效果已经不错,相似性捕捉能力比传统训练方式更强。这个方向还在快速迭代。
七、工具与资源推荐
7.1 预训练模型库
Hugging Face Transformers 是第一站。Sentence-BERT、BERT、CLIP 都在里面,多语言支持做得也好。TensorFlow Hub 上有 LaBSE 这样的多语言句嵌入模型,也有老牌的 USE 通用句嵌入。
7.2 向量数据库
Faiss 适合本地小规模数据,检索速度基本是毫秒级。Milvus 适合分布式大规模数据,元数据过滤这类需求也能覆盖。Pinecone 走云原生路线,完全托管,省运维。
7.3 可视化工具
TensorBoard 配合 TSNE,能把 768 维的嵌入向量降维到 2 维,整个分布一眼看清。Weights & Biases 适合盯训练过程,损失函数和准确率的变化都记录得很清楚。
7.4 数据集
MS MARCO 是信息检索领域的大规模数据集,100 万条查询、10 亿条文档,训练检索模型基本都用得上它。Google 的 Natural Questions 有 8 万条真实用户查询,贴近实际场景。STS-B 规模小一些,5000 对句子带相似性标注,拿来做快速验证很方便。
7.5 书籍
李航的《深度学习自然语言处理》把 NLP 基础讲得比较透。想理解早期语义检索技术,Grigoris Antoniou 的《语义网基础》不能跳过,RDF、OWL 这些概念都覆盖到了。Milvus 团队写的《向量数据库实战》更偏实践,搭大规模系统时可以直接参照。
八、总结:语义检索——AI理解自然语言的关键一步
信息检索从关键词匹配走到语义理解,这一步跨出去,整个逻辑都不一样了。文本转成语义向量,向量数据库负责存储和召回,模型负责理解,这条链路已经能支撑实际业务。
还有不少硬骨头要啃,但方向很清楚:更快、更准、更多模态。对开发者来说,这套技术学下来不亏,知识库、推荐、客服,哪个方向都用得上。
最后用一句话收个尾。语义检索做了那么多,最终目的是让机器听懂人话。说得感性一点,懂你的心。
(全文完)
常见问题(FAQ)
语义检索和传统关键词搜索有什么区别?
传统检索靠关键词匹配,理解不了多义词和意图;语义检索将文本转为向量,按语义相似度匹配,能处理同义表达和上下文消歧,但成本更高、结果不易解释。
语义检索的核心原理是什么?
核心是将文本编码为语义向量,用余弦相似度等度量比较相似性,再通过向量数据库高效检索。常用模型有Sentence-BERT,也可用Bi-Encoder粗排加Cross-Encoder精排提升效果。
语义检索有哪些实际应用场景?
适用于需要理解用户意图的搜索,如搜索“苹果价格走势”能区分水果或股票,也用于问答系统、推荐系统等。相比传统搜索更智能,但需权衡成本和可解释性。
版权与免责声明:本文仅用于信息分享与交流,不构成任何形式的法律、投资、医疗或其他专业建议,也不构成对任何结果的承诺或保证。
文中提及的商标、品牌、Logo、产品名称及相关图片/素材,其权利归各自合法权利人所有。本站内容可能基于公开资料整理,亦可能使用 AI 辅助生成或润色;我们尽力确保准确与合规,但不保证完整性、时效性与适用性,请读者自行甄别并以官方信息为准。
若本文内容或素材涉嫌侵权、隐私不当或存在错误,请相关权利人/当事人联系本站,我们将及时核实并采取删除、修正或下架等处理措施。也请勿在评论或联系信息中提交身份证号、手机号、住址等个人敏感信息。



