Milvus 向量数据库(介绍与使用):核心概念 + 增删改查 + 混合检索实战
一句话总结 :Milvus 是 2019 年开源的向量数据库 ,唯一目标是存储、索引和管理由深度模型生成的大规模嵌入向量 。它和 MySQL 的分工很清晰------MySQL 存"结构化字段",Milvus 存"语义向量" :把图片、文本、音频这些非结构化数据 用嵌入模型转成向量存进去,检索时用相似度 而不是
WHERE条件来找「最像的那几条」。本文用pymilvus从零跑通建库 → 建表 → 写入 → 检索 → 删除的完整链路,并覆盖 FLAT / IVF / HNSW 索引选型与 RRF 混合检索。
📋 目录
- Milvus 是什么
- 四个核心概念
- Milvus 与 MySQL:Collection 和 Field 对照
- 索引类型与相似度度量(重点)
- 环境准备:三种部署形态怎么选
- 五分钟跑通:建库 → 建表 → 写入 → 检索
- 数据操作全解:增删改查
- 进阶检索:过滤、范围、混合检索
- 常见问题 FAQ
- 本章小结
一、Milvus 是什么
Milvus 是一款开源向量数据库 (2019 年提出,Apache-2.0 协议),其唯一目标是:
存储、索引和管理由深度神经网络和其他机器学习模型生成的大规模嵌入向量。
它的定位和关系型数据库完全不同:
| 对比 | 关系型数据库(MySQL) | 向量数据库(Milvus) |
|---|---|---|
| 主要处理的数据 | 遵循预定义模式的结构化数据 | 由非结构化数据转换而来的嵌入向量 |
| 查询方式 | WHERE age > 18 精确匹配 |
相似度搜索:找最"像"的 Top-K |
| 能不能算"像不像" | 不能 | ✅ 通过计算向量间的相似距离 |
| 设计目标 | 事务、一致性、关系 | 高维向量的近似最近邻检索(ANN) |
| 规模 | 百万~千万行 | 可支撑万亿级向量索引 |
为什么需要它?
互联网上非结构化数据 越来越多------电子邮件、论文、物联网传感器数据、照片、蛋白质结构......这些数据计算机无法直接"理解"。做法是用嵌入技术把它们转成向量,然后:
- Milvus 存储并索引这些向量;
- 检索时,计算查询向量与库中向量的相似距离;
- 两个嵌入向量越相似,意味着原始数据源也越相似。
这就是 RAG、以图搜图、推荐系统、语义去重背后的共同底座。
二、四个核心概念
在动手写代码之前,这四层概念必须理清------它们是后面所有 API 的坐标系。
2.1 非结构化数据
指图像、视频、音频、自然语言 等信息,它们不遵循预定义的模型或组织方式。
📌 这类数据占据世界数据的约 80%,正是向量数据库要处理的主战场。
2.2 嵌入向量(Embedding)
嵌入向量是对非结构化数据的特征抽象。
- 数学上,它是一个浮点数或二进制数的数组;
- 现代的嵌入技术(如 BERT、BGE、CLIP、各类 embedding API)负责把非结构化数据转换成这个数组;
- 数组的每一维代表某种"隐含特征",但单看某一维没有人类可读的含义。
text
"我喜欢吃苹果" ──[嵌入模型]──> [0.021, -0.335, 0.887, ..., 0.104] ← 768/1024/1536 维
一张猫的图片 ──[CLIP]─────> [0.442, 0.118, -0.203, ..., 0.771] ← 512 维
2.3 向量相似度搜索
把查询向量与数据库中的所有向量比较,找出最相似的那些。
如果逐个精确比较(暴力搜索),在亿级数据上会慢到不可用,所以实际使用近似最近邻搜索(ANNS, Approximate Nearest Neighbor Search) 来加速------用一点点精度换极大的速度提升。
2.4 Collection 和 Field
Milvus 的数据组织方式和关系数据库几乎一一对应,迁移心智几乎零成本:
| Milvus | 关系数据库 | 描述 |
|---|---|---|
| Collection | 表 | 集合相当于表,用于组织数据 |
| Field | 字段(列) | 字段 Schema 相当于表中的列 |
is_primary |
主键 | 在 Field Schema 中标记为主键 |
dtype |
数据类型 | 如 INT64、VARCHAR、FLOAT_VECTOR |
max_length |
最大长度 | 对应 VARCHAR 类型字段的最大字符数 |
dim |
--- | 向量字段的维度(关系库无直接对应) |
几条硬约束,先记住:
⚠️ ① 一个 Collection 只能有一个主键字段。
⚠️ ② 1 个 Milvus 集群最多支持 64 个数据库。
⚠️ ③ 一个 Collection 可以包含多个向量字段(原讲义标注上限为 4 个,实际请以当前版本文档为准)------这是"多向量混合检索"的基础。
Field Schema 常用属性
Field schema 是字段的逻辑定义,定义 Collection 之前必须先定义它。
| 属性 | 描述 | 备注 |
|---|---|---|
name |
字段名称 | String,必填 |
dtype |
字段数据类型 | 必填 |
description |
字段描述 | String,选填 |
is_primary |
是否为主键 | Boolean,主键字段必填 |
auto_id |
是否自动分配主键 ID | 主键字段必填 |
max_length |
允许插入的字符串最大长度 | VARCHAR 字段必需,范围 [1, 65535] |
dim |
向量维度 | 浮点向量上限 32768 |
is_partition_key |
是否为分区键 | Boolean |
Collection Schema 常用属性
Collection schema 是 Collection 的逻辑定义 ,它必须在 Field schema 之后定义。
| 属性 | 描述 | 备注 |
|---|---|---|
field |
集合中要创建的字段 | 必填 |
description |
集合描述 | String,选填 |
partition_key_field |
用作分区键的字段名 | String,选填 |
enable_dynamic_field |
是否启用动态字段 | Boolean |
💡
enable_dynamic_field=True的含义 :插入数据时,未在 Schema 中定义的字段会被自动接受 ,以键值对形式存在一个名为$meta的特殊 JSON 字段里。开发阶段很方便,生产环境建议关掉以保证数据规整。💡
auto_id=True的含义 :主键自动递增,插入时不需要手动提供。
三、为什么选择 Milvus
| 优势 | 说明 |
|---|---|
| 大规模高性能 | 在处理大规模数据集的向量搜索时性能突出,支持分布式横向扩展 |
| 生态完整 | 提供多语言 SDK(Python / Java / Go / Node.js / C#)与完整工具链 |
| 云扩展性与高可靠 | 计算存储分离,即使部分节点故障也不影响整体可用性 |
| 混合搜索 | 把标量过滤 与向量相似度搜索配对使用,一条查询同时做"语义"和"业务条件" |
🎯 最后一条最实用:"找语义最相似、且价格小于 100 元、且分类是图书的前 10 条"------这就是混合搜索。
四、索引类型与相似度度量(重点)
这一节是全文最该认真读的部分,因为索引和度量一旦建错,后面所有检索结果都是错的。
4.1 先说一条容易被忽略的规则
⚠️ 如果你不指定索引类型,Milvus 会默认使用暴力搜索(FLAT)。
索引是数据的组织单位 ,在搜索或查询插入的实体之前,必须先声明索引类型和相似度度量。
4.2 主流索引类型
Milvus 支持的大部分向量索引都基于近似最近邻搜索(ANNS):
| 索引 | 原理 | 特点 | 适用场景 |
|---|---|---|---|
| FLAT | 暴力搜索(brute-force) | 100% 精确,但效率低,数据量大时很慢 | 小型、百万级数据集,要求完全精确 |
| IVF_FLAT | 倒排文件索引 | 精度与速度可调 (靠 nprobe) |
大规模数据,要求平衡 |
| IVF_SQ8 | IVF + 标量量化 | 4 字节浮点压成 1 字节整数,内存降约 75% | 内存受限,可接受少量精度损失 |
| IVF_PQ | IVF + 乘积量化 | 存储进一步压缩,速度更快 | 超大规模高维数据 |
| HNSW | 基于图的分层可导航小世界 | 查询效率高,召回好 | 对搜索效率要求高的场景 |
⭐ IVF_FLAT 的三步流程
这是理解所有 IVF 系索引的基础:
#mermaid-svg-r7hA8SDwXT5iTbnA{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-r7hA8SDwXT5iTbnA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-r7hA8SDwXT5iTbnA .error-icon{fill:#552222;}#mermaid-svg-r7hA8SDwXT5iTbnA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-r7hA8SDwXT5iTbnA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-r7hA8SDwXT5iTbnA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-r7hA8SDwXT5iTbnA .marker.cross{stroke:#333333;}#mermaid-svg-r7hA8SDwXT5iTbnA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-r7hA8SDwXT5iTbnA p{margin:0;}#mermaid-svg-r7hA8SDwXT5iTbnA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster-label text{fill:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster-label span{color:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster-label span p{background-color:transparent;}#mermaid-svg-r7hA8SDwXT5iTbnA .label text,#mermaid-svg-r7hA8SDwXT5iTbnA span{fill:#333;color:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA .node rect,#mermaid-svg-r7hA8SDwXT5iTbnA .node circle,#mermaid-svg-r7hA8SDwXT5iTbnA .node ellipse,#mermaid-svg-r7hA8SDwXT5iTbnA .node polygon,#mermaid-svg-r7hA8SDwXT5iTbnA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-r7hA8SDwXT5iTbnA .rough-node .label text,#mermaid-svg-r7hA8SDwXT5iTbnA .node .label text,#mermaid-svg-r7hA8SDwXT5iTbnA .image-shape .label,#mermaid-svg-r7hA8SDwXT5iTbnA .icon-shape .label{text-anchor:middle;}#mermaid-svg-r7hA8SDwXT5iTbnA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-r7hA8SDwXT5iTbnA .rough-node .label,#mermaid-svg-r7hA8SDwXT5iTbnA .node .label,#mermaid-svg-r7hA8SDwXT5iTbnA .image-shape .label,#mermaid-svg-r7hA8SDwXT5iTbnA .icon-shape .label{text-align:center;}#mermaid-svg-r7hA8SDwXT5iTbnA .node.clickable{cursor:pointer;}#mermaid-svg-r7hA8SDwXT5iTbnA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-r7hA8SDwXT5iTbnA .arrowheadPath{fill:#333333;}#mermaid-svg-r7hA8SDwXT5iTbnA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-r7hA8SDwXT5iTbnA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-r7hA8SDwXT5iTbnA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r7hA8SDwXT5iTbnA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-r7hA8SDwXT5iTbnA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r7hA8SDwXT5iTbnA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster text{fill:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA .cluster span{color:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA 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-r7hA8SDwXT5iTbnA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-r7hA8SDwXT5iTbnA rect.text{fill:none;stroke-width:0;}#mermaid-svg-r7hA8SDwXT5iTbnA .icon-shape,#mermaid-svg-r7hA8SDwXT5iTbnA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r7hA8SDwXT5iTbnA .icon-shape p,#mermaid-svg-r7hA8SDwXT5iTbnA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-r7hA8SDwXT5iTbnA .icon-shape .label rect,#mermaid-svg-r7hA8SDwXT5iTbnA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r7hA8SDwXT5iTbnA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-r7hA8SDwXT5iTbnA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-r7hA8SDwXT5iTbnA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 聚类
k-means 把高维空间
划分成 nlist 个簇
② 建倒排索引
每个向量映射到所属簇
③ 查询
先找最近的 nprobe 个簇中心
再在簇内做精确线性搜索
逐条拆开:
- 聚类:用 k-means 之类的算法把向量划分为多个子空间(簇),每个簇有一个代表向量(通常是簇中心)。
- 倒排索引 :为每个簇建倒排索引,每个向量映射到它所属的簇。查询时只需关注与查询向量相似的簇,不必扫全空间,时间复杂度显著下降。
- 查询处理 :
- 先把查询向量分配到距离最近的簇中心;
- 再在该簇(或几个簇)内执行精确的线性搜索;
- 用参数
nprobe控制搜索的簇数,从而平衡精度与速度。
🔑
nprobe怎么调 :增大
nprobe→ 搜更多簇 → 召回更高、结果更准,但查询更慢 ;减小
nprobe→ 搜更少的簇 → 更快,但可能漏掉正确结果 。一般从
nprobe = 8~16起调,配合你的召回率指标找平衡点。
IVF_SQ8 与 IVF_PQ 的区别(一句话版)
- IVF_SQ8 = IVF_FLAT + 标量量化 :把每个维度的 4 字节浮点压成 1 字节整数,省内存,精度略降。
- IVF_PQ = IVF_FLAT + 乘积量化 :把向量切成若干子向量,各自独立量化编码,压缩率更高,适合超大规模高维。
HNSW 是什么
HNSW(Hierarchical Navigable Small World)用多层图组织向量:上层稀疏、用于快速跳转,下层稠密、用于精细定位。查询是在图上"逐层下降"的过程。
它的两个关键参数:
M:每个节点的最大连接数(越大越准、越占内存)efConstruction(建索引时)/ef(查询时):候选队列大小(越大越准、越慢)
4.3 相似度度量:三种,排序方向不同!
这是最容易出错的地方------distance 字段的名字叫"距离",但它的排序方向取决于度量类型。
| 度量 | 含义 | 取值范围 | 谁更相似 |
|---|---|---|---|
| L2(欧氏距离) | 两点间的直线距离 | [0, ∞) |
越小越相似 ⬇️ |
| IP(内积) | 对应维度乘积之和 | [-1, 1] |
越大越相似 ⬆️ |
| COSINE(余弦相似度) | 夹角的余弦值 | [-1, 1] |
越大越相似 ⬆️ |
⚠️ 两个必须知道的细节:
① Milvus 计算 L2 时不做最后开方 ,直接返回平方距离 (为了省一次计算),但这不影响排序结果 。
②
FLOAT_VECTOR的默认度量是 COSINE (当前版本文档),但强烈建议显式指定,不要依赖默认值。
该选哪个?看你的嵌入模型
| 数据/模型 | 推荐度量 | 原因 |
|---|---|---|
| 文本嵌入(OpenAI、BGE、Sentence-Transformers 等) | COSINE | 只关心语义方向,不受文本长度/词频导致的模长影响 |
| 图像特征、地理坐标、原始传感器读数 | L2 | 方向和模长都有意义 |
| 已归一化向量 / 稀疏向量 / DPR 类检索模型 | IP | 计算最快 |
| 二值向量 | HAMMING / JACCARD | 二进制专用(越小越相似) |
⭐ 一个能省很多算力的数学结论
当所有向量都已做 L2 归一化(模长为 1)时:
∥ u − v ∥ 2 = 2 − 2 ( u ⋅ v ) \|u - v\|^2 = 2 - 2(u \cdot v) ∥u−v∥2=2−2(u⋅v)
也就是说:
- COSINE ≡ IP(排序完全一致)
- L2 与 IP 排序完全等价(一个越小越好,一个越大越好)
实操价值 :很多主流文本嵌入模型(OpenAI text-embedding-3-*、多数 Sentence-Transformers)输出的就是归一化向量 ,此时用 COSINE 还是 IP,Top-K 结果基本一样,变的只是数值尺度。
💡 但要注意:同一批数据用不同度量,返回的分数尺度不同 。COSINE 可能返回 0.87,L2 可能返回 0.52,产品同学会问"为什么两个数不一样"------它们描述的是同一个相似关系,只是尺度不同 。所以阈值(threshold)必须和度量绑定,不能跨度量比较分数。
五、环境准备:三种部署形态怎么选
| 形态 | 适用场景 | 怎么起 |
|---|---|---|
| Milvus Lite | 学习、Jupyter、笔记本、边缘设备 | pip install pymilvus,内置 |
| Milvus Standalone | 单机生产、开发联调 | Docker 单容器 |
| Milvus Distributed | 大规模生产、亿级以上 | Kubernetes |
| Zilliz Cloud | 不想自己运维 | 全托管云服务 |
三者共用同一套 API ,所以本地写的代码改一个 URI 就能上生产:
python
# 本地 Lite:传文件路径
client = MilvusClient(uri="milvus_demo.db")
# 远程 Standalone / Cloud:传服务地址 + 凭据
client = MilvusClient(uri="http://localhost:19530", token="username:password")
⚠️ Milvus Lite 的能力边界(很重要)
Milvus Lite 是能力子集,不是完整版。已知限制:
| 限制 | 说明 |
|---|---|
| 操作系统 | 只支持 Linux(Ubuntu ≥ 20.04)和 macOS ≥ 11.0 ,不支持 Windows |
| 索引类型 | 只支持 FLAT 。无论你指定 IVF_FLAT 还是 HNSW,实际都走 FLAT |
| 分区 | 不支持分区及分区相关方法(create_partition、partition_name 等) |
| 用户与角色 | 不支持 |
| 别名 | 不支持;rename_collection 也不支持 |
| 一致性级别 | 仅支持 Strong |
🚨 Windows 用户怎么办?
三条路:① 用 Docker 起 Standalone (最推荐);② 用 WSL2 里跑;③ 直接用 Zilliz Cloud 。
另外注意
pymilvus3.0.1 已要求 Python 3.9+(3.8 已不再支持)。
安装
bash
pip install -U pymilvus
# 如果要显式管理 milvus-lite
pip install -U milvus-lite
版本选型建议:
| 服务端 | 客户端 |
|---|---|
| Milvus 3.0(当前 GA 线) | pip install pymilvus(解析到 3.0.x) |
| Milvus 2.6(保守线) | 显式锁版本 :pip install "pymilvus~=2.6.17" |
⚠️ 不要无脑用
latest:pip install pymilvus现在装的是 3.x 客户端,连 2.6 服务端可能出现行为差异 。生产环境请把客户端版本和服务端对齐并写进依赖文件。
六、五分钟跑通:建库 → 建表 → 写入 → 检索
下面这段代码可以直接跑(除 Windows 外,见上节说明)。
6.1 连接
python
from pymilvus import MilvusClient
client = MilvusClient(uri="milvus_demo.db") # 本地文件,Lite 模式
6.2 定义 Schema 并创建 Collection
python
from pymilvus import DataType
# auto_id=False:主键自己指定
# enable_dynamic_field=True:允许插入未定义的字段(存进 $meta)
schema = client.create_schema(auto_id=False, enable_dynamic_field=True)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=5)
schema.add_field(
field_name="color",
datatype=DataType.VARCHAR,
max_length=256,
description="标量字段,用于过滤",
)
# 索引参数
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="", # 留空 = 自动选择(Lite 下实际为 FLAT)
metric_type="COSINE", # 相似度度量
index_name="vector_index",
)
# 创建 Collection(建表 + 建索引一步到位)
client.create_collection(
collection_name="demo_v1",
schema=schema,
index_params=index_params,
)
💡 生产环境(Standalone)用 IVF_FLAT 的话,把
index_type和params换成:
pythonindex_params.add_index( field_name="vector", index_type="IVF_FLAT", metric_type="COSINE", params={"nlist": 128}, # 聚类簇数 index_name="vector_index", )HNSW 则是
index_type="HNSW", params={"M": 16, "efConstruction": 200}。
6.3 查看索引
python
print(client.list_indexes(collection_name="demo_v1"))
print(client.describe_index(
collection_name="demo_v1",
index_name="vector_index",
))
索引的加载与释放 (检索前 Collection 必须处于 loaded 状态;create_collection 后通常会自动加载):
python
client.load_collection(collection_name="demo_v1")
print(client.get_load_state(collection_name="demo_v1"))
# 不需要索引时可以先释放再删除
# client.release_collection(collection_name="demo_v1")
# client.drop_index(collection_name="demo_v1", index_name="vector_index")
6.4 写入数据
下面数据做了取整与省略,仅用于演示格式;实际请填入你嵌入模型产出的真实向量。
python
data = [
{"id": 0, "vector": [0.358, -0.602, 0.184, -0.263, 0.903], "color": "pink_8682"},
{"id": 1, "vector": [0.199, 0.060, 0.698, 0.261, 0.839], "color": "red_7025"},
{"id": 2, "vector": [0.437, -0.560, 0.646, 0.789, 0.208], "color": "orange_6781"},
{"id": 3, "vector": [0.317, 0.972, -0.370, -0.486, 0.958], "color": "pink_9298"},
]
res = client.insert(collection_name="demo_v1", data=data)
print(res) # {'insert_count': 4, 'ids': [0, 1, 2, 3]}
6.5 检索
python
res = client.search(
collection_name="demo_v1",
data=[[0.199, 0.060, 0.698, 0.261, 0.839]], # 查询向量(注意是二维:支持批量)
limit=2, # 返回 Top-2
output_fields=["id", "color"], # 一起带回标量字段
)
for hits in res:
for hit in hits:
print(hit["id"], hit["distance"], hit["entity"]["color"])
📌
search_params在建索引时已指定度量,检索时可以省略;如果要临时覆盖 ,传search_params={"metric_type": "COSINE"}。
跑通这五步,你就已经掌握了 Milvus 80% 的日常用法。
七、数据操作全解:增删改查
7.1 增:insert
python
# 普通插入
client.insert(collection_name="demo_v2", data=data)
插入到指定分区 (仅 Standalone / Distributed 支持,Lite 不支持):
python
client.create_partition(collection_name="demo_v2", partition_name="partitionA")
client.insert(collection_name="demo_v2", data=data, partition_name="partitionA")
7.2 改:upsert
Milvus 的 upsert 是数据级操作,行为很直观:
| 情况 | 结果 |
|---|---|
| 集合中已存在该主键 | 覆盖现有实体 |
| 集合中不存在该主键 | 插入新实体 |
python
res = client.upsert(collection_name="demo_v2", data=data)
⚠️ 一个容易误判的细节 :如果指定的分区里不存在 该 id,那么这次 upsert 不会影响该分区 ,但会影响集合中其他分区里相同 id 的实体------因为主键在 Collection 内唯一,不是分区内唯一。
7.3 删:delete
两种方式,都可以指定分区(不指定则默认作用于整个集合):
python
# ① 按过滤表达式删除
res = client.delete(collection_name="demo_v2", filter="id in [12, 5, 6]")
# ② 按主键删除(指定分区)
res = client.delete(
collection_name="demo_v2",
ids=[1, 2, 3, 4],
partition_name="partitionA",
)
7.4 查:query vs search
这两个 API 的分工必须分清:
| API | 用途 | 是否需要向量 | 例子 |
|---|---|---|---|
query |
按标量条件查数据(类似 SQL 的 WHERE) | ❌ 不需要 | 查所有 color = "red_7025" 的记录 |
search |
按向量相似度查 Top-K | ✅ 需要 | 找与查询向量最像的 10 条 |
python
# 标量查询:不涉及向量
res = client.query(
collection_name="demo_v1",
filter='color like "red%"',
output_fields=["id", "color"],
limit=10,
)
💡 实际业务里两者常组合:先用
filter缩小范围,再做向量search------这就是混合搜索的基本形态。
八、进阶检索:过滤、范围、混合检索
8.1 带过滤的向量检索
filter 参数会在向量检索的同时应用标量过滤,等价于"先筛选再算相似度":
python
res = client.search(
collection_name="demo_v1",
data=[[0.358, -0.602, 0.184, -0.263, 0.903]],
limit=5,
output_fields=["color"],
filter='color like "red%"', # 只在前缀为 red 的记录里找
)
支持的前缀/中缀/后缀匹配 都用 like:"red%"、"%red%"、"%red"。
8.2 范围检索(Range Search)
不是找最近的 Top-K,而是找"距离落在某个区间内"的所有向量。
python
res = client.search(
collection_name="demo_v1",
data=[[0.358, -0.602, 0.184, -0.263, 0.903]],
limit=3,
search_params={
"metric_type": "COSINE",
"params": {
"radius": 0.8, # 边界一
"range_filter": 1.0, # 边界二
},
},
output_fields=["color"],
)
⚠️
radius和range_filter谁是上限、谁是下限,取决于度量方向------这是最容易搞反的地方:
度量 谁更相似 radiusrange_filter最终区间 L2 越小越好 外边界(上限) 内边界(下限) [range_filter, radius]IP / COSINE 越大越好 内边界(下限) 外边界(上限) [radius, range_filter]上面的例子是 COSINE +
radius=0.8, range_filter=1.0,即找相似度在 0.8~1.0 之间的向量。
8.3 混合检索(Hybrid Search)
当你的 Collection 里有多个向量字段 (比如"剧情描述向量" + "海报向量"),可以一次调用同时做多路 ANN 搜索,再把结果融合重排。
#mermaid-svg-BkwwD6nWEmv5GdCD{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-BkwwD6nWEmv5GdCD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BkwwD6nWEmv5GdCD .error-icon{fill:#552222;}#mermaid-svg-BkwwD6nWEmv5GdCD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BkwwD6nWEmv5GdCD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BkwwD6nWEmv5GdCD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BkwwD6nWEmv5GdCD .marker.cross{stroke:#333333;}#mermaid-svg-BkwwD6nWEmv5GdCD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BkwwD6nWEmv5GdCD p{margin:0;}#mermaid-svg-BkwwD6nWEmv5GdCD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster-label text{fill:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster-label span{color:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster-label span p{background-color:transparent;}#mermaid-svg-BkwwD6nWEmv5GdCD .label text,#mermaid-svg-BkwwD6nWEmv5GdCD span{fill:#333;color:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD .node rect,#mermaid-svg-BkwwD6nWEmv5GdCD .node circle,#mermaid-svg-BkwwD6nWEmv5GdCD .node ellipse,#mermaid-svg-BkwwD6nWEmv5GdCD .node polygon,#mermaid-svg-BkwwD6nWEmv5GdCD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BkwwD6nWEmv5GdCD .rough-node .label text,#mermaid-svg-BkwwD6nWEmv5GdCD .node .label text,#mermaid-svg-BkwwD6nWEmv5GdCD .image-shape .label,#mermaid-svg-BkwwD6nWEmv5GdCD .icon-shape .label{text-anchor:middle;}#mermaid-svg-BkwwD6nWEmv5GdCD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BkwwD6nWEmv5GdCD .rough-node .label,#mermaid-svg-BkwwD6nWEmv5GdCD .node .label,#mermaid-svg-BkwwD6nWEmv5GdCD .image-shape .label,#mermaid-svg-BkwwD6nWEmv5GdCD .icon-shape .label{text-align:center;}#mermaid-svg-BkwwD6nWEmv5GdCD .node.clickable{cursor:pointer;}#mermaid-svg-BkwwD6nWEmv5GdCD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BkwwD6nWEmv5GdCD .arrowheadPath{fill:#333333;}#mermaid-svg-BkwwD6nWEmv5GdCD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BkwwD6nWEmv5GdCD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BkwwD6nWEmv5GdCD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkwwD6nWEmv5GdCD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BkwwD6nWEmv5GdCD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkwwD6nWEmv5GdCD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster text{fill:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD .cluster span{color:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD 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-BkwwD6nWEmv5GdCD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BkwwD6nWEmv5GdCD rect.text{fill:none;stroke-width:0;}#mermaid-svg-BkwwD6nWEmv5GdCD .icon-shape,#mermaid-svg-BkwwD6nWEmv5GdCD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkwwD6nWEmv5GdCD .icon-shape p,#mermaid-svg-BkwwD6nWEmv5GdCD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BkwwD6nWEmv5GdCD .icon-shape .label rect,#mermaid-svg-BkwwD6nWEmv5GdCD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkwwD6nWEmv5GdCD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BkwwD6nWEmv5GdCD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BkwwD6nWEmv5GdCD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户查询
AnnSearchRequest 1
filmVector / L2
AnnSearchRequest 2
posterVector / COSINE
融合重排
WeightedRanker / RRFRanker
最终 Top-K
注意:hybrid_search(多向量混合检索)和 search 的批量查询是两件事------后者是"同一向量字段、多个查询向量",前者是"多个向量字段、各自一路检索"。
两种重排策略怎么选
| 策略 | 核心思想 | 什么时候用 |
|---|---|---|
| WeightedRanker | 给每个向量字段分配权重 | 你明确知道某一路更重要(如多模态搜索里,文字描述比颜色更重要) |
| RRFRanker | 按排名位置倒数融合,不看原始分数 | 你没有特定偏好,希望平衡各字段 |
WeightedRanker :权重个数 = ANN 搜索路数,取值范围 [0, 1],越接近 1 越重要。
python
from pymilvus import WeightedRanker
ranker = WeightedRanker(0.8, 0.3) # 两路搜索,分别给 0.8 / 0.3
RRFRanker (倒数排序融合):按每个结果在各自检索列表中的排名计算分数:
s c o r e ( d ) = ∑ i = 1 N 1 k + r a n k i ( d ) score(d) = \sum_{i=1}^{N} \frac{1}{k + rank_i(d)} score(d)=i=1∑Nk+ranki(d)1
| 符号 | 含义 |
|---|---|
| d d d | 文档(实体) |
| N N N | 不同检索路径的数量 |
| r a n k i ( d ) rank_i(d) ranki(d) | 文档 d d d 在第 i i i 个检索器中的排名(从 0 开始) |
| k k k | 平滑参数,控制分数随排名下降的速度,默认 60 |
python
from pymilvus import RRFRanker
ranker = RRFRanker(100) # k = 100;常用默认是 60
💡 RRF 的最大好处是"免疫分数尺度" :不同度量返回的分数范围完全不同(L2 是
[0,∞)、COSINE 是[-1,1]),直接加权没有可比性;而排名是可比的,所以 RRF 在多度量混合时更稳。
完整代码
python
from pymilvus import AnnSearchRequest, RRFRanker
# ① 第一路:filmVector,L2 度量
req1 = AnnSearchRequest(
data=[[0.889, 0.371, 0.238, 0.382, 0.600]],
anns_field="filmVector", # 必须与 Schema 中的字段名一致
param={"metric_type": "L2", "nprobe": 10},
limit=2,
)
# ② 第二路:posterVector,COSINE 度量
req2 = AnnSearchRequest(
data=[[0.025, 0.006, 0.533, 0.768, 0.552]],
anns_field="posterVector",
param={"metric_type": "COSINE"},
limit=2,
)
# ③ 融合检索
res = client.hybrid_search(
collection_name="demo_v3",
reqs=[req1, req2],
ranker=RRFRanker(100),
limit=2,
output_fields=["film_id"],
)
for hits in res:
for hit in hits:
print(hit)
📌
param里的metric_type必须与建索引时一致 (anns_field同理),否则会报错------这是混合检索最常见的失败原因。
九、常见问题 FAQ
Q1:MilvusClient("milvus_demo.db") 在 Windows 上直接报错?
✅ 因为 Milvus Lite 不支持 Windows(仅 Linux / macOS)。用 Docker 起 Standalone,或走 WSL2 / Zilliz Cloud。
Q2:我明明指定了 IVF_FLAT / HNSW,为什么索引类型显示是 FLAT?
✅ 因为 Milvus Lite 只支持 FLAT ,无论你传什么,实际都按 FLAT 执行。需要真实索引能力请用 Standalone / Distributed。
Q3:create_partition 报错 / partition_name 参数无效?
✅ Milvus Lite 不支持分区 。分区、用户角色、别名、rename_collection 都是 Lite 的能力缺口。
Q4:search 返回的 distance 越大越好还是越小越好?
✅ 看度量 :L2 越小越相似 ;IP / COSINE 越大越相似 。另外 L2 返回的是平方距离(没开方),但排序不受影响。
Q5:enable_dynamic_field=True 有什么副作用?
✅ 未定义的字段会被塞进 $meta 这个 JSON 字段。开发期方便,生产期建议关掉------否则字段类型失控、过滤性能下降。
Q6:auto_id=True 之后还能自己指定主键吗?
✅ 不能。 auto_id=True 时主键由系统分配,插入时传了也会被忽略或报错。需要自己控制 ID(比如和业务表对齐)就设 auto_id=False。
Q7:query 和 search 到底用哪个?
✅ 按标量条件 筛数据 → query;按向量相似度 找 Top-K → search。两者可以组合:search 里带 filter。
Q8:混合检索报 metric type not match?
✅ AnnSearchRequest 里的 metric_type 必须与该字段建索引时 的度量一致,anns_field 也必须与 Schema 字段名完全一致。
Q9:nlist / nprobe / M / ef 这些参数怎么调?
✅ 记住一个原则:这些参数都是"精度 ↔ 速度"的旋钮,往"更准"的方向调都更慢。
nlist(建索引):簇数,通常按4 × sqrt(数据量)左右估,常见 128~4096;nprobe(查询):搜几个簇,从 8~16 起调,按召回率找平衡;M/efConstruction(HNSW 建索引):越大越准越占内存;ef(HNSW 查询):必须 ≥limit,越大越准越慢。
Q10:数据量到多少该换掉 Lite?
✅ Lite 官方定位是小规模 (笔记本、Jupyter、边缘设备)。到百万级或需要多用户/分区/真实索引时,就应该迁到 Standalone 。Lite 的数据可以用 milvus-lite dump 导出成 JSON,再导入其他部署。
Q11:pip install pymilvus 装到 3.x,连 2.6 服务端会有问题吗?
✅ 有可能。客户端版本应与服务端对齐 :3.0 服务端配 pymilvus 3.x;2.6 服务端请显式锁 pymilvus~=2.6.17。
十、本章小结
| 主题 | 一句话记住 |
|---|---|
| 定位 | Milvus 存向量 ,MySQL 存字段;前者做"像不像",后者做"是不是" |
| 概念链 | 非结构化数据 → 嵌入向量 → 相似度搜索 → Collection/Field |
| 数据结构 | Collection ≈ 表,Field ≈ 列,只能有一个主键 |
| 索引 | 不指定就默认 FLAT(暴力搜索) ;IVF 系靠 nlist/nprobe 调精度,HNSW 靠图 |
| 度量 | L2 越小越像,IP/COSINE 越大越像;归一化后三者排序等价 |
| 操作 | create_collection → insert → search / query → upsert / delete → drop_collection |
| 混合搜索 | 多向量字段 + hybrid_search + WeightedRanker / RRFRanker |
| 避坑 | Lite 不支持 Windows / 分区 / 真实索引;pymilvus 要和服务端版本对齐 |
一条实践建议 :先用 Milvus Lite 把业务逻辑(嵌入 → 入库 → 检索 → 重排)跑通,等数据量和并发上来了再迁 Standalone 。因为两者共用同一套 API ,迁移成本主要在网络与运维侧,代码几乎不用改。
📚 参考资料与数据说明
- Milvus 官方文档 :https://milvus.io/docs
- 度量类型(Metric Types) :https://milvus.io/docs/v2.6.x/metric.md
- 索引参考 :https://www.milvus-io.com/reference/index
- Milvus Lite 说明与限制 :https://milvus.io/docs/zh/v2.4.x/milvus_lite.md
- PyMilvus(Python SDK) :https://pypi.org/project/pymilvus/
- GitHub :https://github.com/milvus-io/milvus
版本与数据说明 :本文概念与示例基于 EduRAG V7.5 讲义 2.3 节 整理并重写,API 细节与限制项已对照 Milvus 官方文档(2026-10 检索) 核对。服务端版本现状:Milvus 3.0(GA 线) 与 2.6(保守线) 并行;PyMilvus 3.0.1 要求 Python 3.9+ ;Milvus Lite 仅支持 Linux/macOS 。Milvus 迭代较快,索引上限、Lite 支持范围等细节请以你所用版本的官方文档为准。
如果这篇对你有帮助,欢迎点赞 + 收藏。需要的话我可以继续整理这个系列的 Ollama 本地模型部署 、LangChain 接入 Milvus 构建 RAG ,或者从 Milvus Lite 迁移到 Standalone 的实操步骤------评论区告诉我。