1. 起步之前:为什么我会写这样一篇入门文章
先交代一下背景。我最早接触向量数据库是在做大模型问答系统的时候,当时最头疼的问题不是模型效果,而是“怎么从几千上万条知识里快速找出和用户问题最相关的那几段”。最开始用传统数据库的 LIKE 查询,后来试过 Elasticsearch 的关键词匹配,效果都差强人意。直到有天同事甩给我一个词:Embedding。后来我又在项目里逐步研究 ANN、HNSW、IVF 这些概念,才真正把整套链路跑通。
这篇内容我打算写得非常直白,不讲虚的。核心主线就一条:从文本变成向量的过程,也就是 Embedding,再到向量数据库如何用近似最近邻检索(ANN)快速找相似向量。中间会把原理、实操选型、常见坑全部拆开说清楚。
适合谁看?如果你正在做大模型知识库、推荐系统、图片搜索、去重系统,或者只是听到“向量数据库”这个概念但不知道它到底解决什么问题,那这篇文章可以帮你省不少摸索时间。我默认你有一点 Python 和数据库基础,但即使没有,概念部分也能看懂。
2. 从“文本”到“向量”:Embedding 到底是什么
2.1 一个简单的类比:把语言变成坐标
我第一次理解 Embedding 是靠一个类比:想象你把一个人放到一张地图上,地图上的每个点代表他“整体的状态”,比如爱好、职业、性格。如果两个人的点离得近,说明他们相似;离得远,说明差异大。Embedding 做的事情,就是把一段文本、一张图片甚至一条用户行为序列,映射到这样一个“多维地图”上,只不过这个地图不是二维的,而是几百上千维。
常规说法叫“语义向量化”。比如“猫”和“小狗”这两个词的向量距离会非常近,而“猫”和“汽车”的距离会很远。更关键的是,向量不仅能表达“词义相近”,还能表达“语义关系”。比如经典的“国王 - 男人 + 女人 ≈ 女王”这类操作,虽然实际工程中不一定都理想,但至少说明向量空间里保留了语义结构和关系。
2.2 常见 Embedding 模型有哪些
很多人一听“Embedding 模型”就觉得高深,实际上可以理解为一个“文本到向量的转换器”。输入是一句话,输出是一串浮点数。不同模型输出的向量维度不一样,语义表达能力也不一样。
我自己用过的几类模型大概包括:
- OpenAI 系列:text-embedding-ada-002、text-embedding-3-small、text-embedding-3-large。优点是效果稳定,使用简单,直接调 API 就行。缺点是数据要出网,对数据敏感场景不友好,而且有成本。
- 开源中文模型:BGE 系列(如 bge-large-zh、bge-m3)、M3E、text2vec 等。这些在中文语义匹配上表现不错,可以本地部署,特别适合私有化项目。
- 多模态模型:CLIP 可以把图片和文本映射到同一个向量空间,这样就能用文字搜图片,或者用图片找相似图片。
- LLM 的隐式向量:有些方案直接用大模型的中间层输出作为向量,比如近年讨论较多的 Qwen3-VL 这类多模态模型中的 embedding 结构,主要用在跨模态理解任务上,门槛偏高,普通业务场景暂时不太需要。
选择模型时,我一般会看三个指标:向量维度、语义效果、推理速度和成本。维度不是越高越好,维度太高,存储和计算成本都会增加;效果上必须拿自己的业务数据测,网上榜单只能作为参考。
2.3 Embedding 模型的“微调”什么时候需要做
有人问过我:“Embedding 模型要不要微调?”我的答案通常是:先别急着微调,先用通用模型跑一遍基线。如果发现某些专业词汇、特殊语境下的相似度排序明显不对,再考虑用 LoRA 这类轻量方案微调 Embedding 模型。
LoRA(Low-Rank Adaptation)是现在比较主流的微调方式,核心思想是冻结原模型的大部分参数,只训练一小部分低秩矩阵。好处很明显:显存占用小,训练速度快,几十分钟到几小时就能跑完一个中小规模数据集。我曾经在某个法律文书场景中用几千条标注好的“相似问题对”对 bge-large-zh 做 LoRA 微调,效果提升确实比通用模型明显,尤其是对法条简称、案由表述这类领域文本。
但有一点要提醒:微调不是万能的。如果你的数据量很小,比如几百条,微调很容易过拟合;如果你用的模型本身就是大型多模态模型,微调成本更高,需要谨慎评估收益。
2.4 从文本到向量的实操流程
在实际项目中,把一个文本库变成向量集合,大致流程如下:
- 准备文本数据,做清洗:去掉无关字符、统一格式、处理空值。
- 设置切分策略:长文档需要切分成 chunk,切分大小和重叠率直接影响检索质量,后面我会专门讲。
- 批量调用 Embedding 模型,生成向量。
- 把向量连同原始文本、元数据一起写入向量数据库。
- 查询时把用户问题转成向量,再执行 ANN 搜索拿到最相似的若干条。
这里要特别注意第 2 步和第 3 步。很多人一开始图省事,直接把整篇长文档塞给模型生成一个向量,结果检索时命中率很低。原因很简单:一个上千字的文档里包含多个主题,压缩成一个向量之后,很多细节就被“平均”掉了。所以切分策略很关键。
我常用的切分方式有两种:一是按固定长度切分,带一定 overlap;二是按段落或语义结构切分。固定长度简单稳定,适合大多数场景;语义切分效果好,但依赖额外模型,成本更高。常规项目里,我会优先用固定长度加 overlap 的方式,切分长度视文档语言和模型能力而定,中文常见 200 到 500 字左右。
3. 向量数据库:它和普通数据库到底差在哪
3.1 为什么不能用传统数据库存向量
先说结论:不是完全不能,而是“查询效率”跟不上。
传统关系型数据库,比如 MySQL,存向量本身没问题,一个 BLOB 或者 JSON 字段就能塞进去。但问题是,当你要找“与某个向量最相似的 10 个向量”时,它只能把所有向量取出来,逐一计算距离,这就是暴力搜索。数据量小还好,几千条可以接受;但到了几十万、几百万条,每次查询都全表扫描,延迟会爆炸。
向量数据库的核心价值,就是通过专门的索引结构和算法,把“精确但慢”的暴力搜索,变成“近似但快”的近似最近邻检索。这里的“近似”意味着不保证每次都返回全局最优结果,但会尽量接近最优,同时在召回率和查询速度之间做平衡。
3.2 向量数据库的典型架构
一个向量数据库通常包含这些部分:
- 向量索引引擎:负责构建和更新索引,是性能核心。
- 存储引擎:负责持久化向量数据和元数据。
- 查询引擎:负责解析查询请求、调用索引、返回结果。
- 标量过滤:配合 SQL 或 API,支持按标签、时间、状态等标量字段过滤后再检索向量。
- 分片与高可用:大规模场景下支持将数据分布到多节点,保证扩展性和可用性。
这些能力组合起来,让向量数据库可以当作一个完整的检索服务来用,而不只是存储工具。
3.3 主流向量数据库选型对比
现在市面上可选的向量数据库非常多,兆维一下常见的几类:
- Milvus:开源分布式向量数据库,支持大规模数据、丰富的索引类型、完整的 API 和生态,适合生产环境,尤其在推荐、搜索场景下用得多。缺点是组件多,部署和运维复杂度偏高。
- Qdrant:Rust 编写,性能好,部署相对简单,而且提供了比较友好的 Python 客户端和 Web UI。我近期很多项目直接用 Qdrant,几个命令就能在本地跑起来,配合 Docker 很顺手。
- Redis 向量检索:如果你已经有 Redis,可以通过 Redis Stack 的向量集合能力做简单 ANN 搜索,适合轻量级或缓存型场景。它不算全功能向量数据库,但胜在简单。
- Elasticsearch:传统搜索引擎也加入了向量检索能力,比如 dense_vector 字段,适合已有 ES 体系、希望统一关键词与向量搜索的场景。
- Chroma、Weaviate、Pinecone:各有特点。Chroma 简单轻量,适合原型;Pinecone 是托管服务,省心但数据要上云;Weaviate 在语义搜索和插件生态上有自己的优势。
选型没有标准答案,要结合数据规模、部署环境、团队技术栈、成本预算综合判断。我个人的经验是:原型验证用 Chroma 或 Qdrant 起步,生产环境如果数据量大、并发高,优先考虑 Milvus;如果想省运维,就选云托管服务。
3.4 Qdrant 的下载安装与初步体验
因为 Qdrant 上手简单,我以它为例子做个实操演示。
最方便的方式是 Docker 启动:
docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant启动之后,控制台会监听 6333 端口(RESTful API)和 6334 端口(gRPC)。如果你不习惯命令行,Qdrant 还提供了 Web UI,访问http://localhost:6333/dashboard就能看到。
之后用 Python 客户端连接:
pip install qdrant-client创建一个 collection 并插入向量的示例:
from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client = QdrantClient(host="localhost", port=6333) client.recreate_collection( collection_name="demo", vectors_config=VectorParams(size=768, distance=Distance.COSINE), ) points = [ PointStruct(id=1, vector=[0.1, 0.2, ...], payload={"text": "苹果"}), PointStruct(id=2, vector=[0.3, 0.1, ...], payload={"text": "香蕉"}), ] client.upsert(collection_name="demo", points=points)这里的size必须和 Embedding 模型输出的维度一致,比如 bge-large-zh 输出 1024 维,你的 size 就要填 1024。distance 通常用 Cosine 或 Dot Product,下面会讲。
4. ANN 检索核心:从暴力搜索到近似最近邻
4.1 距离度量怎么选
在做 ANN 之前,先明确向量之间的“相似度”怎么算。常见有三种:
- 欧氏距离(L2):衡量空间中的直线距离。值越小表示越相似。适合向量分布比较均匀、绝对距离有意义的场景。
- 余弦相似度(Cosine):衡量两个向量的方向一致性,不关心向量长度。值越接近 1 表示越相似。文本 Embedding 最常用这种。
- 点积(Dot Product):相当于余弦相似度还没归一化。如果模型输出已经做了归一化,点积和余弦结果等价,而且计算更快。
我一般做文本检索直接用 Cosine;如果预训练模型本身默认使用点积优化,或者向量已经归一化,就用 Dot。具体用哪个,强烈建议在测试集上分别跑一下,看最终召回效果,而不是凭直觉拍板。
4.2 暴力搜索的局限
暴力搜索(Brute Force)很好理解:把库里每个向量和目标向量算一遍距离,排个序,取前 K 个。这个做法在数据量小的时候完全可行,准确率 100%。但时间复杂度是 O(N),N 是向量总数。向量维度越高,每次距离计算的开销也越大,所以当 N 达到百万级,一次查询可能就要几十毫秒甚至更久,并发上来就扛不住。
这就是 ANN 存在的意义:牺牲一点点召回率,换来数量级的速度提升。
4.3 主流 ANN 算法与索引原理
ANN 算法有很多种,但主流思路可以概括为几类:
基于树的索引(如 KD-Tree、 Annoy):把向量空间不断划分成子空间,查询时只在部分子树里搜索。树结构简单,构建快,但不适合超高维数据,维度一高容易退化。
基于哈希的索引(如 LSH,局部敏感哈希):设计一组哈希函数,让相似的向量以较大概率被分到同一个桶里。查询时只需要在少数几个桶里搜索。LSH 理论优雅,但实际效果依赖哈希函数的参数调节,通常需要多个表来保证召回。
基于量化的索引(如 PQ,乘积量化):把高维向量切分成若干子向量,对每个子向量分别聚类,用聚类中心的 ID 去近似原向量。这样可以大幅压缩存储空间,查询时用查表方式快速估算距离。IVF(倒排文件)常和 PQ 结合使用,先通过聚类缩小搜索范围,再在候选集里做精确计算。
基于图的索引(如 HNSW):HNSW 是目前最流行的 ANN 索引之一。它构建一个多层图,上层图负责“快速跳跃”,底层图负责“精细搜索”。查询时从顶层开始,逐层向下,最终在底层找到最近邻。HNSW 的召回率高、查询快,但内存占用较大,索引构建时间也比其他方法长一些。
4.4 走近 HNSW:为什么它效果好
HNSW 全称是 Hierarchical Navigable Small World,层次化可导航小世界图。名字听起来拗口,其实思想很简单:类似“六度分隔”理论,社交网络里任意两个人通过少量中间人就能建立联系。HNSW 就是让向量空间里的节点通过“短途连接”实现高效导航。
具体来说,图中有多层结构,上层节点少,连接稀疏,负责快速接近目标区域;下层节点多,连接密集,负责精细定位。搜索时从上往下,每层都用贪心策略找局部最近点,然后跳到下一层继续找,直到底层。
HNSW 有三个重要参数:
- M:每个节点的最大连接数。M 越大,图连接越密,召回越高,但内存和构建时间也增加。
- efConstruction:构建索引时动态候选列表大小。越大索引质量越好,但构建越慢。
- efSearch:查询时动态候选列表大小。越大召回越高,但查询越慢。
在实际工程里,调参目标就是找到“速度、内存、召回”三者都可接受的平衡点。比如我用 Qdrant 的 HNSW 时,M 默认 16,efConstruction 默认 100,查询时 ef_search 设 64 到 128,具体看业务容忍的延迟和召回要求。
4.5 ANN 的度量指标
不少人在做完 ANN 后只问“快不快”,很少问“准不准”。其实业界有一个核心指标叫Recall@K,含义是:用 ANN 取回的结果中,有多少比例是暴力搜索的 Top-K 结果。
比如暴力搜索 Top-10 结果是 A、B、C……ANN 返回的结果里包含 A、B、C 中的 8 个,那么 Recall@10 就是 80%。一般生产环境要求 Recall@10 在 95% 以上才比较放心。如果低于 90%,说明索引参数或者索引类型选得不够合适。
5. 完整项目实操:搭建一个本地知识库问答的向量检索链路
说了这么多概念,下面我用一个具体场景把它们串起来:做一个简单的本地知识库问答系统。用户上传几篇文档,系统切分、Embedding、写入向量数据库;用户提问时,系统先从知识库里检索最相关的片段,再交给大模型生成回答。
这一步不依赖外部 API,数据不出本地,适合对敏感信息有要求的场景。
5.1 环境准备
我用到的组件如下:
- Python 3.9 以上
- Qdrant(Docker 运行)
- sentence-transformers 库用于加载 Embedding 模型
- 一个开源 Embedding 模型,比如
BAAI/bge-large-zh-v1.5 - 可选的 LLM 生成模型,比如本地部署的 Qwen、ChatGLM 系列(生成环节可选,也可以先用检索结果做展示)
安装 Python 依赖:
pip install sentence-transformers qdrant-client我的建议是先把检索链路跑通,生成回答的环节再单独接大模型,否则调试时问题太多,不好定位。
5.2 文档切分与向量化
切分我用固定长度加 overlap 的方式做了一个轻量函数:
def split_text(text, chunk_size=300, overlap=50): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) if end >= len(text): break start = end - overlap return chunks这段代码会按 300 个字符切分,每两个相邻 chunk 有 50 个字符的重叠。重叠的目的就是防止句子被生生切断在边界上,造成语义残缺。
然后加载模型生成向量:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("BAAI/bge-large-zh-v1.5") def embedding_texts(texts): return model.encode(texts, normalize_embeddings=True)这里normalize_embeddings=True很关键,它能保证后续用点积和余弦相似度等价,还能稍微提升检索效果。
5.3 写入 Qdrant
把切分好的文本和向量一起写入 Qdrant:
from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct client = QdrantClient(host="localhost", port=6333) collection_name = "knowledge_base" client.recreate_collection( collection_name=collection_name, vectors_config=VectorParams(size=1024, distance=Distance.COSINE), ) points = [] for idx, chunk in enumerate(chunks): vec = embedding_texts([chunk])[0] points.append( PointStruct( id=idx, vector=vec.tolist(), payload={"text": chunk, "source": "doc1.txt"} ) ) client.upsert(collection_name=collection_name, points=points)这里size=1024是因为我选的模型输出 1024 维。你换成其他模型,维度可能不同,一定要确认。
5.4 检索并输出
查询时,把用户问题同样转成向量,然后调query_points:
query = "这份文档里提到了哪些注意事项?" query_vec = embedding_texts([query])[0] result = client.query_points( collection_name=collection_name, query=query_vec, limit=5, search_params={"hnsw_ef": 64, "exact": False} )返回结果里带 payload,可以直接把 text 字段提取出来,拼成上下文喂给 LLM。如果不接 LLM,也至少能拿到“最相关的5段原文”,这一步对调试 Embedding 模型效果特别有用。
5.5 实测结果分析与参数调整
跑通第一版之后,我通常会做几个小实验:
- 用不同的 query 测试,看返回的片段是不是真的相关。如果不相关,先检查切分长度,再换 Embedding 模型试效果。
- 调整
hnsw_ef从 32 到 128,看召回变化。数据量小时,差异不明显;数据量大时,效果就拉开了。 - 对比
exact=True和exact=False的结果。如果 ANN 结果和精确结果差异很大,说明索引参数太激进,需要增大hnsw_ef。
这种“先精确后近似”的对比习惯,我推荐大家保留下来,它是判断索引质量最直接的手段。
6. 检索链路里的常见问题和排查技巧
6.1 切分不合理导致上下文语义断裂
这是知识库问答里最容易踩的坑。固定长度切分虽然简单,但很可能把一个完整句子拦腰切断,检索到的片段读起来前后不通,大模型拿它生成答案自然效果差。
我后来的做法是:先按段落粗分,段落太长再用固定长度切;切分边界尽量保持在换行符或标点之后。如果预算允许,也可以用专门的语义切分模型。还有一个技巧:切分时保留文档标题作为 chunk 的前缀,比如“第一章 总则”,这样每个片段都自带上下文信息,检索效果会明显更好。
6.2 维度不匹配或向量未归一化
很多人第一次写代码时会报错:Vector dimension mismatch。原因很简单,collection 的size设置和模型输出维度不一致,或者模型换过但 collection 没重建。
解决办法:固定模型,记录维度;模型变更时,重建 collection。
还有一类坑是漏了normalize_embeddings=True。向量未归一化时,用 Cosine 距离虽然理论上不受长度影响,但有些数据库底层实现为了性能可能先做点积,未归一化会产生偏差。所以我的习惯是,无论用什么模型,都先归一化,省掉后患。
6.3 ANN 召回率不达标怎么调
如果你发现 ANN 召回率偏低,按以下顺序排查:
- 先调
ef_search,这个参数提升空间最直接,通常从 64 调到 256,召回能明显涨。 - 再调
M,M 越大连接越密,但内存占用增加,索引时间也变长。 - 确认索引类型是否合适。比如数据量特别小(几百条以内),用精确搜索可能比 ANN 更省心;数据量达百万,HNSW 一般够用;如果维度极高且存储压力大,考虑 IVF+PQ。
- 检查 Embedding 模型本身是否足够好。通用模型在专业领域效果不佳时,调索引参数只是杯水车薪,该微调就微调。
6.4 脏数据造成的检索偏差
一个容易被忽略的问题是文本清洗。文档里经常有页眉页脚、多余换行、乱码字符、表格残缺内容。这些脏数据切分后产生大量无意义 chunk,检索时可能突然冒出来,拉低整体效果。
我习惯在切分前做一遍规则清洗:
- 去除连续空白字符、特殊符号。
- 去掉以页码、日期开头的行。
- 过滤明显过短的 chunk,比如少于 20 字。
- 表格尽量转为 Markdown 格式,保持行列关系可读。
清洗做得好,检索效果提升立竿见影,而且能减少无用向量占据存储空间。
6.5 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索结果完全不相关 | Embedding 模型不适合领域数据 | 换行业语料微调或换更强模型 |
| 召回率低 | HNSW 参数过小 | 调大 hnsw_ef,再调大 M |
| 报向量维度错误 | 索引 size 与模型输出不一致 | 重建 collection 并匹配维度 |
| 查询延迟高 | 数据量大且索引参数过大 | 降 ef_search,或改用量化类索引 |
| 结果老带无用片段 | 切分不干净或 chunk 太短 | 优化切分策略,过滤短文本 |
| 相似度分数整体偏高 | 未归一化导致余弦计算异常 | 模型输出时开启 normalize |
| 文档新增后查不到 | 未执行 upsert 或索引未刷新 | 检查写入结果,确认 collection 正确 |
这里的每一个问题我都实际碰到过,尤其是“未归一化导致相似度分数偏高”这个问题,乍一看分数很高,实际排序乱套,排查了很久才发现是归一化的锅。
7. 粗浅实践之外:一些进阶方向
如果你已经完整跑通了入门链路,还想再深入,可以考虑下面几个方向。
第一,混合检索:在纯向量检索之外,配合关键词检索(BM25)做结果融合。向量擅长语义相似,关键词擅长精确匹配,两者互补在很多场景能有效提升召回质量,尤其是技术文档、法律条文这类含大量专有名词的内容。
第二,多模态向量检索:CLIP 让文本和图片进入同一个向量空间,你可以实现“文字搜图”“以图搜图”。再往下扩展,视频片段、音频也可以抽象成向量,做成跨模态知识库。
第三,微调 Embedding 模型并上线:LoRA 微调只是第一步,真正上线时还要考虑量化、推理服务化、热更新模型等问题。届时你可能需要引入模型服务框架,把 Embedding 推理做成独立服务。
第四,列式存储与资源估算:向量占用内存比想象中更快。一个 1024 维向量用 float32 存储,约 4KB;100 万条就是约 4GB 内存,加上 HNSW 图的额外索引开销,实际内存可能是原始向量的好几倍。做容量规划时,一定先算账,避免内存爆掉。
8. 最后分享几点实操心得
写到最后,说几个我自己的真实体会。
向量数据库和 Embedding 都不是多高深的东西,难的是把每个环节的细节做扎实。数据清洗、切分策略、模型选型、索引参数、召回评估,任何一个环节草率,最终检索质量都会打折。很多项目最后效果不好,并不是数据库或算法的问题,而是最前面的数据处理没做好。
我踩过一次很深的坑:项目刚开始时为了图快,直接用 OpenAI 接口生成所有向量,一轮跑下来花了不小费用,后面换成本地模型时才发现两套向量并不能直接混在同一个 collection 里,所有数据都得重新生成。所以我的建议是,项目前期就要定好 Embedding 模型,中途不要随便换;如果一定要换,就要接受重新向量化的成本。
另外,别把向量检索当成万能药。对于有些需求,比如精确 ID 查找、简单关键词过滤、数值范围筛选,传统数据库反而更合适。合理的架构通常是传统数据库、搜索引擎、向量数据库各司其职,组合使用,而不是一套方案通吃所有查询。
最后再分享一个小技巧:调试阶段不要一上来就配置超大索引参数,先在几千条数据上跑通全流程,确认检索效果能达到预期,再放大数据规模调优参数。这样既节省时间,也更容易定位问题。
如果你正准备做自己的知识库或语义搜索项目,希望这篇文章能帮你少走几步弯路。后续我还会写关于 HNSW 参数调优细节、混合检索实现、以及 Embedding 模型微调的相关内容,欢迎交流。