前言
Milvus是目前最常用的向量数据库之一,本文粗浅介绍其内部查询、存储机制。本文所介绍内容基于Milvus 2.6.x。
Milvus的构成
核心架构
Milvus 的典型分布式依赖可以概括为:
- etcd:元数据存储与服务发现
- 对象存储(MinIO / S3):Segment 数据、索引、统计信息等持久化文件
- 消息队列 / WAL(Pulsar、Kafka,或 Woodpecker 等):写入日志与流式通道
- Milvus 计算组件:向量检索引擎本身
可以简单理解为:对象存储是借来的"硬盘",etcd 是借来的"便签本",MQ/WAL 是写入的"流水账"。Milvus 本身负责索引算法、查询执行、段管理、混合检索。
拆解后看 Milvus 真实组成
css
┌─────────────────────────────────────────────┐
│ Milvus Proxy(API 网关,gRPC/REST) │
├─────────────────────────────────────────────┤
│ StreamingNode(增量写入 / Growing Segment / 实时查询)│
│ QueryNode (历史 Segment 查询 + 分层缓存) │
│ DataNode (compaction + 建索引等批处理) │
│ MixCoord (Root/Data/Query Coord 合并) │
├─────────────────────────────────────────────┤
│ 共享存储:对象存储(S3/MinIO 接口) │
│ 共享元数据:etcd │
│ 写入通道:MQ / WAL │
└─────────────────────────────────────────────┘
etcd
etcd 是一个分布式、可靠的键值存储系统,基于 Raft 共识算法保证强一致性。
etcd在Milvus中的作用
(1) 元数据存储(最主要用途)
存储Milvus 需要持久化的元数据:
- Collection / Schema 定义(字段、维度、索引类型)
- Segment 切分信息(数据如何分片)
- 索引构建状态(built / building / failed)
- 集群拓扑、加载与路由相关元信息
这些元数据量小但访问极频繁,完美匹配 etcd 的 KV + Raft 特性。
注意:查询热路径通常不会"每次 search 都去打 etcd"。Coord 会把分布信息下发给 Worker,节点内存中维护路由;etcd 更偏持久化与协调底座。
(2) 服务发现
Milvus 内部拆成多个角色:
csharp
[Proxy] ← 接收 SDK 请求
[StreamingNode] ← 增量写入与实时查询
[QueryNode] ← 历史数据检索
[DataNode] ← compaction / 建索引
[MixCoord] ← 统一协调
各组件启动时向 etcd 注册自己 + 订阅其他节点,Proxy 才知道该把请求路由到哪个节点。
(3) 分布式协调
| 场景 | etcd 提供的能力 |
|---|---|
| 多 Proxy / 多 Worker 并存 | lease、会话与拓扑感知 |
| Coord 选主与故障转移 | 基于 etcd 的分布式协调 |
| 节点上下线 | 通过会话变化驱动路由更新 |
(4) 配置中心
配额设置、运行时参数、数据库 / 用户权限等信息可通过 etcd 的 Watch 机制实时下发给相关节点。
etcd关键部署配置解读
yaml
ETCD_AUTO_COMPACTION_MODE=revision # 按 revision 自动压缩
ETCD_AUTO_COMPACTION_RETENTION=1000 # 保留最近 1000 个 revision
ETCD_QUOTA_BACKEND_BYTES=4294967296 # 后端存储上限 4GB
ETCD_SNAPSHOT_COUNT=50000 # 每 5 万条 commit 触发一次快照
Milvus 元数据会持续膨胀,需要定期压缩避免 etcd 被打爆。4GB 是常见推荐上限量级,超过会影响性能。
一句话总结
etcd 管"数据怎么找、谁在管、状态如何"。
MinIO / 对象存储
Milvus存算分离,更适合分布式系统。对象存储用于持久化 Segment 的原始数据、索引文件、统计信息等:
一段 768 维 float32 的向量 ≈ 3KB,加上原文 + 元数据 ≈ 10KB+。
| 规模 | 数据量 |
|---|---|
| 100 万条 | ~10GB |
| 1 亿条 | ~1TB |
| 10 亿条 | ~10TB+ |
这种体量的数据不可能全部长期堆在计算节点本地盘里当唯一真相源。
scss
[Milvus 计算节点] ──读/写──> [对象存储 MinIO/S3]
│ │
│ └─ binlog / 索引 / stats
└─ 元数据(etcd) └─ 大文件、按需拉取
这种设计有以下好处:
| 收益 | 说明 |
|---|---|
| 计算节点无状态化 | 持久真相在对象存储;节点可更快重建(本地仍有缓存/mmap) |
| 独立扩缩容 | 数据涨了扩存储,查询慢了扩 QueryNode / StreamingNode |
| 成本优化 | 热数据留内存/本地盘,冷数据留对象存储 |
| 分层存储底座(Milvus2.6版本) | QueryNode 可只加载元数据,查询时再按需拉块 |
Milvus 内部数据的"两段式"流转
写入路径:
javascript
insert(v vectors)
→ Proxy 将写入路由到 StreamingNode
→ StreamingNode 消费 WAL/MQ,写入 Growing Segment
→ seal 后持久化到对象存储(binlog / stats 等)
→ 元数据在 etcd / MixCoord 侧登记("这段在哪、什么状态")
→ DataNode 后续做 compaction、建索引等批处理
查询路径(开启 Tiered Storage 时):
scss
search(query)
→ Proxy 按内存中的分布信息路由到 StreamingNode / QueryNode
→ 利用内存中的统计信息做段级剪枝
→ QueryNode 按需从对象存储加载相关数据块 / 索引到缓存层
→ 在内存(及本地缓存)中计算并返回结果
- 2.5 及默认"传统 load"模型 :
load_collection往往会把 Segment 数据/索引较完整地载入本地,查询主要走内存。 - 2.6 Tiered Storage :
load先加载轻量元数据,字段数据与索引可懒加载/部分加载。
Milvus 的核心能力
向量索引算法 --- 最核心的壁垒
Milvus中能使用多种类型索引,比如:
| 索引 | 原理 | 适用场景 |
|---|---|---|
| HNSW | 分层图导航 | 高 QPS、低延迟(常用推荐) |
| IVF 系列 | 倒排聚类 | 大数据量、可控召回 |
| IVF_RABITQ | IVF + 1-bit 量化(可选 refine) | 2.6 省内存主力之一 |
| DISKANN | 磁盘图索引 | 单机超大规模、内存受限 |
| SCANN | 量化压缩检索 | 吞吐优先场景 |
这里每一种索引算法都是论文级的。基于合理的索引,Milvus能把"在 1 亿条向量里找 Top-10 最相似"从 O(N) 暴力比对降到亚毫秒到毫秒级。
查询执行引擎 --- 把索引能力组织成完整查询
索引解决的是"如何在一个 Segment 内快速找到近邻",查询引擎解决的则是:面对过滤条件、多种向量、多个 Segment 和多个节点,如何生成并执行一份完整的查询计划。
Milvus 对外主要提供两类读取能力:
| 能力 | 作用 | 典型用法 |
|---|---|---|
| Search | 按向量相似度返回 Top-K | 语义检索、以图搜图、推荐召回 |
| Query | 按主键或标量表达式精确读取 | 查详情、按条件筛选、主键批量获取 |
一次典型的向量查询并不只是调用 HNSW 或 IVF,而是由查询引擎完成整条执行链:
css
向量 / 标量过滤条件
→ 解析表达式并生成执行计划
→ 根据统计信息做 Segment 剪枝
→ 在候选 Segment 内执行标量过滤
→ 调用向量索引完成 ANN 检索
→ 汇总 Growing Segment 与 Sealed Segment 的结果
→ 跨节点归并局部 Top-K,返回全局 Top-K
其中包含几项关键能力:
- 向量 + 标量联合过滤 :例如先限定
tenant_id、时间范围和文档类型,再做相似度检索。 - 实时数据与历史数据统一查询:StreamingNode 检索尚未封存的 Growing Segment,QueryNode 检索已封存并建立索引的 Sealed Segment,最终合并结果。
- 分布式并行执行:不同 Segment 可分布在多个节点上并行检索,由 Proxy 汇总局部 Top-K。
- 多向量混合检索:可分别执行稠密向量、稀疏向量或多个向量字段的搜索,再通过加权或 RRF 等方式重排。
- 一致性控制:通过时间戳与一致性级别,在"立即读到最新写入"和"更低查询开销"之间取舍。
所以 Milvus 作为查询引擎的核心,不只是"支持很多 ANN 索引",而是将过滤、剪枝、索引检索、实时可见性、分布式归并和重排组织成一条可扩展的执行流水线。后文会结合 Segment 结构详细拆解这条链路。
Milvus的存储结构
Milvus存储的核心结构是Segment
Segment
Segment ≠ 文档 / 文档片段,这是两个完全不同的概念,很多人会混。
ini
┌─────────────── 逻辑层(用户视角) ───────────────┐
文档 (Document) 一篇 PDF / 一篇文章 / 一个网页
│
├─ Chunk 1 "AGI 是人工智能的终极目标..."
├─ Chunk 2 "图灵测试是判断机器智能..."
└─ Chunk 3 "强化学习是 AGI 的关键路径..."
└────────────────────────────────────────────────┘
┌─────────────── 物理层(Milvus 视角) ───────────────┐
Collection (类似"表") 整个数据集,比如所有文档
│
├─ Segment A (物理桶) 达到 seal 阈值后封存
│ ├─ 行 1: (vec_1, text_1, doc_id="d1")
│ ├─ 行 2: (vec_2, text_2, doc_id="d1")
│ ├─ 行 3: (vec_3, text_3, doc_id="d2")
│ ├─ ...
│ └─ 行 N: (vec_N, text_N, doc_id="d50")
│
└─ Segment B (物理桶) 下一个封存段
└─ 又是一批混合行
└────────────────────────────────────────────────┘
Segment 目标大小由配置控制(如 dataCoord.segment.maxSize)。历史上常见默认是 512MB,较新版本常见默认更大(如 1GB),不要把某个固定值当成永远不变的"官方默认"。
一个具体例子
假设导入 1000 篇文章,每篇切成 50 个 chunk,共 5 万个 chunk。一个错误的观点是认为一个segment是一篇文章:
ini
Segment = 1 篇文章
对象存储文件 = doc_001.binlog (只存这一篇的 50 个 chunk)
但实际上Milvus 这么组织:
bash
5 万行分布到多个 Segment(每个达到 seal/compaction 目标规模)
逻辑上每个 Segment 是一堆"杂烩行":
可能跨很多篇文章的 chunk
每行用 doc_id 等标量字段标识来源
每个 Segment 是一堆"杂烩行" ,每行里有 doc_id 字段标识来自哪篇文章。
对象存储里的 Binlog
Binlog 按 Segment / 字段组织。重要点:Milvus 的 insert binlog 是列存,不是单文件行存。
perl
segment_001/
├─ field_vec.binlog ← 向量列
├─ field_doc_id.binlog ← 标量列
├─ field_text.binlog ← 文本列(若存储)
├─ field_ts.binlog ← 时间戳等系统列
├─ ...
├─ stats / deltalog ← 统计信息、删除日志等
└─ index files ← 索引构建完成后另存
从"行"的视角理解数据内容可以这样想象(这是逻辑视图,不是真实文件布局):
ini
逻辑 Row 0:
vec=[0.12, -0.34, ...768 floats]
doc_id="article-823"
chunk_idx=3
text="深度学习是机器学习的一个分支..."
timestamp=...
逻辑 Row 1:
vec=[0.45, 0.78, ...768 floats]
doc_id="article-104"
...
物理落盘时,上述字段会拆到各自的列文件中,便于按需加载(这也是 2.6 分层存储能"只拉部分列/块"的基础之一)。
Milvus检索的整体流程
- 先做段级剪枝,缩小候选 Segment
- 对命中的 Segment,按负载策略使用已缓存数据,或从对象存储补齐所需块
- 段内用标量过滤(常见为生成 bitset)再做 ANN
- Proxy 合并各节点局部 Top-K
arduino
用户查询: "近一周文档里,找和 query 最相似的 Top-10"
┌──────────────────────────────────────────┐
│ 1. Proxy 解析: 过滤条件 + 向量条件 │
└──────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 2. 段级剪枝(Segment Pruning) │
│ - 用 Bloom Filter / min-max stats 跳过 │
│ 不可能命中的 Segment │
└──────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 3. 段内: 标量过滤(Bitset) + ANN 搜索 │
└──────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 4. Proxy 跨节点合并 Top-K │
└──────────────────────────────────────────┘
关键点 1:段级剪枝 --- 避免无效 IO
Milvus 每个 Segment 维护轻量级统计信息(在内存里):
c
struct SegmentStats {
std::string field_name;
int64_t min_value; // 最小值
int64_t max_value; // 最大值
BloomFilter bf; // 可选,加速等值判断
NullBitmap null_bm; // NULL 标记
};
查询 WHERE create_time > '2026-07-24' 时:
yaml
Segment A: create_time 范围 [2025-01, 2025-06] ← max < 2026-07-24,跳过
Segment B: create_time 范围 [2026-05, 2026-08] ← 有交集,进入后续
Segment C: create_time 范围 [2026-07, 2026-08] ← 有交集,进入后续
不需要访问所有 Segment 的完整数据,往往能节省大量 IO。
关键点 2:Bitset 预过滤(段内)
对候选 Segment,先按过滤条件生成行级 bitset(示意):
ini
// 伪代码:为段内每行生成 0/1 bitset
vector<bool> bitset(rows_count);
for (int i = 0; i < rows_count; i++) {
bitset[i] = (row[i].create_time > '2026-07-24')
&& (row[i].status == "published");
}
HNSW 搜索过程中,每访问一个节点就先查 bitset:
- bitset=0 → 这条不满足过滤,跳过,不算距离
- bitset=1 → 计算向量距离,进入候选堆
HNSW 图遍历:
ini
入口点 → 邻居 A (bitset=1,距离=0.82) ✓
→ 邻居 B (bitset=0) ✗ 跳过
→ 邻居 C (bitset=1,距离=0.91) ✓
...
结果:不满足过滤条件的行不会进入最终结果;但 ANN 本身仍是近似检索,过滤很严时图连通性可能变差,需要靠策略切换保召回。
关键点 3:复合条件的位图运算
复杂 WHERE 用 Bitset 与或非:
ini
WHERE (category = 'tech' AND lang = 'zh')
OR (priority > 8)
执行计划:
ini
bitset_a = (category == 'tech' AND lang == 'zh') // AND
bitset_b = (priority > 8)
final = bitset_a | bitset_b // OR
Milvus 用位运算加速(SIMD 指令),千万行过滤可到毫秒级量级。
关键点 4:Proxy 跨节点合并
scss
QueryNode-1 / StreamingNode ─┐
QueryNode-2 ─┤ 各自返回 局部 Top-K
QueryNode-3 ─┤
QueryNode-4 ─┘
│
▼
Proxy 优先队列合并
(类似归并排序的 merge)
│
▼
全局 Top-K 返回
过滤策略: Standard vs Iterative
Milvus 默认常见路径是 标准预过滤(standard filtering):先标量过滤得到 bitset,再做 ANN。
当过滤表达式很复杂、标量计算成本很高时,可改用 迭代过滤(iterative filtering):先做向量检索迭代出候选,再对候选做标量校验,直到凑够 Top-K。
| 场景 | 策略 | 说明 |
|---|---|---|
| 一般混合检索 | Standard(预过滤 + ANN) | 默认路径,过滤语义清晰 |
| 过滤表达式很重/很复杂 | Iterative Filter | 减少"全段标量扫描"成本 |
| 过滤后剩余极少 | 可能退化为对剩余集合更直接的检索 | 避免图被"挖空"后召回崩掉 |
API 示例:
python
# 默认: standard filtering
client.search(
collection_name="docs",
data=[query_vec],
filter='create_time > "2026-07-24"',
limit=10,
)
# 复杂过滤: 启用 iterative_filter
client.search(
collection_name="docs",
data=[query_vec],
filter='color like "red%" and likes > 50',
limit=10,
search_params={"hints": "iterative_filter"},
)
一图流总结
arduino
┌──────────────┐
│ 过滤 WHERE │
└──────┬───────┘
│ min-max / bloom
▼
┌────────────────────────┐
│ 段级剪枝(skip 不相关段) │
└────────┬───────────────┘
│ 加载/命中缓存中的相关段
▼
┌────────────────────────┐
│ 生成 row-level bitset │
│ 或 iterative 校验候选 │
└────────┬───────────────┘
│
▼
┌────────────────────────┐
│ HNSW/IVF 等 ANN 检索 │
└────────┬───────────────┘
│
▼
┌────────────────────────┐
│ Proxy 全局合并 Top-K │
└────────────────────────┘
Milvus查询存储完整生命周期总结
阶段 1:写入(接触对象存储)
yaml
[SDK: insert(1万条)]
↓
[Proxy → StreamingNode / WAL]
↓ Growing Segment 攒批
↓ 达到 seal 条件
[封存 Segment,写入对象存储]
├── insert binlog(按列)
├── deltalog(后续删除)
└── stats(min-max / bloom filter 等)
↓
[元数据登记]
- segment_id: 1001
- collection: docs
- rows: 10000
- state: Sealed
↓
[DataNode 异步 compaction / 建索引]
阶段 2:加载
用户或系统调用 load_collection("docs"):
csharp
[QueryNode]
↓ 获取 "docs" 的 Segment 列表与元信息
↓ 默认先加载轻量元数据:
│ - schema / segment 分布
│ - 索引元信息、stats
↓ (可选 warmup)预热部分标量/向量索引
↓ 字段原始数据与重型索引可按需再拉
[开启 Tiered Storage 后的状态]
常驻: 元数据 + 热点缓存
冷数据: 仍在对象存储,命中时再加载
若未开启分层存储,则更接近传统模型:load 阶段把 Segment 数据/索引较完整地准备到本地内存或 mmap 映射区,之后查询尽量不打对象存储。
阶段 3:查询
sql
[SDK: search(filter=time>2026-07-24, limit=10)]
Proxy 收到 → 转发给持有相关数据的 StreamingNode / QueryNode
QueryNode 内部:
┌─────────────────────────────────────────┐
│ 1. 段级剪枝(读内存里的 stats) │
│ Seg 1001: 无交集 ✗ 跳过 │
│ Seg 1002 / 1003: 有交集 ✓ │
├─────────────────────────────────────────┤
│ 2. 确保所需列/索引在缓存中;缺失则从对象存储补齐 │
├─────────────────────────────────────────┤
│ 3. 生成 bitset(或 iterative 过滤) │
├─────────────────────────────────────────┤
│ 4. ANN 检索,返回局部 Top-K │
└─────────────────────────────────────────┘
Proxy 合并全局 Top-K
极限场景下Milvus的内存管理
即使有分层存储,理解"全量加载时内存怎么涨"仍然重要:很多集群默认或部分 collection 仍会走高驻留策略;分层存储也只是把成本从"常驻全部"变成"常驻工作集"。
加载 100GB 对象存储数据,若按传统全量 HNSW 驻留估算,内存峰值常常远高于 100GB,具体取决于索引与量化。
内存占用拆解
yaml
┌──────────────────────────────────────────────┐
│ 对象存储 100GB (压缩 binlog / 索引文件) │
└────────────────┬─────────────────────────────┘
│ 解压 / 解码
▼
┌──────────────────────────────────────────────┐
│ 原始数据 150-250GB 量级 │
│ - 向量: N × 768 × 4 bytes │
│ - 标量: doc_id / timestamp / category │
│ - 文本: chunk 内容(如果存了) │
└────────────────┬─────────────────────────────┘
│ 构建/加载索引
▼
┌──────────────────────────────────────────────┐
│ 最终内存占用(量级示意) │
│ HNSW: 向量 + 图层邻接,通常明显高于裸向量 │
│ IVF: 相对更省 │
│ IVF_RABITQ: 1-bit 量化,可降到很低的内存占比 │
│ DiskANN: 内存主要留压缩码/图导航结构 │
└──────────────────────────────────────────────┘
100GB 数据详细算账(传统全量驻留视角)
假设:5000 万个 768 维 float32 向量 + 少量标量 + 文本
| 阶段 | 计算 | 大小 |
|---|---|---|
| 对象存储里 | 压缩后 | 100 GB |
| 解压后原始向量 | 5000万 × 768 × 4 bytes | 143 GB |
| + 标量 + 文本 | 元数据 + 全文(平均 200 字) | + 15 GB |
| 小计:裸数据 | ≈ 158 GB | |
| HNSW 总占用 | 向量 + 图结构,常见可到裸向量数倍 | 常到数百 GB 级 |
| IVF 系列 | 聚类 + 倒排,通常更省 | 更接近裸数据量级 |
| IVF_RABITQ + SQ8 refine | 官方示意约可降到 IVF_FLAT 的 ~28% 内存 | 显著低于 HNSW |
| DiskANN | 磁盘为主,内存留压缩/导航结构 | 可到数十 GB 级 |
结论:同样 100GB 持久化数据,不同索引与是否全量驻留,内存差一个数量级很常见。
若开启 2.6 Tiered Storage,稳态成本更接近"热工作集",而不是上表这种"全库常驻"。
大型系统内存缓解方案
方案 1:选对索引(最立竿见影)
python
# RAG 场景中小规模,HNSW 通常足够
index_params = {
"metric_type": "COSINE",
"index_type": "HNSW",
"params": {"M": 16, "efConstruction": 200}
}
# 内存吃紧时,2.6 可优先评估 IVF_RABITQ
index_params = {
"index_type": "IVF_RABITQ",
"metric_type": "L2",
"params": {
"nlist": 1024,
"refine": True,
"refine_type": "SQ8"
}
}
# 或使用 DiskANN(磁盘图索引)
index_params = {
"index_type": "DISKANN",
"params": {"max_degree": 56, "search_list_size": 100}
}
方案 2:向量量化(减少单向量字节数)
| 量化方式 | 每向量大小 | 768 维 5000 万条 | 内存 | 召回率 |
|---|---|---|---|---|
| FP32(原始) | 3072 B | 143 GB | 基准 | 100% |
| FP16 | 1536 B | 71 GB | -50% | 很高 |
| SQ8(8-bit) | 768 B | 36 GB | -75% | 高 |
| PQ / RaBitQ | 更低 | 可到数 GB~数十 GB | 极省 | 取决于 refine |
python
# 经典乘积量化
index_params = {
"index_type": "IVF_PQ",
"params": {"nlist": 1024, "m": 8, "nbits": 8}
}
方案 3:Mmap(Milvus 2.4+)
mmap 通过 collection / field 属性配置,而不是 load(mmap_enabled=True):
python
# 创建时开启 collection 级 mmap
client.create_collection(
collection_name="docs",
schema=schema,
properties={"mmap.enabled": "true"}
)
# 或对已有 collection 修改后重新 load
client.alter_collection_properties(
collection_name="docs",
properties={"mmap.enabled": "true"}
)
client.release_collection("docs")
client.load_collection("docs")
方案 4:分片(Shard)+ 多节点
python
# 建表时设置 shard 数(具体 API 随 SDK 版本略有差异)
# 数据按 shard 分散,查询可并行到多个节点
方案 5:开启 2.6 Tiered Storage
让 QueryNode 把本地内存/磁盘当缓存,而不是全库容器。适合冷热不均、总数据远大于单机内存的场景。
总结
数据量 < 10GB → HNSW + 全内存,单机通常够用
10GB < 数据 < 100GB → HNSW + FP16/SQ8,或 IVF_RABITQ;考虑 mmap
100GB < 数据 < 1TB → IVF_RABITQ / DiskANN / IVF_PQ + 分片;优先评估 Tiered Storage
数据 > 1TB → 分布式 + 量化 + 分层存储 / mmap 组合
ChromaDB 对比Milvus的关键短板
没有真正的预过滤(混合检索痛点)
python
# ChromaDB 的 where 过滤
results = collection.query(
query_embeddings=[vec],
where={"timestamp": {"$gt": 1234567890}},
n_results=10
)
Chroma常见执行形态(简化):
- ANN 先拿一批候选
- 再用 where 条件过滤
- 过滤很严时,可能凑不齐结果甚至接近空结果
Milvus 的混合检索能力(并非全部从 2.6 才开始有):
- 段级剪枝先排除不可能命中的 Segment
- 段内可通过 Bitset 做标准预过滤,再跑 ANN
- 过滤表达式很重时,可用
hints=iterative_filter - 2.6 新增 JSON Path Index :对 JSON 路径建索引(如
metadata["user"]["city"]),复杂 JSON 过滤可有数量级加速(官方案例约 100x,取决于数据与表达式)
持久化与压缩
ChromaDB 更偏向本地/嵌入式场景;Milvus 面向对象存储持久化,并配合列存、压缩、分层加载等生产级能力。
ChromaDB 适合什么场景
| 场景 | 推荐 | 原因 |
|---|---|---|
| 本地开发 / Demo | ✅ ChromaDB | 零配置,Python 一行启动 |
| 小规模 RAG(< 100 万条) | ✅ ChromaDB | 简单够用 |
| 中等规模(100 万-1000 万) | ⚠️ 可用 | 内存与单机能力要评估 |
| 大规模(> 1000 万条) | ❌ 更看 Milvus | 需要分布式、分层与运维能力 |
| 生产 RAG 长期服务 | ❌ 更看 Milvus | 可靠性、可观测性、混合检索更成熟 |
一句话总结
ChromaDB = 轻量嵌入式,简单但粗放;Milvus = 工程师思维,有 mmap / DiskANN / 量化 / 分片 / 段剪枝 / 2.6 分层存储一整套工具。 本地测试 ChromaDB 完全够;生产 RAG 一旦数据量与 QPS 上来,通常还是 Milvus 这类系统更稳。
附:Milvus 2.6.x 新特性一览
1. 内置 Embedding(Data-in, Data-out)
从 2.6.0 开始,Milvus 支持通过 Function 接口接入外部 Embedding 服务,插入/检索时可直接使用原始文本:
python
# 旧方式:需要外部 Embedding 模型
vec = embedding_model.encode("text")
collection.insert([vec, "text", ...])
# 2.6.x:配置 Embedding Function 后,可直接围绕原始文本构建链路
# (具体 provider / schema / Function 定义见官方 Embedding Function 文档)
collection.insert([
{"text": "Milvus is a vector database", ...}
])
# Milvus 按配置调用 OpenAI、Cohere、Bedrock、Vertex AI 等服务完成向量化
这大幅简化了 RAG 工程链路,常见场景下无需再自建一层 Embedding 中转服务。
2. 新增索引和数据类型
| 新增能力 | 说明 | 适用场景 |
|---|---|---|
| IVF_RABITQ | IVF + 1-bit 量化,可选 SQ8 等 refine | 内存紧张的规模化部署 |
| INT8_VECTOR | 支持 int8 量化向量作为输入 | 模型已输出量化向量的场景 |
| MINHASH_LSH | 基于 MinHash 的 LSH 索引 | 近重复检测、文本去重 |
3. 成本优化
Milvus 2.6.x 通过分层存储 + RaBitQ 等能力显著降低资源成本
Milvus 3.0 核心变化
这次更新主要带来了两个层面的改变:
-
数据不搬家,就地检索 :这是3.0最核心的变化。它引入了External Collections 功能,允许你直接在Parquet、Lance、Iceberg、Vortex等开放数据湖格式上定义和查询Milvus Collection。这意味着数据可以留在对象存储(如S3)或数据湖中,Milvus会在原地为其建立索引并提供检索能力,无需再复制一份数据到向量数据库内,极大地节省了存储成本和ETL开销。
-
更强大的检索引擎:Milvus 3.0不再只是一个向量近邻搜索引擎,其功能更加丰富。
- 服务端计算 :将排序(ORDER BY)、聚合(Aggregation)、分面搜索(Faceted Search) 等操作下推到数据库引擎内部执行,减少了应用层处理的负担。
- 多向量检索 :通过引入 StructArray ,原生支持一个实体对应多个向量的场景,并能更好地支持ColBERT、ColPali这类延迟交互(Late-interaction)模型。
- 稀疏向量增强:优化了稀疏向量索引,体积更小,查询性能(QPS)更高。
此外,作为配套的存储引擎Loon(Storage v3) ,其内部基准测试显示,可将S3上的点读I/O从Parquet的约9.4MB大幅降低至约0.07MB ,减少了约135倍