向量数据库,简单说就是专门用来存「向量」、并能极快地找出「最相似向量」的数据库。要理解它,得先接上 embedding 这个概念——如果对「文本怎么变成向量」还不熟,可以先看 从 Prompt 到输出 和 多模态 Token。
一、从 embedding 说起:为什么需要它
我们已经知道:文本、图像、音频经过模型都会变成向量(embedding),而且有个关键性质——语义相近的东西,向量在空间里也相近。「猫」和「猫咪」的向量挨得近,和「股票」的向量离得远。
那问题来了:我有一百万段文档、每段都存成了向量。用户问一句话(也变成一个向量),我想找出语义最接近的那几段——
- 传统数据库只会精确匹配 / 关键词匹配:
WHERE title = 'xxx'、LIKE '%猫%'。它不懂「意思相近」。 - 我要的是按「向量距离」找最近的,而不是按字段相等找。
这就是向量数据库要干的事:最近邻搜索(Nearest Neighbor Search)——给一个查询向量,返回库里离它最近的 top-k 个。
1 | 查询向量 q ──► 在百万向量里找离 q 最近的 5 个 ──► 返回对应的原文 |
二、为什么要「专门的」数据库:暴力算太慢
最朴素的办法:拿 q 和库里每个向量都算一遍距离,排序取前 k。但向量动辄几百上千维、数量上百万——每次查询都全量比一遍,太慢了。
所以向量数据库的核心本事是 ANN(Approximate Nearest Neighbor,近似最近邻)索引:用一点点精度换巨大的速度,不用比全部就能快速锁定「大概率最近的那些」。常见索引:
- HNSW:把向量连成一张分层的图,查询时沿着图「跳着找」,又快又准,最主流。
- IVF:先把向量聚成很多簇,查询时只在最近的几个簇里找。
- PQ(乘积量化):把向量压缩,省内存、加速比较。
关键词是「近似」:它不保证 100% 找到绝对最近的那个,但能在毫秒级返回「足够近」的结果,这在实际应用里完全够用。
三、它实际存什么
一条记录(很多库叫 point / entry)通常是四样东西打包在一起,外加数据库自己维护的索引结构:
1 | { |
- id:唯一标识,用来更新 / 删除 / 去重。RAG 里常编码成「文档号 + 第几块」(如
doc42_chunk7),方便定位和重建。 - vector:真正用来检索的 embedding,一个定长的浮点数组,维度由模型决定(768 / 1024 / 1536…)。同一集合里所有向量维度必须一致,距离度量(余弦 / 点积 / 欧氏)建库时就定好。相似度搜索只跑在这个字段上,其他字段都不参与找最近邻。
- 原文 text:被切出来的那段原始文本。存它是为了命中后直接把原文返回塞进 prompt,不用拿 id 再查一遍。(大数据 / 合规场景也可只存 id 引用,原文放对象存储。)
- metadata:任意结构化附加字段,干三件事——过滤(做「语义相似 + 条件」的混合查询,如
tag = 'HR'范围内找最相似)、溯源(标注「来自手册第 12 页」)、重建上下文(doc_id+chunk_index把相邻块拼回去)。为了让过滤也快,这些字段通常单独建索引。
除了记录,数据库还存一块看不见但关键的派生数据——ANN 索引结构(HNSW 的图、IVF 的簇中心、PQ 的码本),插入时自动构建并持久化,往往占用不小的内存 / 磁盘。
记录按集合(Collection)组织(类似关系库的「表」),一个集合统一约定向量维度和距离度量。进阶场景里,一条记录还可能挂多个向量(稠密 + 稀疏做混合检索,或图文多模态向量)。
四、最主要的用途:RAG(检索增强生成)
这才是向量数据库这几年爆火的原因。大模型有两个硬伤:知识会过时、答不了你的私有数据,而且容易「一本正经地编」。RAG(Retrieval-Augmented Generation) 用向量库给它外挂一个可检索的知识库:
1 | 【离线建库】 |
这样模型是「带着查到的资料回答」,而不是凭记忆硬编。它的好处:
- 知识可更新:换文档、加文档即可,不用重新训练模型。
- 答案有出处:能告诉你这段回答来自哪份文档,便于核查。
- 省上下文:不用把整个知识库塞进 prompt,只检索最相关的几段。
Agent 的「长期记忆」、企业知识库问答,底层很多就是这套。
五、有哪些选择
- 专用向量库:Pinecone、Weaviate、Milvus、Qdrant、Chroma。
- 给现有数据库加向量能力:
pgvector(给 PostgreSQL 加)、Redis、Elasticsearch 等。
小项目用
pgvector往往就够了,数据量和并发上来了再考虑专用向量库——不要一上来就堆重型组件。
六、进阶:HNSW 索引内部是怎么运转的
第二节说 HNSW「把向量连成分层的图、跳着找」。这里把它拆开——这也是面试最常被追问的点。
1. 核心思想:小世界图 + 分层
- 小世界图(NSW):每个向量是图上一个节点,和相近的若干节点连边。检索用贪心路由——从某个点出发,每一步都跳到离目标更近的邻居,直到邻居里没有更近的。因为图像社交网络一样「任意两点只隔几跳」,不用比全部就能逼近目标。
- 分层(Hierarchical):在 NSW 上叠几层「高速公路」,很像跳表。顶层稀疏、跨度大,负责大步逼近目标区域;越往下越密,负责精细定位;底层(Layer 0)包含全部节点。
2. 搜索是怎么走的(看动画)
下面这个动画自动演示了整个过程:★ 是查询目标,盯着高亮的小球看它怎么从顶层大跨度逼近、逐层下降、最后在底层锁定最近邻。
如果动画加载不出来,可直接打开:HNSW 搜索动画
一句话概括这条路径:顶层用很少的跳数冲到目标区域 → 逐层下降、越来越精细 → 底层(含全部节点)做最后精搜,返回 top-k。
3. 每个节点的「层」是怎么定的:随机抽,不是算
这点很反直觉但很关键:一个节点能上到第几层,和它的向量内容、位置完全无关,是插入时按概率公式随机抽的。
1 | level = floor( -ln(random(0,1)) × mL ) // mL = 1 / ln(M) |
它产生一个指数衰减的分布:
- 绝大多数节点抽到 level 0(只待在底层);越高层节点呈指数级骤减。以 M=16 为例(
mL≈0.36),约 94% 的节点只在 Layer 0,到 Layer 1 的约 6%,再上去零点几个百分点——自然堆成金字塔。 - 一个节点出现在「从 0 到它 level」的所有层:抽到 level 2 就同时在 Layer 0、1、2。所以越往下层节点越全,Layer 0 永远包含全部节点。
最贴切的类比是跳表:像不停掷硬币,连续正面才升一级——少数「运气好」的节点被随机提拔成高层的「高速公路」。用指数衰减是为了让每高一层节点约为下层的 1/M,层数期望约 log_M(N),从而把搜索压到对数级。
所以上面动画里
5(到顶层)、13(只到中层)、2(只在底层),对应的就是它们各自被随机抽到的 level——谁在高层是运气,不是因为它「更重要」。
4. 三个调优旋钮
| 参数 | 含义 | 调大的影响 |
|---|---|---|
| M | 每个节点保留多少条边 | 召回更高更准,但更占内存 |
| ef_construction | 建图时的候选集大小 | 图质量更好,但建库更慢 |
| ef_search | 查询时的候选集大小 | 召回更高但查询更慢——速度 / 精度的主旋钮 |
HNSW 的代价是吃内存(图 + 向量常驻)和删除麻烦(多用标记删除 + 定期重建),但凭又快又准,成了向量检索的事实标准。
七、进阶:文档怎么切块(Chunking)
第四节说 RAG 的第一步是「文档 → 切成小块(chunk)」。这一步看似简单,却直接决定检索的天花板——切得太碎,答案凑不齐;切得太大,embedding 被稀释、检索不准。值得单独讲透。
1. 最常用:递归字符切分(Recursive Splitting)
「递归」的意思是:先在最大的边界(段落)切,块还太大就降一级用更小的边界(句子、逗号)再切,一级级往下,直到每块都不超过设定大小——在「自然断句」和「控制大小」之间找平衡。它是 LangChain 等框架的默认做法。
两个核心参数:
1 | chunk_size : 每块多大(常用 256~1024 token) |
为什么要重叠(overlap):如果一个完整意思正好被切在两块边界上,任一块都不完整,检索时两边都可能匹配不上。让相邻块头尾重叠一部分,保证跨边界信息不丢。
2. 用一个段落走一遍
设 chunk_size = 40 字(演示用,设小),分隔符优先级 段落 → 换行 → 句号 → 逗号 → 单字。原文一段:
1 | 员工每年享有15天带薪年假。入职满3年后,年假增加至20天。年假需提前3天申请,经主管批准后生效。未休年假可结转至次年,但最多保留5天。 |
递归过程:
1 | 第1级 按段落\n\n 切 → 没有空行,整块 66 字 > 40 → 降级 |
切出 4 个句子后,贪心合并回去,只要不超过 40 字就继续拼:
1 | 块1 = 句① + 句② = 28 字 ≤ 40 ✓ (再加句③→47 超了,收口) |
最终切成 2 块,每块都是完整句子,绝不从句子中间切断:
1 | 块1:员工每年享有15天带薪年假。入职满3年后,年假增加至20天。 |
对比朴素的「固定长度」直接数到第 40 字一刀切,会把 「20天」 劈成 2 和 0天,语义被破坏、这块的 embedding 就废了。递归切分优先在自然边界断,避免了这个问题。
3. 一句话超过 chunk_size 时怎么办
很多人卡在这:合并后超了会不会硬截断?分三种情况,只有最后一种才真的硬切:
- A. 两句合并会超 → 根本不合并。合并是贪心试探,加之前先算会不会超;会超就不加、开新块。所以「合并超出」的结果是分块,不是截断,句子一个字都没动。
- B. 单独一句本身就超长 → 递归继续降级。在这句内部用更细的分隔符(逗号 → 词)再切,再贪心合并,仍然在标点边界断。
- C. 连最小片段都还超长(如超长无标点串、URL、代码)→ 走到最后一级(逐字符)做字符硬切。这是唯一的硬截断,且配合 overlap 把断点尾部带进下一块缓冲。
1 | 分隔符优先级:段落 → 换行 → 句号 → 逗号 → 单字符("") |
也就是说 chunk_size 不是「绝不超过的硬上限」——一个无法再分的原子片段允许略微超出,除非显式开启强制字符切。能在自然边界分块就绝不硬切,硬切只是兜底。
4. 其他策略与 chunk 大小的权衡
| 方式 | 怎么切 | 适合 |
|---|---|---|
| 固定长度 + 重叠 | 死板按 N 个 token 切 | 最简单基线,文本均匀时够用 |
| 递归字符切分 | 段落→句子→词 层级切(最常用默认) | 通用文档,首选 |
| 结构感知切分 | 按 Markdown 标题 / HTML / 代码函数切 | 结构化文档、代码、手册 |
| 语义切分 | 用 embedding 找话题断点处切 | 追求质量、不在乎成本;较新较贵 |
chunk 大小的权衡:切太小→检索精准但上下文碎、答案凑不齐;切太大→一个向量代表太多内容、embedding 被稀释、检索变差还费 token。经验值 2561024 token、重叠 1020%,再按文档类型和召回效果实测调整。
chunking 是 RAG 里最需要按数据调参的一环,没有放之四海皆准的最优值。先用「递归切分 + 适中 chunk_size + 适度 overlap」作默认,跑通再调。
一句话总结
向量数据库 = 存 embedding、并用 ANN 索引做「按语义相似度找最近邻」的数据库。它把「精确匹配」换成了「意思相近就行」,是给大模型接私有知识、做 RAG 和长期记忆的标准地基。
顺带一提,这和 MCP 能接上:一个「知识库 MCP Server」背后,往往就是一个向量数据库在干检索的活——模型通过 MCP 发起检索,Server 去向量库查回最相关的资料再喂回来。