语义搜索入门:什么时候使用,以及如何实现一套语义检索系统
一、什么是语义搜索?
在传统搜索系统中,我们搜索一段内容,通常依赖的是:
关键词匹配
例如文档中存在:
员工离职后,需要及时关闭内部系统账号和访问权限。
如果用户搜索:
员工 离职 账号
传统关键词搜索很容易找到这篇文档,因为:
员工
离职
账号
这些关键词真实存在于文档中。
但是现实中的用户并不一定知道文档原文是怎么写的。
例如用户可能搜索:
员工辞职之后公司账号怎么办?
此时用户使用的是:
辞职
公司账号
怎么办
而文档中使用的是:
离职
系统账号
关闭访问权限
从字面来看,两边并不完全相同。
但是从人的理解来看:
辞职
≈
离职
公司账号怎么办
≈
关闭内部系统账号和访问权限
它们表达的其实是同一个意思。
这就是语义搜索想解决的问题。
所谓:
Semantic Search
也就是:
不仅仅判断“有没有相同的词”,而是判断“用户的问题和文档表达的意思是否相似”。
可以简单记成:
关键词搜索
=
找相同的字
语义搜索
=
找相近的意思
二、为什么会出现语义搜索?
传统关键词检索其实非常强大。
像 Elasticsearch、OpenSearch 这一类搜索引擎,通过:
倒排索引
+
BM25
已经能够很好地解决大量搜索问题。
例如搜索:
Spring Boot
文档中出现:
Spring Boot 项目开发规范
自然可以匹配。
但是问题在于:
用户表达一个意思,并不一定使用文档中的原始词语。
例如知识库里面写:
用户无法正常登录系统时,可以通过账号中心执行密码重置。
用户却问:
忘记密码了怎么办?
关键词搜索可能重点寻找:
忘记
密码
怎么办
但是原文里面可能根本没有:
忘记密码
只有:
无法登录
密码重置
语义搜索则希望能够理解:
忘记密码怎么办
≈
密码重置
因此语义搜索特别适合处理:
用户不知道标准术语
用户不知道文档原文
用户使用自然语言提问
同一个意思存在很多不同表达方式
这也是为什么现在 AI 知识库、RAG 系统越来越依赖语义搜索。
三、什么时候应该使用语义搜索?
语义搜索并不是所有搜索场景都适合。
判断要不要使用语义搜索,可以先问一个问题:
用户搜索时,是在寻找“某个准确的词”,还是在表达“某个意思”?
如果用户主要是在表达意思,那么语义搜索通常非常适合。
比如企业知识库。
文档:
员工工作满一年后,可以享受五天带薪年假。
用户可能问:
一年可以休几天年假?
也可能问:
公司的带薪假有多少天?
还可能问:
工作一年以后能休多少天?
这三个问题的字面完全不同。
但实际上都在问:
年假天数
这种场景就非常适合语义搜索。
再比如客服知识库。
知识库写:
订单付款完成后,如果商家尚未发货,可以在订单详情页申请退款。
用户问:
东西还没寄出来,我能退钱吗?
关键词几乎没有完全对应。
但是语义非常接近。
所以:
知识库问答
客服问答
企业文档搜索
技术文档搜索
合同搜索
论文搜索
FAQ搜索
RAG
通常都非常适合语义搜索。
四、什么时候不要只使用语义搜索?
这是实际做项目时非常重要的一点。
不要产生一个错误认知:
语义搜索比关键词搜索高级
所以:
语义搜索 > 关键词搜索
实际上不是。
例如用户搜索:
ERR_10086
这时候他可能就是想找到:
ERR_10086
而不是寻找:
和 ERR_10086 意思相似的内容
再例如:
订单号:
202609100001
用户搜索:
202609100001
最重要的是:
精确匹配
再例如搜索:
NullPointerException
或者:
Spring Boot 3.5.2
或者:
GB/T 22239-2019
这些:
错误码
订单号
产品型号
版本号
标准编号
类名
方法名
专有名词
关键词搜索往往更可靠。
所以可以这样理解:
| 搜索内容 | 更适合 |
|---|---|
| 订单号 | 关键词搜索 |
| ERR_10086 | 关键词搜索 |
| NullPointerException | 关键词搜索 |
| 产品型号 | 关键词搜索 |
| “忘记密码怎么办” | 语义搜索 |
| “员工辞职后账号怎么办” | 语义搜索 |
| “公司一年可以休多少天假” | 语义搜索 |
| 知识库自然语言问答 | 语义搜索 |
而真正成熟的系统,通常不是二选一。
而是:
关键词搜索
+
语义搜索
这叫:
Hybrid Search
也就是:
混合检索
这一点后面再详细讲。
五、语义搜索到底是怎么实现的?
语义搜索最核心的技术只有两个概念:
Embedding
+
Vector Search
也就是:
文本向量化
+
向量相似度搜索
整个过程可以先记成:
文本
↓
Embedding Model
↓
Vector
↓
Vector Database
↓
相似度搜索
这里最重要的是:
Embedding
六、什么是 Embedding?
Embedding 可以简单理解为:
把一段文字转换成一组能够表示其语义特征的数字。
例如:
员工离职后需要关闭账号
经过 Embedding 模型:
员工离职后需要关闭账号
↓
Embedding Model
↓
[0.12, -0.53, 0.88, 0.21, ...]
这串数字:
[0.12, -0.53, 0.88, 0.21, ...]
就叫:
Vector
中文叫:
向量
真实情况下可能不是四个数字,而是几百、上千甚至更多维。
例如:
[0.121,
-0.532,
0.882,
0.217,
...
]
重点并不是记住这些数字。
重点在于:
意思越接近的文本,在向量空间中的位置通常越接近。
例如:
员工离职以后需要关闭账号
可能转换成:
Vector A
而:
员工辞职以后公司账号怎么办?
转换成:
Vector B
因为两个句子的语义接近:
Vector A
≈
Vector B
于是系统就可以通过比较两个向量的距离,判断:
这两句话表达的意思非常相似
七、向量相似度是什么意思?
假设知识库里有三段内容:
A:
员工离职后需要关闭账号。
B:
员工每年可以享受五天带薪年假。
C:
公司服务器每天凌晨进行数据库备份。
分别生成:
A → Vector A
B → Vector B
C → Vector C
用户搜索:
辞职后系统权限怎么办?
同样进行:
用户问题
↓
Embedding
↓
Query Vector
然后计算:
Query Vector
和
Vector A
有多相似?
再计算:
Query Vector
和
Vector B
有多相似?
以及:
Query Vector
和
Vector C
有多相似?
最终可能得到:
A 0.94
B 0.55
C 0.21
那么系统自然认为:
A
最符合用户的问题。
这就是:
Vector Similarity Search
也就是:
向量相似度检索
八、常见的相似度计算方式
向量检索中经常会看到:
Cosine Similarity
Dot Product
Euclidean Distance
刚入门的时候不用急着研究复杂数学。
其中非常常见的是:
Cosine Similarity
也就是:
余弦相似度
它本质上是在比较:
两个向量的方向有多接近
可以简单理解:
意思越接近
↓
Vector方向越接近
↓
相似度越高
例如:
员工辞职后账号怎么办?
员工离职后关闭系统访问权限。
相似度:
0.93
而:
员工辞职后账号怎么办?
服务器每天凌晨两点备份数据库。
相似度可能:
0.17
于是就能把真正相关的文档排到前面。
九、为什么需要向量数据库?
假设你的知识库里面只有:
10条数据
实际上根本不需要专门的向量数据库。
完全可以:
Query Vector
↓
依次和10个Vector比较
↓
排序
但是如果变成:
100万 Chunk
如果每次用户搜索都:
Query Vector
和
100万个 Vector
全部计算一次
性能就会出现问题。
于是需要:
Vector Index
也就是:
向量索引
通过专门的数据结构快速找到:
距离 Query Vector 最近的一批向量
于是出现了:
Vector Database
例如:
Milvus
Qdrant
Weaviate
Pinecone
但需要特别注意:
做语义搜索,不一定必须拥有一个独立的向量数据库。
比如:
Elasticsearch
本身就支持向量搜索。
PostgreSQL 可以通过:
pgvector
支持。
所以:
语义搜索
≠
必须使用 Milvus
更加准确应该是:
语义搜索
=
Embedding
+
Vector
+
Vector Index/Search
至于 Vector 放在哪里,是架构选择。
十、完整的语义搜索系统分成两个阶段
真正做语义搜索,一定要理解:
知识入库
和:
用户检索
是两个不同阶段。
十一、第一阶段:知识入库
假设用户上传:
员工管理制度.pdf
第一步一般不是直接 Embedding。
而是:
PDF
↓
Apache Tika
↓
提取文本
例如:
第一章 入职管理……
第二章 请假管理……
员工每年可以享受五天带薪年假……
第三章 离职管理……
员工离职后应及时关闭所有内部系统访问权限……
然后进入:
Chunk
也就是:
文本切片
十二、为什么必须进行 Chunk?
假设一本 PDF:
300页
你如果直接:
整本PDF
↓
Embedding
↓
一个Vector
那么这个 Vector 就要同时表达:
入职
请假
薪资
离职
报销
考勤
绩效
信息安全
大量不同主题。
这样语义就会变得非常模糊。
所以一般需要:
Document
↓
Chunk
例如:
Chunk 1
员工入职流程……
Chunk 2
员工每年享受五天带薪年假……
Chunk 3
员工离职以后需要关闭系统账号……
每个 Chunk 单独:
Chunk
↓
Embedding
↓
Vector
这样:
一个 Vector
对应一个比较明确的语义单元。
十三、Chunk 应该多大?
这是 RAG 和语义搜索里面非常重要的问题。
假设 Chunk 太小:
员工离职。
上下文可能不够。
你不知道:
离职以后到底怎么处理?
如果 Chunk 太大:
整整20页员工制度
又会包含太多不同语义。
所以 Chunk 本质上是在寻找:
上下文完整性
和:
语义纯度
之间的平衡。
实际项目中经常会根据:
Token
字符数
段落
标题
Markdown结构
进行切分。
例如:
标题:
3.2 离职管理
正文:
员工提出离职以后,应由部门负责人确认,
同时通知管理员关闭OA、邮箱、VPN等系统权限。
最好整个作为:
一个 Chunk
而不要机械切成:
员工提出离职以后,应由部门
和:
负责人确认,同时通知管理员……
否则语义会被破坏。
所以好的 Chunking 往往应该:
优先保持自然语义边界
而不仅仅是:
每500个字符一刀切
十四、知识入库完整流程
到这里整个入库链路就很清晰了:
PDF / Word
↓
Apache Tika
↓
Text
↓
Cleaning
↓
Chunking
↓
┌─────────┴─────────┐
↓ ↓
Chunk Text Metadata
↓
Embedding
↓
Vector
↓
Vector Search Engine
其中一条数据最终可能保存成:
{
"id": "chunk_10001",
"documentId": "doc_1001",
"title": "员工管理制度",
"chapter": "离职管理",
"content": "员工离职以后,应及时关闭内部系统访问权限。",
"page": 18,
"vector": [
0.12,
-0.53,
0.88,
0.21
]
}
注意这里并不是只保存 Vector。
还应该保留:
原始文本
+
Vector
+
Metadata
因为最后给用户看的仍然是:
原始文本
而不是:
[0.12, -0.53, 0.88 ...]
十五、第二阶段:用户进行语义搜索
假设用户输入:
辞职以后公司的系统账号怎么办?
第一步:
Query
↓
Embedding
↓
Query Vector
注意:
查询时通常应该使用和知识入库时兼容的 Embedding 模型。
如果知识库使用:
Embedding Model A
生成 Vector。
查询突然换:
Embedding Model B
两套向量空间未必兼容。
这样搜索结果就可能出现严重问题。
因此:
Document Embedding
和:
Query Embedding
通常需要保持模型一致。
十六、什么是 TopK?
用户问题生成 Query Vector 后,需要去向量库搜索。
例如:
Search TopK = 5
意思就是:
找和当前问题最相似的前 5 个 Chunk。
可能得到:
Top 1
员工离职后应关闭内部系统账号。
similarity = 0.94
Top 2
员工离职需要归还电脑及门禁卡。
similarity = 0.87
Top 3
管理员负责企业系统账号权限维护。
similarity = 0.81
Top 4
新员工入职需要创建系统账号。
similarity = 0.72
Top 5
员工账号权限每季度需要审计。
similarity = 0.68
这就是:
TopK Retrieval
因此你以后看到:
TopK = 5
TopK = 10
TopK = 20
不要觉得神秘。
就是:
先召回前几个最相关结果
十七、Metadata Filter 为什么非常重要?
假设你的知识库有:
1000家公司
用户 A 属于:
Company 100
用户搜索:
年假多少天?
如果纯粹做:
Vector Search
很可能搜到:
Company 200
的员工制度。
语义完全正确。
但是:
业务上完全错误。
所以语义搜索不能只关注:
Vector Similarity
还必须结合:
Metadata Filter
例如:
companyId = 100
于是查询实际上应该变成:
WHERE companyId = 100
AND
Vector Similarity Search
再比如知识库权限:
departmentId
userId
knowledgeBaseId
documentType
language
createdAt
都可能需要参与过滤。
所以真正的语义检索应该理解成:
业务过滤
+
向量相似度检索
而不是单纯:
找最近的 Vector
十八、为什么语义搜索通常要配合关键词搜索?
来看一个问题:
Spring Boot 3.5 Redis ERR_10086 怎么解决?
这里有两种信息。
第一种:
Spring Boot 3.5
Redis
ERR_10086
这些是:
精确关键词
尤其:
ERR_10086
非常适合 BM25。
而:
怎么解决
属于:
用户意图
语义搜索更擅长。
所以最合理的是:
Query
↓
┌─────────┴─────────┐
↓ ↓
Keyword Search Vector Search
↓ ↓
BM25 Semantic Score
│ │
└─────────┬─────────┘
↓
Merge / Fusion
↓
Ranking
↓
TopK
这个就叫:
Hybrid Search
也就是:
混合检索
现在做比较成熟的知识库系统,一般都应该认真考虑 Hybrid Search。
十九、Hybrid Search 为什么通常比纯 Vector 更稳定?
假设用户搜索:
Redis ERR_10086
语义搜索可能找到:
Redis连接失败问题排查
意思确实非常相关。
但是还有一个文档:
ERR_10086 Redis Connection Timeout
显然第二个更应该优先。
关键词搜索能很好捕获:
ERR_10086
语义搜索又能够捕获:
connection timeout
≈
连接失败
两个组合起来,就会更加稳定。
所以可以简单理解:
BM25
负责:
“词准不准”
Vector Search
负责:
“意思准不准”
Hybrid Search
负责:
“词和意思都尽量准”
二十、语义搜索之后为什么还会有 Rerank?
这是再往生产系统走一步非常重要的知识。
假设 Vector Search:
TopK = 20
召回:
20个 Chunk
向量检索本身非常擅长:
快速从大量内容里面召回相关结果
但第一名和第二名到底谁更准确,不一定永远排序完美。
因此可以增加:
Reranker
也叫:
重排序模型
流程:
100万 Chunk
↓
Vector Search
↓
Top 20
↓
Reranker
↓
重新判断 Query 与每个 Chunk 的相关性
↓
Top 5
所以:
Vector Search
更像:
快速海选
而:
Reranker
更像:
精细复审
最后送给 LLM 的可能只有:
Top 5
二十一、完整的生产级语义搜索架构
现在可以把所有东西串起来:
【知识入库】
PDF / Word / PPT / HTML
↓
Apache Tika
↓
Text
↓
Cleaning
↓
Chunking
↓
Chunk
↓
Embedding
↓
Vector
↓
Vector Search Engine
【用户查询】
用户自然语言 Query
↓
Embedding
↓
Query Vector
↓
┌───────┴────────┐
↓ ↓
Keyword Search Vector Search
↓ ↓
BM25 Semantic TopK
└───────┬────────┘
↓
Fusion
↓
Metadata Filter
↓
Rerank
↓
TopK
↓
返回搜索结果
如果这是 RAG 系统:
TopK
↓
LLM Prompt
↓
Large Language Model
↓
回答用户问题
于是完整链路变成:
Document
↓
Tika
↓
Chunk
↓
Embedding
↓
Vector Store
↓
Semantic Search
↓
Hybrid Search
↓
Rerank
↓
LLM
↓
Answer
这基本就是一个现代知识库系统的核心主干。
二十二、一个最小语义搜索 Demo 应该怎么做?
假设我们只有三个知识:
Document 1
员工每年可以享受五天带薪年假。
Document 2
员工离职以后需要关闭内部系统账号。
Document 3
公司数据库每天凌晨两点进行备份。
第一步:
Embedding(document1)
→ vector1
Embedding(document2)
→ vector2
Embedding(document3)
→ vector3
保存:
document1 + vector1
document2 + vector2
document3 + vector3
用户:
辞职之后系统权限怎么办?
执行:
Embedding(query)
→ queryVector
然后:
similarity(queryVector, vector1)
similarity(queryVector, vector2)
similarity(queryVector, vector3)
假设:
vector1 = 0.41
vector2 = 0.94
vector3 = 0.12
最终返回:
员工离职以后需要关闭内部系统账号。
恭喜。
这实际上已经完成了一套最简单的:
Semantic Search
系统。
Vector Database 只是把:
存储
索引
查询
排序
这几个步骤工业化了。
二十三、语义搜索最容易踩的几个坑
第一个坑:
整个 PDF 生成一个 Vector
通常不好。
应该:
Document
↓
Chunk
↓
Vector
第二个坑:
Chunk 切得太碎
导致上下文缺失。
第三个坑:
Chunk 切得太大
导致一个 Vector 包含太多主题。
第四个坑:
只相信 similarity score
实际上还必须考虑:
权限
租户
知识库
文档类型
时间
标签
等 Metadata。
第五个坑:
只做 Vector Search
对于:
编号
错误码
版本号
人名
产品名
专有名词
效果可能不如:
BM25
因此正式项目最好考虑:
Hybrid Search
第六个坑:
Embedding 模型换了
但是历史 Vector 没有重新生成。
这时候:
新Query Vector
和:
旧Document Vector
可能处于不同向量空间。
结果就会异常。
因此更换 Embedding 模型时通常要考虑:
重新 Embedding
二十四、做项目时推荐怎样渐进实现?
第一版不要一上来就设计:
Tika
+
Kafka
+
Milvus
+
Elasticsearch
+
Reranker
+
LLM
+
十几个微服务
这样反而很容易把自己绕进去。
最开始可以:
Document
↓
Chunk
↓
Embedding
↓
Vector Store
↓
TopK Search
先把:
纯语义搜索
跑通。
然后增加:
Metadata Filter
再增加:
Keyword Search
形成:
Hybrid Search
再增加:
Reranker
最后如果需要 AI 问答:
Retrieval
↓
LLM
这样逐步演化会比较清晰。
二十五、最终总结
理解语义搜索,其实只需要抓住一条主线:
文本
↓
Embedding
↓
Vector
↓
Vector Similarity Search
↓
找到意思最接近的文本
传统关键词检索关注:
“有没有这个词?”
而语义检索关注:
“是不是这个意思?”
例如:
文档:
员工离职后应关闭所有内部系统访问权限。
用户:
辞职以后公司的账号怎么办?
两句话字面不同。
但是:
Embedding
↓
Vector
↓
Similarity
↓
高度相似
于是语义搜索能够把它找出来。
如果进一步放到真正的知识库系统里:
用户上传文件
↓
Apache Tika
↓
Text
↓
Chunk
↓
Embedding
↓
Vector
↓
Vector Database
↓
Semantic Search
↓
Hybrid Search
↓
Rerank
↓
TopK
↓
LLM
↓
Answer
你可以把整个体系最终压缩成几个概念:
Tika
负责:
文件 → 文本
Chunk
负责:
长文本 → 小语义单元
Embedding
负责:
文本 → Vector
Vector Search
负责:
找意思相似的 Chunk
BM25
负责:
找关键词匹配的 Chunk
Hybrid Search
负责:
关键词 + 语义一起搜索
Rerank
负责:
对召回结果进行更精细排序
LLM
负责:
根据检索结果组织最终答案
所以语义搜索真正适合的场景,不是“我知道我要搜索哪个准确字符串”,而是:
我知道自己想找什么意思,但我不知道文档里究竟用了什么词。
这就是语义搜索最核心的价值。