数据库基础体系 · 第 62/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Milvus 完整基础:Collection、Segment、索引、查询和集群部署

Milvus 是面向向量检索的数据库系统。它不仅保存向量,还负责标量字段管理、过滤、索引构建、数据可见性、持久化、查询节点扩展和故障恢复。

理解 Milvus,不能只记住“把向量插入数据库,再调用搜索接口”。至少需要建立下面这条因果链:

  1. Collection 定义数据模型;
  2. 插入数据后形成不同生命周期的 Segment
  3. 对已封存的数据构建向量 索引
  4. 查询节点同时处理新数据和已索引数据;
  5. 通过一致性级别、时间戳和加载状态决定查询能看到什么;
  6. 在独立模式或集群模式中,由不同组件完成协调、存储、索引和查询。

下文以 Milvus 2.x 官方公开语义为基础。具体参数和支持的索引类型应以所安装版本的官方文档为准,尤其是 Milvus Lite、Standalone 和 Distributed 的能力并不完全相同。


一、先建立模型:Milvus 中到底存储什么

一个典型的向量数据集可以表示为:

xi=(idi,vi,mi)x_i = (id_i, v_i, m_i)

其中:

  • idiid_i 是主键;
  • viRdv_i \in \mathbb{R}^ddd 维向量;
  • mim_i 是若干标量元数据,例如文档类型、租户、时间、权限标签。

一次向量搜索通常是:

TopK(q,D,F)\operatorname{TopK}(q, D, F)

其中:

  • qq 是查询向量;
  • DD 是 Collection 中的数据;
  • FF 是标量过滤条件;
  • 返回与 qq 最相似的 KK 条记录。

Milvus 的职责不是简单执行一个 SQL ORDER BY distance LIMIT K。它还要处理:

  • 数据仍在内存中、尚未形成稳定文件的情况;
  • 已经封存但尚未完成索引的 Segment;
  • 删除标记对搜索结果的影响;
  • 多个 Segment 的局部 TopK 合并;
  • 查询请求应该看到哪个时间点的数据;
  • 分布式节点之间的结果合并与故障恢复。

二、Collection:逻辑数据集和 Schema

2.1 Collection 是什么

Collection 类似关系数据库中的表,但它不是关系表的直接替代品。

Collection 定义:

  • 字段名称和字段类型;
  • 主键;
  • 向量字段的维度;
  • 是否允许动态字段;
  • 可选的分区键、分区和其他集合级属性。

常见字段类型包括:

  • INT8INT16INT32INT64
  • FLOATDOUBLE
  • BOOL
  • VARCHAR
  • JSON
  • ARRAY
  • FLOAT_VECTOR
  • BINARY_VECTOR
  • 某些版本支持的其他向量类型

对于浮点向量,必须指定维度。例如:

v=[v1,v2,,v768]v = [v_1,v_2,\ldots,v_{768}]

则该字段的维度必须是 768。插入 767 维或 769 维向量都会失败。

主键可以是整数或字符串。auto_id=True 时,主键由 Milvus 生成;否则客户端必须提供主键。

2.2 一个可运行的 Collection 示例

下面示例使用 PyMilvus 的 MilvusClient 接口。它采用 Milvus Lite 的本地文件方式,适合学习 API 生命周期;生产集群应把 uri 换成服务器地址,并配置认证信息。

安装客户端:

python -m pip install -U pymilvus

完整示例:

from pymilvus import MilvusClient, DataType

client = MilvusClient(uri="./milvus_demo.db")

schema = client.create_schema(
    auto_id=False,
    enable_dynamic_field=False,
)

schema.add_field(
    field_name="id",
    datatype=DataType.INT64,
    is_primary=True,
)
schema.add_field(
    field_name="title",
    datatype=DataType.VARCHAR,
    max_length=256,
)
schema.add_field(
    field_name="tenant_id",
    datatype=DataType.INT64,
)
schema.add_field(
    field_name="embedding",
    datatype=DataType.FLOAT_VECTOR,
    dim=4,
)

collection_name = "documents"

if client.has_collection(collection_name):
    client.drop_collection(collection_name)

client.create_collection(
    collection_name=collection_name,
    schema=schema,
)

这里每一步都有明确约束:

  • id 是主键,因此每条记录必须唯一;
  • title 是字符串,并且长度不能超过 max_length
  • tenant_id 可以用于标量过滤;
  • embedding 是四维浮点向量;
  • enable_dynamic_field=False 表示未声明的字段不能随意插入。

如果启用动态字段,未声明字段通常会进入特殊的动态字段结构。动态字段适合元数据经常变化的场景,但会降低 Schema 的显式约束能力,也可能使过滤和字段管理更复杂。稳定的数据模型通常应优先声明字段。

2.3 主键不是向量内容

主键只负责标识记录,不代表向量相似度。

下面两条记录的主键可以是任意不连续的整数:

rows = [
    {
        "id": 101,
        "title": "Milvus introduction",
        "tenant_id": 1,
        "embedding": [1.0, 0.0, 0.0, 0.0],
    },
    {
        "id": 205,
        "title": "Vector indexing",
        "tenant_id": 1,
        "embedding": [0.9, 0.1, 0.0, 0.0],
    },
]

主键 101205 的大小关系不会影响向量距离。

2.4 Insert、Upsert、Delete 和事务边界

插入:

result = client.insert(
    collection_name=collection_name,
    data=rows,
)
print(result)

insert 的语义是写入新实体。若主键已经存在,不能把它当作关系数据库中的普通更新语句来理解。

更新通常使用 upsert

client.upsert(
    collection_name=collection_name,
    data=[
        {
            "id": 101,
            "title": "Milvus introduction, revised",
            "tenant_id": 1,
            "embedding": [1.0, 0.0, 0.0, 0.0],
        }
    ],
)

upsert 的具体行为和限制应结合所用版本确认。工程上应明确主键写入策略,不要同时让多个业务流程无条件修改同一个主键。

删除可以按主键:

client.delete(
    collection_name=collection_name,
    ids=[205],
)

也可以按过滤表达式:

client.delete(
    collection_name=collection_name,
    filter="tenant_id == 1",
)

重要边界是:Milvus 不是提供通用多语句事务的关系数据库。以下操作不能自然地组成一个跨步骤 ACID 事务:

  1. 插入一批向量;
  2. 更新一批元数据;
  3. 删除另一批记录;
  4. 再修改一个外部业务数据库。

即使某次批量写入作为一个请求提交,也不能据此推断跨请求、跨 Collection、跨外部系统具备事务原子性。需要事务语义时,应在业务层设计幂等键、写入状态、补偿逻辑和版本字段。


三、Segment:Milvus 如何组织数据

3.1 Segment 的定义

Segment 是 Milvus 管理数据的一种物理组织单元。一个 Collection 的数据不会始终以一个整体文件存在,而会被划分为多个 Segment。

常见生命周期可以抽象为:

insert
  ↓
growing segment
  ↓ seal
sealed segment
  ↓ flush / 持久化
持久化的 sealed segment
  ↓ index build
带向量索引的 sealed segment

不同版本的内部状态和触发条件可能不同,但理解这三个概念最重要:

  • Growing Segment:仍可能接收新写入,通常由查询节点以实时数据结构处理;
  • Sealed Segment:不再接收新写入,可以进行稳定存储和索引构建;
  • Indexed Segment:已针对某个向量字段构建索引,查询时可使用该索引。

Segment 不是业务上的分区,也不等同于 Collection 的 partition。Segment 通常由系统根据写入和调度策略自动管理,业务不应依赖某个 Segment 的编号或当前数量。

3.2 为什么不能每次插入都直接构建全局索引

假设 Collection 已有 NN 条向量,每次新增一条就重建全局索引,成本可能接近:

O(Nd)O(Nd)

其中 dd 是向量维度。随着 NN 增大,这种方式不可行。

Milvus 通常让新数据先进入 Growing Segment,达到条件后封存为 Sealed Segment,再在 Segment 级别构建索引。查询时:

  1. 对已有索引的 Sealed Segment 查询;
  2. 对尚未索引的 Sealed Segment 使用原始数据或临时结构;
  3. 对 Growing Segment 查询实时数据;
  4. 合并各 Segment 的候选结果;
  5. 应用删除标记和过滤条件;
  6. 返回全局 TopK。

这解释了一个常见现象:刚插入的数据可以在某种一致性设置下被查询到,但它不一定已经拥有与历史数据相同的持久化索引。

3.3 Flush 的含义和误解

flush 的作用是推动当前可持久化的数据形成稳定的存储状态,并触发后续处理。它不应被理解为:

  • “立刻完成所有索引”;
  • “立刻让所有客户端在任意一致性级别下都看到数据”;
  • “把多个业务请求组成事务”。

在分布式部署中,写入通常经过消息流和对象存储等组件,索引构建也可能是异步的。因此:

insert 成功
≠ index 已完成
≠ 所有 QueryNode 已加载新索引
≠ 所有客户端立即可见

如果业务在导入完成后需要确认状态,应显式检查导入、Flush、索引和加载状态,而不是用固定睡眠时间猜测。

3.4 Compaction 和删除

Milvus 的删除通常不是立即改写所有向量文件,而是写入删除标记。查询时,删除标记会参与过滤;后台 Compaction 再把多个小 Segment 合并,清理已经无效的数据。

因此删除可能经历:

delete request
  ↓
产生删除标记
  ↓
查询路径过滤被删除实体
  ↓
后台 Compaction
  ↓
生成更紧凑的新 Segment

删除后立即观察底层对象存储文件,不能据此判断删除失败。反过来,如果删除量很大、Compaction 长时间积压,也可能导致存储空间和查询开销增加。


四、向量距离:索引和查询必须使用一致的度量

常见度量包括欧氏距离、内积和余弦相似度。

4.1 欧氏距离

dL2(x,y)=i=1d(xiyi)2d_{\text{L2}}(x,y)=\sqrt{\sum_{i=1}^{d}(x_i-y_i)^2}

距离越小越相似。

4.2 内积

sIP(x,y)=i=1dxiyis_{\text{IP}}(x,y)=\sum_{i=1}^{d}x_i y_i

分数越大越相似。若向量没有归一化,向量长度也会影响内积结果。

4.3 余弦相似度

scos(x,y)=xyx2y2s_{\cos}(x,y)= \frac{x\cdot y}{\|x\|_2\|y\|_2}

它主要比较方向。若先把向量归一化为单位向量,则:

x=y=1\|x\|=\|y\|=1

此时有:

xy22=22(xy)\|x-y\|_2^2=2-2(x\cdot y)

所以单位向量上的 L2 排序与内积排序等价,但返回值的含义和数值范围仍不同。

4.4 一个完整算例

查询向量:

q=(1,0)q=(1,0)

候选向量:

a=(1,0),b=(0.8,0.6),c=(1,0)a=(1,0),\quad b=(0.8,0.6),\quad c=(-1,0)

它们都已归一化。

余弦相似度为:

  • qa=1q\cdot a=1
  • qb=0.8q\cdot b=0.8
  • qc=1q\cdot c=-1

所以排序为:

a > b > c

L2 距离平方为:

  • d2(q,a)=0d^2(q,a)=0
  • d2(q,b)=22×0.8=0.4d^2(q,b)=2-2\times0.8=0.4
  • d2(q,c)=4d^2(q,c)=4

排序仍然是:

a < b < c

反例是未归一化向量:

a=(1,0),b=(10,1)a=(1,0),\quad b=(10,1)

内积更偏好长度大的向量,而余弦相似度主要看方向。若模型训练和离线评测使用余弦相似度,在线却用未归一化向量的内积,结果可能系统性偏离。

索引创建时使用的 metric_type 必须与搜索时一致,向量预处理也必须一致。


五、向量索引:从精确搜索到近似搜索

5.1 FLAT:精确但需要扫描

FLAT 不建立近似结构。对每个查询向量,计算它与候选集合中所有向量的距离,再取 TopK。

若有 NNdd 维向量,单查询计算量近似为:

O(Nd)O(Nd)

它的优点:

  • 结果可作为召回率基准;
  • 不需要调节近似参数;
  • 小数据集通常足够快。

缺点是数据规模增大后延迟和 CPU 成本线性增长。

5.2 IVF:先聚类,再搜索部分倒排列表

IVF 先通过聚类把数据分成 nlistn_{\text{list}} 个簇。查询时:

  1. 计算查询向量与各簇中心的距离;
  2. 选择最近的 nproben_{\text{probe}} 个簇;
  3. 只扫描这些簇内的向量;
  4. 合并候选结果。

nprobe=nlistn_{\text{probe}}=n_{\text{list}},它接近全量扫描;若 nproben_{\text{probe}} 很小,速度更快,但可能漏掉真实近邻。

核心取舍是:

nlist 增大:
  簇更细,单簇候选可能更少;
  训练和管理成本增加。

nprobe 增大:
  检查更多簇;
  召回率通常提高,延迟也增加。

IVF_FLAT 保留原始浮点向量,因此精度主要由候选簇裁剪造成。

5.3 PQ:压缩向量以减少内存和计算

Product Quantization 将一个向量拆成若干子空间,再用较小的码本表示每个子向量。

设:

x=(x(1),x(2),,x(m))x=(x^{(1)},x^{(2)},\ldots,x^{(m)})

每个子向量由一个码字近似:

x(j)ckj(j)x^{(j)}\approx c^{(j)}_{k_j}

最终只保存码字编号 kjk_j,而不是完整的浮点数。

优点:

  • 显著降低内存;
  • 适合大规模向量;
  • 可以利用压缩码快速估算距离。

缺点:

  • 产生量化误差;
  • 参数和训练数据分布敏感;
  • 精度下降不一定能通过简单调大搜索参数完全恢复。

IVF_PQ 同时有两种近似:

  1. IVF 只搜索部分簇;
  2. PQ 用近似码表示向量。

因此需要分别评估 nprobe 和 PQ 参数的影响。

5.4 HNSW:图搜索

HNSW 为向量建立多层近邻图。查询从较高层开始快速接近目标区域,再逐层下降,在底层图上进行更精细的候选搜索。

常见参数含义:

  • M:每个节点维护的邻居数量;
  • efConstruction:构建图时的候选搜索宽度;
  • ef:查询时的候选搜索宽度。

通常:

M 增大:
  图更密;
  内存和构建成本增加;
  可能改善召回率。

efConstruction 增大:
  建图更慢;
  图质量通常更好。

ef 增大:
  查询更慢;
  召回率通常更高。

HNSW 并不是“永远最快”。它通常需要较多内存,并且高写入、频繁删除、超大规模数据时,需要结合 Segment、Compaction 和资源配置评估。

5.5 DiskANN 和自动索引

某些 Milvus 版本和部署环境还支持 DiskANN 等面向磁盘或大规模数据的索引。它们对硬件、文件系统、内存、磁盘和版本有更强依赖。

AUTOINDEX 的含义是让 Milvus 根据配置和数据情况选择索引策略,而不是一个可以脱离版本和部署环境独立解释的算法名称。使用自动索引时,仍然要通过离线基准确认:

  • Recall@K;
  • p95/p99 延迟;
  • 内存和磁盘使用;
  • 构建时间;
  • 写入期间的资源竞争。

5.6 召回率不能只看理论

用 FLAT 作为精确基线。对同一批查询,计算近似索引返回集合 AKA_K 与精确 TopK 集合 EKE_K

Recall@K=AKEKK\operatorname{Recall@K} = \frac{|A_K\cap E_K|}{K}

例如精确结果是:

[10, 11, 12, 13, 14]

近似结果是:

[10, 11, 12, 20, 21]

则:

Recall@5=35=0.6\operatorname{Recall@5}=\frac{3}{5}=0.6

反例是只测平均延迟:平均延迟可能很好,但少数租户、少数过滤条件或高并发下的 p99 延迟和召回率明显恶化。因此索引参数必须和真实查询分布一起评测。


六、创建索引、加载 Collection 和执行搜索

6.1 为向量字段创建 HNSW 索引

接着前面的代码,插入数据后创建索引:

index_params = MilvusClient.prepare_index_params()

index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={
        "M": 16,
        "efConstruction": 200,
    },
)

client.create_index(
    collection_name=collection_name,
    index_params=index_params,
)

这里:

  • M=16 控制图的邻居规模;
  • efConstruction=200 控制构建阶段的搜索宽度;
  • COSINE 必须与业务向量和查询方式一致。

并非所有部署模式都支持完全相同的索引集合。若当前版本或 Milvus Lite 不支持 HNSW,可使用该环境支持的索引类型,或先用 FLAT 验证数据和查询逻辑。

6.2 Load 的意义

创建索引并不等于查询节点已经可以使用它。通常还需要加载 Collection:

client.load_collection(collection_name)

可以把它理解为:

对象存储中的数据和索引
        ↓
QueryNode 加载到查询所需的内存或缓存
        ↓
查询服务可用

如果 Collection 未加载,常见失败表现包括:

  • 查询报 Collection 未加载;
  • 查询超时;
  • 只有部分数据可见;
  • 加载内存不足;
  • 索引构建完成但搜索性能没有改善。

修改索引或数据后,加载状态和加载版本可能需要重新确认。不要把 create_index 的成功返回直接解释为所有 QueryNode 已完成加载。

6.3 向量搜索

search_result = client.search(
    collection_name=collection_name,
    data=[[1.0, 0.0, 0.0, 0.0]],
    anns_field="embedding",
    limit=2,
    filter='tenant_id == 1',
    output_fields=["title", "tenant_id"],
    search_params={
        "metric_type": "COSINE",
        "params": {
            "ef": 64,
        },
    },
)

for hits in search_result:
    for hit in hits:
        print(
            "id=", hit["id"],
            "distance=", hit["distance"],
            "entity=", hit.get("entity"),
        )

输入是一条四维查询向量,limit=2 表示返回两个结果。ef=64 是 HNSW 查询阶段的搜索宽度;如果使用 IVF,搜索参数应改为相应的 nprobe 等参数,不能混用。

搜索流程可以形式化为:

Rj=TopK(q,Sj,F)R_j = \operatorname{TopK}(q,S_j,F)

其中 SjS_j 是第 jj 个 Segment。最终结果是:

R=TopK(q,jRj,F)R = \operatorname{TopK}\left(q,\bigcup_j R_j,F\right)

只取每个 Segment 的局部 TopK 通常足以完成全局 TopK 合并,但过滤条件和删除标记必须在正确阶段处理,否则可能出现结果数量不足或结果错误。

6.4 标量 Query

如果不需要向量相似度,只需要按主键或标量条件读取数据,可以使用 query

rows = client.query(
    collection_name=collection_name,
    filter="tenant_id == 1",
    output_fields=["id", "title", "tenant_id"],
    limit=10,
)

for row in rows:
    print(row)

querysearch 的差异是:

  • query:按过滤条件读取实体;
  • search:按向量距离排序;
  • 混合检索:先进行向量搜索,再用标量条件约束候选范围。

如果业务需要全文、稀疏向量和稠密向量组合检索,还应使用对应版本支持的混合检索能力,不应把所有搜索都简化为一个 Dense Vector 字段。


七、数据可见性:一致性级别不等于事务隔离

7.1 为什么刚写入的数据不一定立即可见

分布式 Milvus 中,写入和查询由不同节点及不同内部组件处理。写入请求可能已经被接受,但查询节点还没有追上对应的数据时间戳。

因此必须区分:

  • 写入请求是否成功;
  • 数据是否已经持久化;
  • 数据是否已经被索引;
  • 查询节点是否已加载;
  • 当前查询的一致性级别允许看到哪个时间点。

7.2 常见一致性语义

Milvus 提供多种一致性级别,常见概念包括:

  • Strong:尽量读取最新可见数据,延迟和系统协调成本通常更高;
  • Session:同一会话中保证相应的写后读语义;
  • Bounded:允许一定时间延迟,在新鲜度和性能之间折中;
  • Eventually:不保证立即可见,延迟通常更低。

具体默认值和客户端参数名称应以版本文档为准。

一致性级别解决的是“查询可以看到哪个时间点的数据”,不是完整事务隔离。例如:

事务 A:
  插入向量
  更新外部订单状态
  查询 Collection

不能仅因为选择了 Strong,就推断外部订单状态和 Milvus 数据处于同一个事务快照中。

7.3 数据版本和删除的反例

如果一个实体刚刚被删除,而查询使用较弱的一致性级别,短时间内仍可能看到旧版本结果。若业务有严格的撤回要求,应:

  1. 设计业务版本号或有效时间;
  2. 查询时附加版本过滤;
  3. 在需要的位置使用更强的一致性;
  4. 对删除后的可见性进行实际验证。

不要只依赖“删除接口返回成功”。


八、Collection、Partition、Segment 的区别

这三个概念经常被混淆。

Collection

逻辑数据集和 Schema 的边界,类似一张面向向量的表。

Partition

Collection 内部的逻辑子集。可以按业务规则把数据写入不同 Partition,并在查询时指定 Partition。Partition 适合有明确路由条件的场景,例如时间范围或租户分组,但过多 Partition 会增加管理和查询调度复杂度。

Segment

系统内部的物理数据块,由写入、封存、Flush、Compaction 和索引流程产生。

关系可以表示为:

Collection
  ├── Partition A
  │     ├── Segment 1
  │     └── Segment 2
  └── Partition B
        ├── Segment 3
        └── Segment 4

Partition 是用户可控制的逻辑组织;Segment 是系统调度的物理组织。不要把每个租户创建成一个 Collection,也不要为了模拟分片而手工依赖 Segment。


九、Milvus 的组件和数据流

9.1 逻辑组件

Milvus 分布式架构通常包含以下职责:

  • Proxy:接收客户端请求,负责路由、校验和部分结果协调;
  • RootCoord:管理 Collection、Schema、数据库和全局元数据;
  • DataCoord:管理数据 Segment、Flush、Compaction 和数据状态;
  • QueryCoord:管理查询节点、Segment 分配和查询负载;
  • IndexCoord:协调索引构建任务;
  • DataNode:处理写入数据流并生成数据文件;
  • QueryNode:加载 Segment、执行向量搜索和标量过滤;
  • IndexNode:执行索引构建任务;
  • etcd:保存元数据和协调状态;
  • 对象存储:保存持久化数据、索引和日志文件;
  • 消息存储:传递写入和时间戳相关的数据流。

不同发行版和版本可能把某些能力合并、拆分或交给外部组件,但职责划分大致如此。

9.2 写入路径

一次写入可以抽象为:

Client
  ↓
Proxy
  ↓
消息存储 / 写入协调
  ↓
DataNode
  ↓
Growing Segment
  ↓ seal
对象存储中的持久化文件
  ↓
IndexNode 构建索引
  ↓
QueryNode 加载

写入成功返回后,后台还可能继续进行:

  • Segment 封存;
  • 数据持久化;
  • 索引构建;
  • QueryNode 加载;
  • Compaction。

9.3 查询路径

查询通常经过:

Client
  ↓
Proxy
  ↓
QueryCoord 分配查询任务
  ↓
QueryNode
  ├── Growing Segment
  ├── Sealed Segment
  ├── 向量索引
  └── 标量过滤 / 删除标记
  ↓
局部结果合并
  ↓
Proxy 返回全局 TopK

查询延迟不仅取决于索引算法,还取决于:

  • Segment 数量;
  • 是否有大量 Growing Segment;
  • Filter 选择性;
  • QueryNode 是否发生缓存未命中;
  • 并发和结果合并;
  • Compaction、索引构建与查询之间的资源竞争。

十、Standalone、Milvus Lite 和 Distributed

10.1 Milvus Lite

Milvus Lite 适合:

  • 本地开发;
  • 单进程测试;
  • 小规模离线实验;
  • 验证 API 和数据模型。

它不等于生产分布式集群。它没有独立的 Proxy、QueryNode、DataNode 集群,也不提供分布式部署所需的高可用边界。

前面的:

client = MilvusClient(uri="./milvus_demo.db")

就是本地模式示例。它的主要价值是降低学习成本,而不是模拟生产故障和扩容行为。

10.2 Standalone

Standalone 通常把 Milvus 运行在单机或单实例环境中,适合:

  • 开发测试;
  • 中小规模部署;
  • 不需要横向扩展的环境。

它仍然依赖持久化和协调组件的正确配置。单实例意味着关键服务故障可能直接影响整个服务,不应把 Standalone 当作自动高可用方案。

10.3 Distributed

Distributed 模式将协调、数据、索引和查询能力拆分为可扩展组件。适合:

  • 大规模向量数据;
  • 高并发查询;
  • 持续写入和查询同时发生;
  • 需要独立扩展 QueryNode、DataNode 或 IndexNode;
  • 需要多实例故障容忍。

典型部署依赖:

Milvus 服务
  ├── Proxy
  ├── RootCoord
  ├── DataCoord
  ├── QueryCoord
  ├── IndexCoord
  ├── QueryNode
  ├── DataNode
  └── IndexNode

外部基础设施
  ├── etcd
  ├── 对象存储
  └── Kafka / Pulsar 等消息存储

使用 Kubernetes Helm 部署时,命令和 values 会随 Chart 版本变化。应先查看当前 Chart 的参数:

helm repo add milvus https://milvus-io.github.io/milvus-helm/
helm repo update

helm show values milvus/milvus > milvus-values.yaml

确认当前 Chart 对 Cluster、对象存储、消息存储、持久卷和资源限制的配置方式后,再安装。例如某些版本支持:

helm install milvus-cluster milvus/milvus \
  --namespace milvus \
  --create-namespace \
  --set cluster.enabled=true

这条命令是否足够,取决于 Chart 默认值和外部依赖配置。生产部署不能仅复制命令,还必须检查:

kubectl get pods -n milvus
kubectl get svc -n milvus
kubectl get pvc -n milvus
kubectl describe pods -n milvus
kubectl logs -n milvus <pod-name>

<pod-name> 是实际查询结果中的 Pod 名称,执行时必须替换为真实名称。

10.4 部署时的故障边界

QueryNode 故障

通常可以重新调度查询任务并重新加载 Segment,但期间可能出现:

  • 查询延迟升高;
  • 可用副本不足;
  • 加载内存不足;
  • 某些请求失败或超时。

DataNode 故障

影响写入处理和数据落盘进度。若消息存储和持久化配置正确,已确认的数据通常可以通过日志和对象存储恢复;但未完成提交或外部依赖异常时,需要依据实际状态确认。

IndexNode 故障

通常影响索引构建进度,不一定影响已经加载完成的历史索引查询。新 Segment 可能暂时只能走未索引路径,导致查询延迟增加。

etcd 故障

会影响元数据和协调状态,是严重故障。必须为 etcd 配置持久化和高可用,而不是把它当作普通临时 Pod。

对象存储故障

会影响数据、索引和恢复能力。对象存储不是可选的“缓存”,其可用性直接影响持久化数据。

消息存储故障

会影响写入流、时间戳推进和部分实时查询语义。消息积压还可能造成数据延迟、Segment 封存延迟和资源压力。


十一、生产查询中的过滤、权限和多租户

向量相似并不等于用户有权读取。

例如一条文档可能与查询向量高度相似,但属于另一个租户。正确查询应把租户条件放入过滤表达式:

client.search(
    collection_name="documents",
    data=[[1.0, 0.0, 0.0, 0.0]],
    anns_field="embedding",
    limit=10,
    filter="tenant_id == 42 and doc_type == 'manual'",
    output_fields=["id", "title", "tenant_id"],
    search_params={
        "metric_type": "COSINE",
        "params": {"ef": 64},
    },
)

但过滤能否达到理想性能,取决于:

  • 过滤字段是否存在;
  • 条件选择性;
  • Segment 数据分布;
  • 版本和索引类型对过滤的执行方式;
  • 向量候选和标量过滤的先后路径。

安全边界仍应放在服务端:

  • Milvus 认证和网络访问控制;
  • Collection 或数据库级权限;
  • 应用层租户校验;
  • 不允许客户端任意修改 tenant_id
  • output_fields 做白名单控制。

不要只依赖客户端传入的 tenant_id 过滤条件。若调用方可以自由改写过滤表达式,过滤就不是权限控制。


十二、导入、版本和备份的实际边界

12.1 批量导入与在线写入不同

少量实时数据适合 insert。大规模离线数据通常应考虑批量导入机制,以减少客户端逐条请求、降低网络往返和写入调度压力。

批量导入完成后仍需确认:

  1. 导入任务状态;
  2. Segment 是否生成;
  3. 索引是否完成;
  4. Collection 是否加载;
  5. 召回率和查询延迟是否符合预期。

“导入任务成功”不自动等于“在线查询已经使用新索引”。

12.2 数据版本必须由业务管理

Embedding 模型升级后,同一文本可能产生完全不同的向量空间。旧向量和新向量直接混搜通常没有明确的几何意义。

可以使用版本字段:

embedding_model = "model-v2"
embedding_dim = 768
embedding_created_at = ...

查询时过滤同一模型版本,或建立新 Collection 完成迁移后切换流量。不要仅依赖 Collection 名称中的字符串约定而不记录实际模型和预处理配置。

12.3 备份不能只备份对象存储

生产恢复至少要考虑:

  • Collection 和 Schema 元数据;
  • 主键与标量字段;
  • 原始文档或可重新生成向量的来源;
  • 向量数据和索引;
  • etcd 元数据;
  • 对象存储;
  • 消息存储中尚未完成处理的数据;
  • 认证、网络和部署配置。

索引通常可以从向量数据重建,因此备份策略可以在“备份索引文件”和“恢复后重建索引”之间权衡。但重建时间、计算成本和业务恢复时间目标必须实际测量。


十三、常见失败表现和诊断顺序

13.1 插入报维度错误

原因通常是:

  • 模型输出维度与 Schema 不一致;
  • 某批数据被错误截断或拼接;
  • 稠密向量和稀疏向量字段混用。

诊断:

print(len(rows[0]["embedding"]))

并检查模型配置、预处理代码和 Collection Schema。

13.2 搜索返回空结果

常见原因:

  • Collection 未加载;
  • 查询了错误的 Collection 或 Partition;
  • Filter 把所有数据排除了;
  • 查询向量字段名称错误;
  • 数据尚未达到当前一致性级别的可见时间点;
  • 主键删除标记已经生效。

应依次检查集合是否存在、加载状态、实体数量、过滤表达式和一致性设置,而不是先调大 efnprobe

13.3 搜索结果明显不准

诊断顺序通常是:

  1. 用 FLAT 建立精确基线;
  2. 确认索引与搜索使用同一度量;
  3. 确认向量归一化策略一致;
  4. 检查过滤条件;
  5. 调整 HNSW 的 ef 或 IVF 的 nprobe
  6. 评估是否是 PQ 量化误差;
  7. 检查不同模型版本是否混在一起。

如果 FLAT 结果也不符合业务预期,问题通常不在 ANN 索引,而在 Embedding 模型、文本切分、归一化、语言分布或标签定义。

13.4 查询延迟突然升高

可能原因包括:

  • Growing Segment 数量增加;
  • 新数据尚未完成索引;
  • QueryNode 内存不足导致加载或缓存压力;
  • Compaction 与索引构建抢占 CPU、磁盘或内存;
  • Filter 选择性差;
  • 查询并发超过 QueryNode 能力;
  • Segment 数量过多,结果合并成本增加。

应同时观察:

  • 查询 p50、p95、p99;
  • QueryNode CPU、内存和磁盘;
  • Segment 状态;
  • 索引构建和 Compaction backlog;
  • 消息积压;
  • 对象存储和 etcd 延迟。

只增加副本不一定有效。如果瓶颈在索引构建、对象存储、消息存储或标量过滤,扩展 QueryNode 可能只是掩盖问题。


十四、从本地示例到生产系统的最小迁移路径

一个较稳妥的迁移过程是:

  1. 使用 Milvus Lite 验证 Schema、Insert、Search、Query 和 Delete;
  2. 使用 Standalone 验证持久化、索引构建、Load 和重启恢复;
  3. 使用接近生产规模的数据建立 FLAT 基线;
  4. 选择 HNSW、IVF、PQ 或其他索引并测量 Recall@K;
  5. 在生产拓扑中分别压测写入、查询、Compaction 和索引构建;
  6. 验证 QueryNode、DataNode、IndexNode、etcd、对象存储和消息存储的故障恢复;
  7. 建立数据版本、备份、权限和成本监控;
  8. 最后再进行流量切换。

最容易犯的错误,是把“本地单进程 API 能运行”当成“分布式生产部署已经验证”。本地模式只能证明调用方式和部分数据逻辑正确,不能证明高可用、扩容、数据恢复、实时可见性和高并发性能正确。

Milvus 的核心可以归纳为:Collection 定义逻辑数据模型,Segment 承载数据生命周期,索引在精确度、内存和延迟之间做取舍,QueryNode 合并实时与历史数据,协调组件负责状态推进,而集群部署的可靠性取决于 Milvus 服务和外部元数据、对象存储、消息存储共同组成的完整数据链路。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。