Appearance
2.2 向量与语义搜索
传统搜索 vs 语义搜索
传统关键词搜索: 在文本里找有没有这个词。
搜索"退款" → 只找包含"退款"这个字的文档语义搜索(Semantic Search): 理解意思,找相关内容。
搜索"退款" → 能找到包含"退钱""申请退回""refund"的文档
即使这些文档里没有"退款"两个字这就是为什么 RAG 用向量搜索而不是关键词搜索:用户的问题和文档里的描述,往往用词不同但意思相近。
Chunking(分块):RAG 里最容易踩的坑
做向量搜索前,长文档要先切成小段(Chunk)再分别 Embedding。切得好不好,直接决定检索质量——很多 RAG 效果差,根子在分块,不在模型。
为什么不能整篇文档塞进去:
- 一篇文档讲很多主题,整篇的向量是"平均味道",对具体问题不精准
- 检索出来要塞进 Prompt,太大既贵又稀释重点
几种分块策略:
| 策略 | 做法 | 适合 |
|---|---|---|
| 定长切分 | 每 N 个字符/Token 切一段 | 最简单,通用兜底 |
| 按结构切分 | 按标题、段落、Markdown 层级切 | 文档结构清晰时(最推荐) |
| 语义切分 | 按语义边界切(一个完整意思一段) | 质量最高,但实现复杂 |
两个关键参数:
Chunk Size(块大小):每段多大。太大不精准,太小丢上下文。
常见 300-800 字,按你的内容调。
Overlap(重叠):相邻块重叠一部分(比如 10-15%),
避免一句话被从中间切断、两边都丢失语境。javascript
// 最简定长 + 重叠分块
function chunkText(text, size = 500, overlap = 80) {
const chunks = []
for (let i = 0; i < text.length; i += size - overlap) {
chunks.push(text.slice(i, i + size))
}
return chunks
}⚠️ 常见误解:以为换个更强的 Embedding 模型就能救烂检索。多数时候,先把分块和文档清洗做好,收益比换模型大得多。
⚠️ 维度一致性:同一个向量库里的所有向量,必须用同一个 Embedding 模型生成(不同模型维度和语义空间都不同,不能混用)。换 Embedding 模型 = 整库重建。详见 1.9 OpenAI 兼容协议。
向量数据库
向量数据库专门用来存储和搜索向量。它能在几百万个向量里,快速找到和查询向量最相近的那些。
主流选择:
| 数据库 | 特点 | 适合场景 |
|---|---|---|
| Pinecone | 全托管、简单 | 快速上手,不想管基础设施 |
| Qdrant | 开源、高性能 | 自托管,功能完整 |
| Weaviate | 开源、支持混合搜索 | 需要结合关键词和语义 |
| pgvector | PostgreSQL 插件 | 已经在用 PostgreSQL 的项目 |
| Chroma | 轻量、本地运行 | 开发测试阶段 |
💡 给你的建议:项目初期用 Chroma(本地,零成本),上线后迁到 Pinecone 或 Qdrant。
Reranking(重排序)是什么
向量搜索找到的结果,相关性是按数学距离排的,但"数学上相似"不等于"对这个问题最有用"。
Reranking 是对初步检索结果做二次排序,用更精确的模型重新判断相关性。
向量搜索返回 20 个结果(速度快,精度一般)
↓
Reranker 对这 20 个结果打分(速度慢,精度高)
↓
取最高分的 5 个给 AI 参考什么时候需要 Reranking:
- 搜索结果质量不稳定
- 文档量很大,初步检索噪音多
- 对准确性要求高的场景
Hybrid Search(混合搜索)
结合关键词搜索(传统搜索引擎的方式,业内叫 BM25——这个名字你不需要记,知道它是"关键词搜索"的技术叫法就够了)和语义搜索,取两者的优势。
用户问:"2024年第三季度的营收是多少"
关键词搜索:能精确找到包含"2024"、"第三季度"、"营收"的文档
语义搜索:能找到"24年Q3收入情况"这类表述不同但意思相同的内容
混合搜索:两者结合,既精确又全面Index(索引)是什么
在数据库里,索引是为了加速查找建立的数据结构。在向量数据库里也是类似的概念——把向量按特定方式组织,让搜索更快。
你通常不需要手动管理索引,向量数据库会自动处理,但知道这个词是什么意思,看文档时不会懵。
🛠️ 实战练习:分块大小对检索的影响
拿一篇你熟悉的长文档(比如某个库的 README),用不同的 size 切分,做同一个查询,对比检索效果:
javascript
// 复用 2.1 节的 embedTexts() 和 cosineSimilarity()
const longDoc = readFileSync("./some-doc.md", "utf-8")
const question = "怎么安装这个库?"
for (const size of [200, 500, 1200]) {
const chunks = chunkText(longDoc, size, Math.floor(size * 0.15))
const chunkEmbeddings = await embedTexts(chunks)
const [qEmb] = await embedTexts([question])
const best = chunks
.map((c, i) => ({ c, score: cosineSimilarity(qEmb, chunkEmbeddings[i]) }))
.sort((a, b) => b.score - a.score)[0]
console.log(`\n块大小 ${size} → 最相关片段:\n`, best.c.slice(0, 120))
}观察要点:
- 块太小(200):是不是检索到的片段太零碎、缺上下文?
- 块太大(1200):是不是检索到一大段、里面真正相关的内容被稀释了?
- 哪个大小对这篇文档、这个问题最合适?
进阶挑战:给 chunkText 加上"按 Markdown 标题切分",对比定长切分和按结构切分的检索质量差异。
📌 关键结论
- 语义搜索理解意思而不是匹配词语,这是 RAG 的核心优势
- 分块(Chunking)质量决定 RAG 效果,先调好分块再考虑换模型
- 同一向量库必须用同一个 Embedding 模型,换模型要整库重建
- 开发阶段用 Chroma,生产环境根据需求选向量数据库
- 搜索质量不好时,加 Reranking 通常能显著提升
- Hybrid Search 结合了关键词和语义,适合需要精确匹配的场景
下一节:2.3 Agent 设计模式