目录
-
- 一、先把几个词对齐
- 二、第一性原理:为什么会出现向量库
- 三、Milvus 里一张表到底长什么样
- 四、一次查询实际走哪几步
- 五、索引:用可接受的错,换掉全表扫描
- 六、最小例子:把心智模型写成 API
- 七、对日常工作意味着什么
- 八、可带走的结论

官方文档:Milvus Docs Schema:Schema Explained 索引选型:Index Selection 过滤检索:Filtered Search
关系库擅长回答「id = 1001」「status = 已支付」。RAG、以图搜图、相似客诉一上来问的是另一件事:这两段内容像不像。等号解不了「像不像」,于是工程上把内容变成向量,把「像」定义成几何距离。Milvus 做的不是把 MySQL 换皮,而是把「存向量、建近似索引、带过滤地找近邻」做成数据库能力。
本文面向已经熟悉 MySQL / Elasticsearch 的工程师。阅读前请先有「一张表有行有列」的直觉;不必先会训练模型。
本文覆盖:为什么需要向量、Milvus 里一张 Collection 装什么、一次检索怎么走、FLAT / IVF / HNSW 差在哪、什么时候不该上 Milvus。不覆盖:K8s 集群运维、Embedding 模型训练、完整 RAG 工程、与 Pinecone / Weaviate 的跑分对比。
读完后,你应能:用一句话解释向量检索;对照关系表说清 Collection / Entity / 向量字段 / 标量字段;判断自己的场景是该用倒排、该用向量,还是两者一起用。
一、先把几个词对齐
同事嘴里的「向量库」「embedding」「ANN」「召回」经常指同一条链上的不同环节。
| Embedding | 把文本/图/音频编成一组固定长度的浮点数;模型做这件事,不是 Milvus |
| 向量(Vector) | Embedding 的产物,一条高维坐标 |
| 相似度 / 距离 | 两条向量近不近:常见 COSINE / IP / L2 |
| ANN | Approximate Nearest Neighbor,用近似换速度,不保证每次都是全局最近 |
| Collection | Milvus 里最接近「表」的东西 |
| Entity | 一行:主键 + 向量 + 可选标量字段 |
| 标量字段 | 标题、类目、时间、状态等,用来过滤,不是拿来算「语义近」 |
一句话记:模型负责把世界编成坐标;Milvus 负责把海量坐标存住,并在可接受误差内尽快找出邻居。
二、第一性原理:为什么会出现向量库
从约束往回推,只需要四步。
向量检索很像 Elasticsearch 的倒排:都是「先建结构,再少扫一点」。差别是倒排按 词项 切 文档;向量索引按 空间位置 组织 点。

图 1:左边查「苹果」只回含这个词的文档;右边在向量空间里,「水果」「iPhone」也可能落在近邻圈内。
常见情况这样处理。真正麻烦的是边界情况:法律条款、订单号、错误码仍然要精确匹配。向量库补的是语义召回,不替代主键和倒排。
三、Milvus 里一张表到底长什么样
Collection 可以按关系库来对照,但有两处不能混。
| Database / Table | Database / Collection | Collection 必须有至少一个向量字段 |
| Row | Entity | 一行里的 向量维度写进 Schema,事后不能偷偷改 dim |
| Column | Field | 字段分向量字段和标量字段,职责不同 |
| Primary key | Primary key | 仍要有;用来定位、去重、点查 |
| B+Tree / 倒排 | Vector index + 可选标量索引 | 向量字段必须建索引才能 load 后检索 |
| WHERE | Filter 表达式 | 过滤的是标量;「像不像」走向量 |
一条知识库切片在 Schema 里通常是:
| id | 主键 | 回表、去重、删除 |
| embedding | FLOAT_VECTOR,如 dim=768 | 语义近邻 |
| title / chunk | VARCHAR | 展示原文;也可做少量标量条件 |
| biz_type / created_at | 标量 | biz_type == "faq" and created_at > … |
Milvus 不替你算 Embedding。写入前,业务侧先用同一套模型把文本变成 dim 维向量;查询时必须用同一套模型编查询句。模型和库换一套,近邻关系就作废。
四、一次查询实际走哪几步
用户打的是自然语言。Milvus 收到的是已经编好的 query vector。中间那一跳在应用进程里,不在数据库内核里。

图 2:Embedding 在库外完成;Milvus 内部并行的是「在索引里找近邻」和「用标量把范围收窄」,最后吐 TopK 实体。
对调用方,官方约定的顺序是固定的:建表 → 写入 → 建索引 → load → search。跳过 load,集合数据还在,但查询节点读不到。
#mermaid-svg-yLFT7XdUQ7yqY6oM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yLFT7XdUQ7yqY6oM .error-icon{fill:#552222;}#mermaid-svg-yLFT7XdUQ7yqY6oM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yLFT7XdUQ7yqY6oM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .marker.cross{stroke:#333333;}#mermaid-svg-yLFT7XdUQ7yqY6oM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yLFT7XdUQ7yqY6oM p{margin:0;}#mermaid-svg-yLFT7XdUQ7yqY6oM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster-label text{fill:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster-label span{color:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster-label span p{background-color:transparent;}#mermaid-svg-yLFT7XdUQ7yqY6oM .label text,#mermaid-svg-yLFT7XdUQ7yqY6oM span{fill:#333;color:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .node rect,#mermaid-svg-yLFT7XdUQ7yqY6oM .node circle,#mermaid-svg-yLFT7XdUQ7yqY6oM .node ellipse,#mermaid-svg-yLFT7XdUQ7yqY6oM .node polygon,#mermaid-svg-yLFT7XdUQ7yqY6oM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .rough-node .label text,#mermaid-svg-yLFT7XdUQ7yqY6oM .node .label text,#mermaid-svg-yLFT7XdUQ7yqY6oM .image-shape .label,#mermaid-svg-yLFT7XdUQ7yqY6oM .icon-shape .label{text-anchor:middle;}#mermaid-svg-yLFT7XdUQ7yqY6oM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .rough-node .label,#mermaid-svg-yLFT7XdUQ7yqY6oM .node .label,#mermaid-svg-yLFT7XdUQ7yqY6oM .image-shape .label,#mermaid-svg-yLFT7XdUQ7yqY6oM .icon-shape .label{text-align:center;}#mermaid-svg-yLFT7XdUQ7yqY6oM .node.clickable{cursor:pointer;}#mermaid-svg-yLFT7XdUQ7yqY6oM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .arrowheadPath{fill:#333333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yLFT7XdUQ7yqY6oM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yLFT7XdUQ7yqY6oM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yLFT7XdUQ7yqY6oM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster text{fill:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM .cluster span{color:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yLFT7XdUQ7yqY6oM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yLFT7XdUQ7yqY6oM rect.text{fill:none;stroke-width:0;}#mermaid-svg-yLFT7XdUQ7yqY6oM .icon-shape,#mermaid-svg-yLFT7XdUQ7yqY6oM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yLFT7XdUQ7yqY6oM .icon-shape p,#mermaid-svg-yLFT7XdUQ7yqY6oM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yLFT7XdUQ7yqY6oM .icon-shape .label rect,#mermaid-svg-yLFT7XdUQ7yqY6oM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yLFT7XdUQ7yqY6oM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yLFT7XdUQ7yqY6oM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yLFT7XdUQ7yqY6oM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
createCollection
insert
createIndex
loadCollection
search / query
search 是向量近邻(可带 filter)。query 是按标量/主键取行,不算「像不像」。两者不要混着用错。
带过滤时,请求里会同时出现「找谁像」和「只在哪一类里找」。例如:在 biz_type == "faq" 的子集里,找与用户问题最近的 5 条。过滤表达式写复杂了会拖延迟;基础用法先保持「少量标量条件 + TopK」。
五、索引:用可接受的错,换掉全表扫描
没有索引时,FLAT 对每个实体算一次距离,召回最高,规模一大就不可用。ANN 的第一性原理就一句话:先丢掉大部分点,再在剩下的点里精算。

图 3:左边是精确但线性的;右边先圈候选集。丢掉的点里可能有真正的最近邻,这就是召回率代价。
基础阶段先记住三种,不必一次调完参数。
| FLAT | 全量算距离 | 数据量小、要对齐召回上限 | 查询随 N 变慢 |
| IVF_FLAT | 先分簇,查询只探 nprobe 个簇 | 内存紧、带过滤较常见 | nprobe 越大越准也越慢 |
| HNSW | 分层近邻图上跳 | 内存够、要高召回低延迟 | 图占内存;过滤比例极高时图会碎 |
| AUTOINDEX | 让 Milvus 选默认实现 | 入门、还没开始调参 | 极致调优时仍要换成显式类型 |
度量也要和模型一致,不能凭感觉挑。
| L2 | 欧氏距离小 | 向量没归一化时,模长大的样本会「看起来更远」 |
| IP | 内积大 | 模长会进分数;未归一化时和「夹角相似」不是一回事 |
| COSINE | 夹角小(已按方向比) | 多数文本 Embedding 入门先用它,少踩模长坑 |
入门默认:AUTOINDEX + COSINE。要自己上 IVF / HNSW 时,先锁死同一份评测集再改 nprobe / ef,不要在线上凭一次手感调。
六、最小例子:把心智模型写成 API
环境:Python 3、pymilvus 与 Milvus 2.x 兼容版本、实例地址 http://localhost:19530。向量用假数据占位;换成真实模型时,写入和查询必须同一套。
from pymilvus import MilvusClient
import random
dim = 8
client = MilvusClient("http://localhost:19530")
client.create_collection("faq", dimension=dim, metric_type="COSINE")
rows = [
{"id": 1, "vector": [random.random() for _ in range(dim)], "title": "如何退货"},
{"id": 2, "vector": [random.random() for _ in range(dim)], "title": "运费谁承担"},
]
client.insert("faq", rows)
hits = client.search(
collection_name="faq",
data=[[random.random() for _ in range(dim)]],
limit=2,
output_fields=["title"],
)
print(hits)
做完后你应看到:返回列表里每条带 id、distance 和 title。distance 在 COSINE 下越大越像(具体是否已转成相似度,以当前 SDK 文档为准,读字段时不要和 L2 混比)。
小数据量下这套没问题。放到百万级,假向量和没评测的索引参数都会让「看起来能搜」变成「召回随机」。
七、对日常工作意味着什么
上 Milvus 之前,先问三件事。
适合的典型场景:问答知识库、相似工单、以图搜图、推荐里的向量召回层。不适合:订单状态机、库存扣减、强一致转账;也不适合把未切片的整本手册塞成一条 768 维向量还指望定位到某一段。
仍未完全搞清、需要你自己用数据验证的是:过滤比例很高时,HNSW 是否比 IVF 更差。官方和社区都指出图在高过滤比下会碎;这不是口诀能代替评测的点。
八、可带走的结论
整理完毕,完结撒花~🌻



