一句话回答

Milvus 是一个面向大规模向量检索的开源、云原生数据库。它把向量、标量元数据和索引作为可持久化、可过滤、可扩缩容的数据来管理,适合为 RAG、语义搜索、推荐和多模态检索提供低延迟 ANN 召回。

面试时可以先给出下面这条主线:

文档切块 → Embedding → 写入 Milvus → 对 Query 做 Embedding → 向量 ANN + 元数据过滤/BM25 → 融合与 Rerank → 组装上下文 → LLM 生成。

需要先强调一个容易加分的结论:

RAG 不一定必须使用专用向量数据库。 小规模、静态、单机数据可以用 NumPy/Faiss;已有 PostgreSQL 可以先用 pgvector;已有 Elasticsearch 且强依赖全文检索时可以直接做混合搜索。只有当数据量、并发、过滤、实时更新、持久化、高可用或独立扩缩容需求上来后,专用向量数据库的价值才明显。

为什么需要“向量数据库”

这个问题最好从传统关系数据库擅长什么讲起。

MySQL、PostgreSQL 等关系数据库主要面向结构化数据和精确查询。一条典型 SQL 可能是:

1
2
3
4
5
SELECT *
FROM documents
WHERE tenant_id = 1001
AND status = 'active'
AND created_at >= '2026-01-01';

这类查询的条件是明确的:字段等于什么、是否在某个范围、两张表如何关联。关系数据库可以通过 B+ Tree、Hash、Bitmap 等索引快速定位记录,并提供事务、约束、Join 和聚合能力。

但 RAG 面对的问题通常是:

给定“员工离职后邮箱还能用多久?”,从几十万或上百万个文本块中找出语义最接近的若干段内容。

数据库中可能存的是“劳动关系终止后,企业账户保留七日”。这两段文本没有足够的精确字段关系,=、范围查询和普通 B+ Tree 都无法表达“语义上是否相似”。LIKE '%离职%' 或全文检索能够匹配词面,却不一定能处理同义改写、口语化问题和跨语言表达。

因此,两类数据库解决问题的出发点不同:

对比维度 传统关系数据库 向量数据库
主要数据 数字、字符串、日期等结构化字段 Embedding 向量及其标量元数据
查询目标 找到满足确定条件的记录 找到距离最近、语义最相似的 Top-K
匹配方式 等值、范围、Join、聚合 COSINE、IP、L2 等距离计算
常用索引 B+ Tree、Hash、Bitmap HNSW、IVF、PQ、DiskANN
返回语义 满足条件或不满足条件 按相似度排序的近似结果
典型优势 ACID 事务、复杂 SQL、数据完整性 高维向量 ANN、Recall-Latency 权衡、大规模并行召回
RAG 中的角色 保存用户、权限、订单、文档状态等业务事实 保存 Chunk Embedding 并完成语义召回

为什么普通索引不适合向量检索

B+ Tree 依赖一维有序关系。例如数字 10 < 20 < 30,可以通过树结构快速排除大量不相关数据。但一个 768 维向量不存在这样简单的全局顺序,而且还可能需要分别按照 COSINE、IP 或 L2 判断距离。

假设知识库有 $N$ 个 Chunk,每个向量维度为 $d$。如果只把向量作为数组或 BLOB 存进关系数据库,而没有专用向量索引,那么每次检索都需要:

  1. 读取大量甚至全部向量;
  2. 对 Query 与每个向量计算距离;
  3. 对结果排序并取 Top-K。

暴力检索的计算复杂度约为:

$$ O(Nd) $$

当 $N$ 很小时这完全可接受;当数据达到百万、亿级且并发升高时,每次全量扫描代价过高。向量数据库使用 ANN 索引,只访问更可能包含近邻的桶、图节点或磁盘区域,以可控的少量 Recall 损失换取显著更低的延迟。

所以,向量数据库主要解决的不只是“存一个浮点数组”,而是以下问题:

  1. 高效 ANN 检索:通过 HNSW、IVF、DiskANN 等索引减少候选范围;
  2. 持久化和增量更新:支持 Insert、Upsert、Delete、Compaction,而不是每次重建一个内存索引;
  3. 标量过滤:在 tenant_id、权限、时间、语言、文档类型等条件下做向量搜索;
  4. 分布式扩展:数据分片、查询并行、负载均衡、故障恢复;
  5. 一致性选择:控制“刚写入的数据多久能被搜索到”;
  6. 多租户与安全:数据库、Collection、Partition、RBAC、TLS;
  7. 运维能力:监控、备份、限流、资源隔离、滚动升级;
  8. 混合检索:在同一系统中管理 Dense、Sparse/BM25 和多向量字段。

关系数据库真的不能做向量检索吗

不能绝对地说关系数据库不能做。PostgreSQL 安装 pgvector 后,也能定义向量类型并使用 HNSW、IVFFlat 等索引:

1
2
3
4
5
SELECT chunk_id, content
FROM chunks
WHERE tenant_id = 1001
ORDER BY embedding <=> :query_vector
LIMIT 10;

这时 PostgreSQL 已经通过扩展获得了专门的向量存储、距离运算和 ANN 索引能力。对于数据量不大、QPS 不高、团队已有 PostgreSQL,并且需要事务和 Join 的项目,pgvector 往往比新增 Milvus 更简单。

专用向量数据库的价值主要在于向量成为核心工作负载以后

  • 查询、写入、索引构建和 Compaction 可以独立扩缩容;
  • 提供更多向量索引、压缩、磁盘和 GPU 路径;
  • 面向大量向量做分片、并行检索和全局 Top-K 归并;
  • 原生支持多向量、Dense + Sparse/BM25、过滤和 Recall-Latency 调优;
  • 避免大规模向量查询与关系数据库中的 OLTP 事务争抢 CPU、内存和 I/O。

因此,选择向量数据库并不是因为关系数据库“不能存向量”,而是因为两者的主要优化目标不同。

面试回答可以概括为:

传统关系数据库擅长结构化数据上的精确匹配、事务和 Join,但 RAG 需要在高维向量中按照 COSINE、IP 或 L2 找语义最相似的 Top-K。普通 B+ Tree 无法有效支持这种高维近邻查询,只把向量存成数组会退化为全量距离计算。向量数据库通过 HNSW、IVF、DiskANN 等 ANN 索引,以及分片、并行查询、标量过滤和增量更新,把语义检索做成可扩展的在线数据库能力。不过,如果数据量较小且已有 PostgreSQL,使用 pgvector 可能更经济;只有当向量成为主要工作负载时,Milvus 这类专用系统的优势才明显。

2. 向量检索基础

2.1 Embedding 是什么

Embedding 是把文本、图片、音频等对象编码为稠密向量:

$$ E(x) \in \mathbb{R}^d $$

训练良好的 Embedding 模型会让语义相似对象的向量更接近。数据库不理解自然语言语义,它只负责存储模型生成的向量,并根据指定的距离函数检索。

三个工程约束必须记住:

  • 写入文档和查询时必须使用兼容的 Embedding 模型
  • 向量维度必须与 Collection Schema 一致;
  • 切换 Embedding 模型通常意味着重新生成全量向量并重建索引。

因此生产系统应保存 embedding_modelembedding_versionchunk_version,不能只保存向量。

2.2 常见距离度量

余弦相似度 COSINE

$$ \cos(q,x)=\frac{q\cdot x}{\|q\|\|x\|} $$

只关注方向,通常适合文本语义向量。值越大越相似。

内积 IP

$$ IP(q,x)=q\cdot x $$

同时受方向和模长影响,值越大越相似。如果向量已经做单位归一化,则 IP 与 COSINE 的排序等价。

欧氏距离 L2

$$ L2(q,x)=\sqrt{\sum_{i=1}^{d}(q_i-x_i)^2} $$

值越小越相似。常见于图像、几何或模型明确要求使用 L2 的场景。

度量 越大/越小越相似 常见场景 注意点
COSINE 越大 文本语义检索 忽略模长
IP 越大 已归一化向量、推荐 未归一化时模长会影响结果
L2 越小 图像、几何距离 与模型训练目标保持一致

最重要的工程规则不是“文本一律用 COSINE”,而是:

优先遵循 Embedding 模型的 Model Card;建索引和搜索时使用相同 metric。

2.3 KNN、ANN、Recall

  • KNN/Exact Search:遍历所有向量,结果准确,计算量大;
  • ANN/Approximate Nearest Neighbor:通过索引只检查部分候选,速度快,但可能漏掉真正近邻;
  • Top-K:返回最相似的 K 个结果;
  • Recall@K:真实相关结果中有多少出现在前 K 个结果中。

若真实 Top-10 中有 8 个被 ANN 找到,则:

$$ Recall@10=\frac{8}{10}=0.8 $$

ANN 的核心权衡是:

1
2
更深地搜索索引 → Recall 更高 → 延迟和 CPU 开销更大
更激进地压缩向量 → 内存更低 → 精度可能下降

向量数据库调优不能只看 QPS,也不能只看 Recall,通常要同时看:

  • Recall@K / NDCG@K / MRR;
  • P50、P95、P99 延迟;
  • QPS;
  • 索引构建时间;
  • 内存、磁盘和网络开销;
  • 数据写入到可检索的延迟。

3. Milvus 的数据模型

3.1 核心概念

1
2
3
4
5
6
7
8
Milvus Instance
└── Database
└── Collection
├── Schema / Fields
├── Partition
│ └── Segment
│ └── Entity
└── Indexes
概念 类比关系数据库 说明
Database Database 逻辑隔离多个业务
Collection Table 一组 Schema 相同的 Entity
Entity Row 一条记录,例如一个 Chunk
Field Column 主键、向量、VARCHAR、JSON、时间等
Partition Partition 按业务或租户缩小检索范围
Segment 存储/执行单元 Milvus 内部组织、索引和查询数据的基本单元
Index Index 向量 ANN 索引或标量索引

一个 Collection 必须有主键,可以包含多个向量字段和多个标量字段。RAG 中一般是一条 Entity 对应一个 Chunk,而不是一整篇 Document。

3.2 Growing Segment 与 Sealed Segment

Milvus 写入的数据先进入增长中的 Segment:

  • Growing Segment:仍在接收实时写入,通常以较接近暴力扫描的方式参与查询;
  • Sealed Segment:不再写入,可构建完整索引并由 Query Node 加载查询;
  • Compaction:合并小 Segment、清理删除数据和降低碎片。

这解释了两个常见现象:

  1. 刚写入的数据虽然能被搜索,但性能特征可能与已建索引的历史数据不同;
  2. 高频小批量写入和删除会产生碎片,Compaction、批量写入策略与 Segment 状态会影响 P99 延迟。

3.3 RAG 推荐 Schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
chunk_id             VARCHAR / primary key
document_id VARCHAR
tenant_id VARCHAR / partition key 候选
content VARCHAR 或 TEXT
title VARCHAR
source_uri VARCHAR
page_no INT32
chunk_no INT32
language VARCHAR
acl_group ARRAY<VARCHAR> 或可索引标量
updated_at TIMESTAMPTZ/INT64
is_active BOOL
embedding_model VARCHAR
embedding_version VARCHAR
content_hash VARCHAR
dense_vector FLOAT_VECTOR
sparse_vector SPARSE_FLOAT_VECTOR(可选)

设计原则:

  • 主键要稳定、可重建,常用 hash(document_id + chunk_no + version)
  • 高频过滤字段显式定义并建立标量索引;
  • 灵活但低频的元数据可以放 JSON 或动态字段 $meta
  • tenant_id、权限和 is_active 必须在召回时过滤,不能只在返回后过滤;
  • 原文、页码、标题、URL 等引用信息要与向量放在同一 Entity 或可通过主键稳定回表;
  • 大对象不要直接塞入一条记录,可只存 Chunk 和对象存储 URI。

4. Milvus 2.6.x 架构

4.1 总体设计

Milvus Distributed 的核心思想是:存储与计算分离、控制面与数据面分离、查询/写入/离线任务可独立扩缩容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                         ┌──────────────┐
Client / SDK ───────────→│ Proxy │ 无状态接入、校验、路由、结果归并
└──────┬───────┘

┌──────▼───────┐
│ Coordinator │ 元数据、拓扑、调度、时间戳
└──────┬───────┘
┌─────────────┼─────────────┐
┌───────▼──────┐ ┌────▼─────┐ ┌─────▼─────┐
│Streaming Node│ │Query Node│ │ Data Node │
│WAL/实时数据 │ │历史查询 │ │索引/压缩 │
└───────┬──────┘ └────┬─────┘ └─────┬─────┘
└──────────────┼─────────────┘

┌──────────────────▼──────────────────┐
│ Metadata / WAL / Object Storage │
│ etcd / Woodpecker / S3、MinIO 等 │
└─────────────────────────────────────┘

Access Layer:Proxy

  • 对外提供统一访问入口;
  • 校验请求并做路由;
  • 将查询分发到多个节点;
  • 对各分片 Top-K 结果做多级归并;
  • 本身无状态,可通过负载均衡横向扩展。

Coordinator

  • 管理 Collection、Partition、Index 等元数据操作;
  • 管理时间戳和一致性;
  • 调度 Query Node、Data Node、Streaming Node;
  • 负责拓扑、负载均衡、Segment 和任务管理。

Streaming Node

  • 处理实时写入路径;
  • 将操作记录到 WAL;
  • 查询尚未转为历史数据的 Growing Segment;
  • 推进数据由 growing 到 sealed 的状态转换。

Query Node

  • 从对象存储加载历史 Segment 和索引;
  • 执行 Sealed Segment 上的向量检索与标量过滤;
  • 并行计算局部 Top-K。

Data Node

  • 执行历史数据的离线处理;
  • 构建索引;
  • 执行 Compaction;
  • 将处理结果写入对象存储。

Storage

  • 元数据存储:保存 Schema、拓扑、检查点等,需要强一致和高可用;
  • WAL:保证实时写入的持久性和故障恢复;
  • 对象存储:保存 Binlog、索引和历史数据,常用 S3/MinIO。

面试不要只背组件名,要说明为什么拆分:

RAG 的查询、写入和索引构建资源模型不同。查询偏 CPU/内存,索引构建是离线计算,写入依赖日志和实时可见性。拆分后可以针对瓶颈独立扩容,也能让计算节点更接近无状态,方便故障恢复。

4.2 写入链路

1
2
3
4
5
6
7
8
Client Insert/Upsert
→ Proxy 校验与路由
→ Streaming Node 写 WAL
→ Growing Segment 可参与实时查询
→ Segment Seal
→ Data Node 持久化、Compaction、建索引
→ 对象存储
→ Query Node 加载新索引

insert 不应被理解成“每一行同步写入最终索引”。Milvus 是面向批量、分段和异步索引构建的系统。生产中应尽量批量写入,避免每条数据单独请求。

4.3 查询链路

1
2
3
4
5
6
7
8
Query → Embedding
→ Proxy 路由到相关 Shard/Segment
→ Streaming Node 搜 Growing 数据
→ Query Node 并行搜 Sealed Segment 索引
→ Segment 局部 Top-K
→ 节点级归并
→ Proxy 全局归并/重排
→ Top-K 结果

分布式 Top-K 不能简单让每个分片只返回 1 条。一般需要每个相关分片返回局部 Top-K,再进行全局归并,否则可能漏掉全局最优结果。

5. Milvus 索引:面试重点

5.1 FLAT

原理:遍历全部向量并计算真实距离。

  • 优点:Recall 为 100%,无 ANN 误差,几乎不需要训练索引;
  • 缺点:复杂度接近 $O(Nd)$,数据大或并发高时慢;
  • 适用:小数据集、离线评测 Ground Truth、强过滤后候选极少、极高 Recall 场景。

5.2 IVF_FLAT

IVF(Inverted File)先通过聚类把向量划分到 nlist 个桶,查询时只搜索离 Query 最近的 nprobe 个桶。

1
2
3
4
全部向量
├── centroid 1 → posting list 1
├── centroid 2 → posting list 2
└── centroid n → posting list n

关键参数:

  • nlist:桶的数量;太少则每个桶太大,太多则训练与管理成本增加;
  • nprobe:查询时探测多少个桶;越大 Recall 越高、延迟越大。

IVF_FLAT 桶内仍保存原始向量,精度通常好,但内存占用不低。

5.3 IVF_SQ8 与 IVF_PQ

量化通过牺牲部分精度减少内存:

  • SQ8:把每个浮点维度量化为 8 bit;
  • PQ:把向量拆成多个子向量,分别用码本编码,压缩更强。

特点:

  • IVF_PQ 内存小,适合大规模和资源受限场景;
  • 压缩会引入距离估计误差;
  • 可以扩大候选后用原始向量 Refinement,换取更高精度。

5.4 HNSW

HNSW(Hierarchical Navigable Small World)构建多层近邻图:

  • 上层节点少,用于快速跳转到目标区域;
  • 下层节点多,用于局部精细搜索;
  • 查询像“从高速公路进入城市道路”。

关键参数:

  • M:每个节点的连接度;越大通常 Recall 越高、内存和构建成本越大;
  • efConstruction:建图时的候选宽度;越大索引质量越高、构建越慢;
  • ef:查询时的候选宽度;越大 Recall 越高、查询越慢,且应不小于 Top-K。

优点:高 Recall、低延迟、非常适合较小 Top-K。缺点:图结构内存开销大、构建相对慢。

5.5 DiskANN

DiskANN 使用适合 SSD 的图索引思路,使大部分索引/原始数据不必全部常驻内存。

  • 优点:数据超出内存时仍能保持较好的延迟和 Recall;
  • 缺点:依赖高性能 NVMe SSD,对磁盘随机读和缓存命中更敏感;
  • 适用:亿级、十亿级数据,内存成本成为主要瓶颈的场景。

5.6 Sparse Index 与 BM25

Milvus 支持 SPARSE_FLOAT_VECTOR 和倒排索引,可用于:

  • Milvus 内置 BM25 Function;
  • SPLADE、BGE-M3 等模型生成的 Learned Sparse Embedding;
  • Dense + Sparse Hybrid Search。

BM25 更关注词频、逆文档频率和文档长度归一化:

$$ score(D,Q)=\sum_{q_i\in Q}IDF(q_i)\cdot \frac{f(q_i,D)(k_1+1)} {f(q_i,D)+k_1(1-b+b\cdot\frac{|D|}{avgdl})} $$

对错误码、专有名词、数字和型号,Sparse/BM25 往往能补足 Dense 检索的盲区。

5.7 如何选索引

场景 优先尝试 原因
小数据或需要 Ground Truth FLAT 准确、简单
数据在内存、低 Top-K、高 Recall HNSW 查询速度和召回率通常较好
内存较紧、可接受轻微精度损失 IVF_SQ8 / IVF_PQ 压缩明显
大 Top-K 或适合聚类裁剪 IVF 系列 可通过nprobe 控制扫描范围
数据超过内存且有 NVMe DiskANN 降低内存压力
关键词/BM25 SPARSE_INVERTED_INDEX 稀疏倒排检索
不想先决定具体索引 AUTOINDEX 先建立基线,再根据评测显式调优

正确的选型方式是用自己的数据做 Recall-Latency 曲线,不是背一个“百万数据就一定用 HNSW”的固定答案。数据分布、维度、Top-K、过滤比例、并发和硬件都会改变结果。

6. 标量过滤、分区和多租户

6.1 为什么过滤必须在召回阶段执行

错误做法:先向量召回 Top-10,再在应用层删除无权限数据。

假设 Top-10 中 9 条属于其他租户,过滤后只剩 1 条;但索引中可能还有大量属于当前租户的相关文档,只是它们没有机会进入原始 Top-10。这叫后过滤截断

正确做法:

1
filter='tenant_id == "acme" and is_active == true and language == "zh"'

权限过滤必须进入所有召回路径,包括 Dense、Sparse/BM25 和任何缓存层。

6.2 标量索引

Milvus 除了向量索引,也支持标量字段索引,例如:

  • VARCHAR/数字字段:INVERTED;
  • BOOL、低基数或数组:BITMAP;
  • JSON:对常用 JSON Path 建索引;
  • 动态字段 $meta:可保存未显式声明的字段,但高频过滤字段更适合固定 Schema。

没有标量索引不代表不能过滤,但可能退化为更昂贵的扫描。

6.3 Partition 与 Partition Key

  • 手动 Partition:显式把数据分到不同 Partition,可独立 Load/Release;
  • Partition Key:按某个字段的值自动路由到物理 Partition;
  • 带 Partition Key 等值条件的查询可以减少无关数据扫描。

不要为每个小用户无限创建手动 Partition。租户很多时,Partition Key 通常比“一租户一 Partition”更可扩展。

6.4 多租户四种粒度

方式 隔离性 Schema 灵活度 规模 典型场景
Database per tenant 租户较少 强合规、独立权限
Collection per tenant 中高 中等 各租户 Schema/索引不同
Partition per tenant 较强 共享 Schema 中等 需要独立 Load/Release
Partition Key 逻辑隔离为主 共享 Schema 最大 大量小租户

多租户选择不只看数量,还要看:RBAC、跨租户查询、冷热数据、Schema 差异和资源隔离。高安全场景不能仅依赖应用层 tenant_id 过滤。

7. 一致性:为什么刚写入的数据可能搜不到

Milvus 是分布式、存算分离系统。写入先进入 WAL 和实时链路,查询节点维护自己的可服务时间。搜索时需要在“更新鲜的数据”和“更低的延迟”之间权衡。

Milvus 提供四种一致性级别:

一致性 含义 适用场景
Strong 搜索等待到最新写入可见 写后立即读、更新后必须马上查到
Session 同一客户端会话看到自己的写入 交互式应用
Bounded Staleness 允许有界陈旧,默认级别 大多数 RAG 在线检索
Eventually 不等待最新数据传播 最看重延迟、允许短暂旧数据

面试回答:

Strong 不等于检索更“相关”,它只保证数据可见性更新鲜;代价可能是 Query Node 等待进度追上,导致更高延迟。离线批量构建知识库可用 Bounded/Eventual,用户上传后立刻问答则可考虑 Session 或 Strong。

一致性、索引 Recall 和业务正确性是三件不同的事:

  • 一致性解决“最新写入是否可见”;
  • ANN Recall 解决“近邻是否被索引找到”;
  • RAG 正确性还取决于切块、Embedding、Rerank 和 LLM。

8. 用 Milvus 构建完整 RAG

8.1 离线写入流程

1
2
3
4
5
6
7
8
9
10
数据源
→ 解析 PDF/HTML/Word
→ 清洗页眉页脚、乱码、重复内容
→ 按标题/段落/语义切块
→ 添加 document_id、page_no、ACL 等元数据
→ 生成 content_hash
→ Embedding 批处理
→ 批量 Insert/Upsert Milvus
→ 建索引/加载 Collection
→ 离线检索评测

Chunking 原则

  • Chunk 太小:语义不完整,答案可能跨块;
  • Chunk 太大:噪声变多,Embedding 语义被稀释,Prompt 成本上升;
  • 固定字符切分可能截断标题、表格和代码;
  • 可以按标题和段落切分,必要时保留适度 overlap;
  • 用子块检索、返回父块是一种常见 Parent-Child Retrieval;
  • 同一文档召回多个相邻块时,要去重并控制上下文垄断。

8.2 在线查询流程

1
2
3
4
5
6
7
8
9
10
用户 Query
→ 身份、租户、权限解析
→ Query 规范化/改写/拆分
→ 生成 Query Embedding
→ Dense Top-N + BM25/Sparse Top-N
→ RRF/加权融合
→ Cross-Encoder Rerank
→ 去重、相邻块扩展、Token 预算
→ Prompt + 引用
→ LLM 生成

为什么需要 Reranker

Dense 检索通常是 Bi-Encoder:Query 和 Document 分别编码,可预计算文档向量,因此很快;但它只能通过单个向量粗略表达相关性。

Cross-Encoder Reranker 把 Query 和候选 Document 一起输入模型:

$$ score=f(q,d) $$

它能做更细粒度的词语对齐和上下文判断,但不能对整个知识库逐条推理,所以应先用 Milvus 把候选从百万级缩到几十或几百条,再精排。

8.3 RRF 融合

不同检索器的分数尺度不同,BM25 可能是几十,COSINE 通常在一个较小区间,不能直接相加。RRF 只使用排名:

$$ RRF(d)=\sum_{r\in R}\frac{1}{k+rank_r(d)} $$

优点是无需校准原始分数,适合作为强基线。限制是丢弃了原始置信度,并默认各路召回同等重要。

9. 从零运行一个 Milvus Lite Demo

Milvus 有三种主要部署方式:

  • Milvus Lite:嵌入 Python 进程,适合本地学习和小规模原型;
  • Milvus Standalone:Docker 单机服务,适合开发测试和中小规模生产;
  • Milvus Distributed:Kubernetes 分布式集群,适合大规模、高可用和独立扩缩容。

下面用 Milvus Lite 跑通“文本 → 向量 → 入库 → 过滤检索”。

9.1 安装

1
pip install -U "pymilvus[milvus-lite]" sentence-transformers

Milvus Lite 官方支持的本地平台主要是 Ubuntu 和 macOS;Windows 用户可在 WSL2 中运行本例,或改用 Docker 部署 Milvus Standalone。

9.2 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from hashlib import sha256

from pymilvus import DataType, MilvusClient
from sentence_transformers import SentenceTransformer

DB_FILE = "./milvus_demo.db"
COLLECTION = "rag_chunks"
DIM = 384

client = MilvusClient(DB_FILE)
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")

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

# 1. 定义 Schema
schema = MilvusClient.create_schema(
auto_id=False,
enable_dynamic_field=False,
)
schema.add_field("chunk_id", DataType.VARCHAR, is_primary=True, max_length=64)
schema.add_field("document_id", DataType.VARCHAR, max_length=128)
schema.add_field("tenant_id", DataType.VARCHAR, max_length=64)
schema.add_field("title", DataType.VARCHAR, max_length=256)
schema.add_field("content", DataType.VARCHAR, max_length=4096)
schema.add_field("page_no", DataType.INT32)
schema.add_field("is_active", DataType.BOOL)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=DIM)

# 2. 定义索引。Lite 会使用 FLAT;Standalone/Distributed 再按评测选 HNSW/IVF/DiskANN
index_params = client.prepare_index_params()
index_params.add_index(
field_name="embedding",
index_name="embedding_idx",
index_type="AUTOINDEX",
metric_type="COSINE",
)

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

# 3. 准备文档
docs = [
{
"document_id": "leave_policy",
"tenant_id": "demo",
"title": "请假制度",
"content": "员工申请年假需要至少提前三个工作日在系统中提交。",
"page_no": 1,
"is_active": True,
},
{
"document_id": "email_policy",
"tenant_id": "demo",
"title": "账号管理制度",
"content": "劳动关系终止后,企业邮箱账号保留七日,随后自动停用。",
"page_no": 3,
"is_active": True,
},
{
"document_id": "travel_policy",
"tenant_id": "other_company",
"title": "差旅制度",
"content": "高铁出行应优先购买二等座。",
"page_no": 2,
"is_active": True,
},
]

texts = [doc["content"] for doc in docs]
vectors = model.encode(
texts,
normalize_embeddings=True,
show_progress_bar=False,
).tolist()

rows = []
for doc, vector in zip(docs, vectors):
chunk_key = f'{doc["document_id"]}:{doc["page_no"]}:{doc["content"]}'
rows.append({
"chunk_id": sha256(chunk_key.encode("utf-8")).hexdigest(),
**doc,
"embedding": vector,
})

# 4. 批量写入
insert_result = client.insert(collection_name=COLLECTION, data=rows)
print("insert:", insert_result)

# 5. 查询向量和权限过滤
query = "离职以后公司邮箱还能使用多长时间?"
query_vector = model.encode(
[query],
normalize_embeddings=True,
show_progress_bar=False,
).tolist()

result = client.search(
collection_name=COLLECTION,
data=query_vector,
anns_field="embedding",
search_params={"metric_type": "COSINE", "params": {}},
filter='tenant_id == "demo" and is_active == true',
limit=3,
output_fields=["document_id", "title", "content", "page_no"],
)

for hit in result[0]:
print(hit["distance"], hit["entity"])

如果切换到 Standalone/Distributed,核心业务代码不需要重写,只需替换连接方式:

1
2
3
4
client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus",
)

注意:Milvus Lite 用于学习和小规模原型,不代表 Distributed 的性能、组件和容灾能力。Lite 当前仅使用 FLAT 向量索引、只提供 Strong Consistency,也不支持 Partition 和 RBAC;过滤表达式可以运行,但不能用它验证分布式标量索引性能。

10. Dense + BM25 混合检索示例

以下代码展示核心结构,Embedding 生成部分省略。BM25 Function 会把 content 自动转换为 Sparse Vector。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from pymilvus import (
AnnSearchRequest,
DataType,
Function,
FunctionType,
MilvusClient,
)

client = MilvusClient(uri="http://localhost:19530", token="root:Milvus")

schema = client.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field("chunk_id", DataType.VARCHAR, is_primary=True, max_length=64)
schema.add_field(
"content",
DataType.VARCHAR,
max_length=8192,
enable_analyzer=True,
)
schema.add_field("tenant_id", DataType.VARCHAR, max_length=64)
schema.add_field("dense", DataType.FLOAT_VECTOR, dim=1024)
schema.add_field("sparse", DataType.SPARSE_FLOAT_VECTOR)

bm25 = Function(
name="content_bm25",
input_field_names=["content"],
output_field_names=["sparse"],
function_type=FunctionType.BM25,
)
schema.add_function(bm25)

indexes = client.prepare_index_params()
indexes.add_index(
field_name="dense",
index_name="dense_idx",
index_type="AUTOINDEX",
metric_type="COSINE",
)
indexes.add_index(
field_name="sparse",
index_name="sparse_idx",
index_type="SPARSE_INVERTED_INDEX",
metric_type="BM25",
params={"inverted_index_algo": "DAAT_MAXSCORE"},
)

client.create_collection(
collection_name="hybrid_chunks",
schema=schema,
index_params=indexes,
)

# 写入时提供 chunk_id、content、tenant_id、dense;sparse 由 BM25 Function 生成
# client.insert(collection_name="hybrid_chunks", data=rows)

query_text = "ORA-01555 是什么原因?"
query_dense = [...] # 使用与文档 dense 相同的 Embedding 模型生成 1024 维向量
expr = 'tenant_id == "demo"'

dense_req = AnnSearchRequest(
data=[query_dense],
anns_field="dense",
param={},
limit=50,
expr=expr,
)

sparse_req = AnnSearchRequest(
data=[query_text],
anns_field="sparse",
limit=50,
expr=expr,
)

rrf = Function(
name="rrf",
input_field_names=[],
function_type=FunctionType.RERANK,
params={"reranker": "rrf", "k": 60},
)

results = client.hybrid_search(
collection_name="hybrid_chunks",
reqs=[dense_req, sparse_req],
ranker=rrf,
limit=20,
output_fields=["chunk_id", "content"],
)

这里的 RERANK 是 Milvus 内部对多路检索结果做 RRF/加权融合,不等同于 Cross-Encoder 模型精排。常见完整流程仍然是:

1
Milvus Dense + BM25 → RRF Top-50 → 外部 Cross-Encoder → Top-5 → LLM

11. Insert、Upsert、Delete、Flush、Load

11.1 Insert 与 Upsert

  • insert 用于新增;不要假设它会为你检查主键重复;
  • upsert 根据主键插入或替换,适合文档更新和幂等写入;
  • 更新 Embedding 时应同步更新 content_hash、模型版本和时间;
  • 文档级更新要避免旧 Chunk 残留,可以先写新版本、切换 is_active,再延迟删除旧版本。

一个稳妥的版本切换模式:

1
2
3
4
5
写入 document_version = v2
→ 验证 v2 完整且可检索
→ 将路由/过滤切到 v2
→ 标记 v1 inactive
→ 异步删除 v1

这比“先删旧数据再写新数据”更容易避免知识库空窗。

11.2 Flush

flush() 强制推进数据持久化,但不应在每次小批量 Insert 后调用。频繁 Flush 会制造过多小 Segment,增加索引和 Compaction 压力。

11.3 Load 与 Release

  • Collection/Partition 需要加载后才能高效参与查询;
  • Load 把所需历史数据和索引加载到查询资源;
  • Release 释放查询资源,不等于删除持久化数据;
  • 热数据保持加载,冷数据可按业务和 Partition 管理。

12. 为什么选择 Milvus,而不是别的向量数据库

先给结论:没有对所有场景都最好的向量数据库,只有与现有技术栈、数据规模和团队能力最匹配的选择。

12.1 对比表

方案 更适合什么 相对 Milvus 的优势 相对 Milvus 的不足/取舍
Faiss 单机算法实验、离线索引、GPU 检索 底层算法丰富、轻量、控制力强 是检索库而非完整数据库;持久化、增量更新、过滤、分布式、高可用要自己做
pgvector 已有 PostgreSQL,中小规模,强事务/Join 数据和业务表共库、SQL/事务/生态成熟、运维简单 专用向量工作负载与独立扩缩容能力较弱;大规模向量可能与 OLTP 争资源
Elasticsearch 已有 ES,全文搜索、日志/搜索平台 BM25、文本分析器、聚合和搜索生态强,混合搜索自然 纯向量大规模成本与专用调优可能不是最优;ES 集群本身也复杂
Qdrant 想要较轻量服务、强 payload filtering API 简洁、过滤体验好、部署相对直接、支持分布式和混合查询 在超大规模存算分离、索引/硬件选择广度上需结合实测比较
Weaviate 快速 AI 应用、模块化向量化、多租户 开箱即用的 BM25 Hybrid、模块和对象模型友好 自托管集群仍有运维成本;架构、资源模型与 Milvus 不同,需要用真实负载验证
Pinecone 不想自建、全托管 Serverless 运维负担小、快速上线、按服务使用 闭源托管、供应商绑定、成本和数据合规需评估,底层控制较少
Chroma 本地原型、教学、开发者体验 上手非常快,可嵌入本地;也有 Cloud/Distributed 路线 本地模式不等同于大型生产集群;生产能力要按具体部署模式评估
Milvus 大规模专用向量检索、独立扩缩容、多索引/多向量 云原生存算分离、索引类型丰富、Dense/Sparse/BM25、GPU/磁盘索引、开源 分布式自托管组件多、资源和运维门槛高;小项目可能过重

12.2 为什么不用 Faiss

Faiss 是高性能向量检索库,Milvus 的很多向量索引能力也建立在成熟 ANN 库之上。但 Faiss 不直接解决:

  • 分布式分片和副本;
  • WAL、故障恢复和在线增量更新;
  • 标量元数据过滤;
  • 多租户、RBAC、TLS;
  • 在线服务、限流、监控、备份;
  • 多节点 Top-K 归并。

因此:离线实验或单机百万级静态索引用 Faiss 很合理;要把它做成生产数据库,需要自行补齐大量工程能力。

12.3 为什么不用 pgvector

如果公司已经有 PostgreSQL,而且只有几十万到几百万向量、QPS 不高,还需要复杂事务和 Join,pgvector 往往是最务实的第一选择:

1
2
3
4
5
SELECT id, content
FROM chunks
WHERE tenant_id = 42
ORDER BY embedding <=> :query_vector
LIMIT 10;

选择 Milvus 的理由通常不是“PostgreSQL 不能搜向量”,而是:

  • 希望向量检索和 OLTP 解耦,独立扩缩容;
  • 数据达到更大规模,索引和内存成为主要成本;
  • 需要 DiskANN、GPU、多向量、Sparse/BM25 等更专用能力;
  • 查询、写入、索引构建需要独立资源池;
  • 可接受额外系统和数据同步成本。

pgvector 的 ANN + 过滤要特别测试 Recall。其 HNSW/IVFFlat 在过滤时可能需要 Iterative Scan、Partial Index 或分区才能返回足够候选。

12.4 为什么不用 Elasticsearch

如果核心需求是站内搜索、复杂分词、短语匹配、字段加权、聚合和关键词检索,Elasticsearch 很强,而且已支持向量 KNN 和 Hybrid Search。

选择 Milvus 的典型理由:

  • 向量是主工作负载而不是附加字段;
  • 数据和 QPS 需要专用向量集群扩展;
  • 更看重 ANN 索引多样性、磁盘/GPU 路径和多向量检索;
  • 团队愿意让 ES 负责文本搜索、Milvus 负责向量召回,再在应用层融合。

但如果团队已经稳定运行 ES,新增 Milvus 会引入双写、双索引一致性和额外运维,未必值得。

12.5 为什么不用 Qdrant 或 Weaviate

这两者都不是“不能用于生产”的轻量玩具,也支持过滤、分布式和混合检索。合理比较应落在真实需求:

  • 数据规模和增长速度;
  • 单租户大 Collection 还是大量小租户;
  • Dense、Sparse、BM25、多模态、多向量需求;
  • 过滤表达式和过滤比例;
  • 内存、SSD、GPU 预算;
  • 自托管复杂度和团队熟悉度;
  • P95/P99、Recall、写入吞吐和恢复时间。

选择 Milvus 可以强调其云原生存算分离、丰富索引和大规模扩展路线;选择 Qdrant/Weaviate 则可能更看重 API 体验、特定过滤/多租户模型或更简单的起步。最终必须用同一批数据和同一 Embedding 做基准测试。

12.6 为什么不用 Pinecone

Pinecone 是全托管方案,优势是无需管理底层集群,适合快速上线。选择自建 Milvus 的原因可能是:

  • 私有化、离线或数据主权要求;
  • 希望掌控索引、资源、硬件和版本;
  • 大规模稳定负载下,自建成本模型更合适;
  • 避免供应商绑定。

反过来,如果团队没有向量数据库运维能力,Pinecone 或托管 Milvus/Zilliz Cloud 可能比自建更可靠。

12.7 什么时候不该选 Milvus

  • 只有几万条静态向量;
  • 现有 PostgreSQL/Elasticsearch 已满足延迟和 Recall;
  • 强依赖跨表事务、复杂 Join;
  • 团队没有 Kubernetes、对象存储和分布式系统运维能力;
  • 无法承担业务数据与向量库之间的同步复杂度;
  • 使用全托管服务更符合成本和合规要求。

面试中的高质量结论:

我选择 Milvus 不是因为它在所有维度都最好,而是因为项目的主要矛盾是大规模向量召回、独立扩缩容、过滤和混合检索;如果项目规模小且数据本来就在 PostgreSQL,我会先用 pgvector,避免过度设计。

13. 生产调优

13.1 先建立评测集

至少包含:

  • 同义改写和自然语言问题;
  • 错误码、型号、数字、人名;
  • 否定、比较、时间范围;
  • 权限和租户过滤;
  • 热门 Query 与长尾 Query;
  • 有答案、无答案、多答案;
  • 文档刚更新和刚删除的场景。

每个 Query 标注相关 Chunk 和相关性等级,再分别评估 Dense、BM25、Hybrid、Rerank,而不是只看最终 Demo 是否“感觉不错”。

13.2 分阶段调参

  1. 固定 Chunking 和 Embedding,测 FLAT 得到 Ground Truth;
  2. 比较 HNSW、IVF、DiskANN 的 Recall-Latency 曲线;
  3. efnprobe 等搜索深度;
  4. 加入真实过滤条件重新评测;
  5. 调每路 Top-N、RRF/权重与 Rerank 候选数;
  6. 最后做端到端答案评测和压测。

不要同时修改 Chunk 大小、Embedding 模型、索引和 Reranker,否则无法判断收益来自哪里。

13.3 内存估算

仅原始 FP32 向量的大小约为:

$$ Memory=N\times d\times 4\ bytes $$

例如 1 亿条、1024 维 FP32:

$$ 10^8\times1024\times4\approx409.6\ GB $$

这还没有包括:

  • HNSW 图边;
  • 主键和标量字段;
  • Segment、缓存和执行临时内存;
  • 副本;
  • 操作系统和进程开销。

副本数为 2 时,查询侧数据占用不能简单仍按一份估算。生产容量规划应保留余量,并考虑热点 Collection 同时加载。

13.4 常见性能问题

Recall 低

  • Embedding 模型不适合领域或中英文不匹配;
  • 文档和 Query 使用了不同模型/版本;
  • 距离度量不匹配;
  • Chunking 错误;
  • ef/nprobe 太小;
  • PQ 压缩过强;
  • 过滤条件导致候选不足;
  • Top-K 太小;
  • Dense 本来就不擅长精确词,应加入 BM25。

延迟高

  • 查询节点内存不足、频繁从磁盘加载;
  • HNSW ef 或 IVF nprobe 过大;
  • 过滤字段无索引;
  • 返回过多大字段;
  • Segment 过碎、Compaction 压力大;
  • 热点租户或 Collection 资源竞争;
  • 多分片结果归并和网络开销大;
  • Query Embedding/Reranker 本身慢,误以为是 Milvus 慢。

写入慢或数据迟迟不可查

  • 每条数据单独 Insert,批量太小;
  • 频繁 Flush;
  • 索引构建/Compaction 资源不足;
  • 一致性级别与实时可见性预期不一致;
  • 写入速率超过 Streaming/Data 节点处理能力。

14. 高可用、安全和运维

14.1 高可用不是“有副本就结束”

需要分别考虑:

  • Proxy 多副本和负载均衡;
  • Coordinator 故障转移;
  • Query Node 副本与重新加载时间;
  • WAL 和对象存储可靠性;
  • etcd 高可用与备份;
  • 节点宕机后 Segment 重分配;
  • 跨可用区网络和对象存储延迟;
  • 恢复时是否还能满足 P99。

14.2 安全

  • 启用认证,不使用默认弱口令;
  • 配置 TLS;
  • 使用 RBAC 和最小权限;
  • 租户隔离不能只靠客户端传入过滤条件;
  • 敏感原文考虑加密、脱敏和审计;
  • 备份与对象存储也要有访问控制;
  • 删除数据时同时考虑主库、Milvus、缓存、备份和离线文件。

14.3 监控指标

至少关注:

  • 搜索 QPS、P50/P95/P99、错误率和超时;
  • Insert/Upsert 吞吐;
  • 写入到可见的延迟;
  • Segment 数量、Growing/Sealed 状态;
  • Index Build、Load、Compaction 队列;
  • Query/Data/Streaming 节点 CPU、内存、磁盘和网络;
  • 对象存储错误和延迟;
  • Recall 离线趋势与线上零结果率;
  • 各租户资源使用和热点分布。

只有系统指标没有检索质量指标是不够的:数据库可以 10 ms 返回完全不相关的结果。

15. 常见面试题与参考答案

15.1 为什么 RAG 需要向量数据库

RAG 要从大量非结构化文档中找到语义相关证据。Embedding 把文本映射为向量,向量数据库通过 ANN 索引降低全量距离计算成本,并提供持久化、增量更新、元数据过滤、多租户、高可用和扩缩容。小规模 RAG 不一定需要专用向量数据库,数据量和工程需求上来后才体现价值。

15.2 Milvus 与 Faiss 有什么区别

Faiss 是向量相似度检索算法库;Milvus 是数据库。Faiss 提供索引和搜索,但分布式、持久化、实时写入、标量过滤、权限、高可用和运维通常要自己实现。Milvus 把这些能力组合成在线服务。

15.3 为什么 Milvus 要存算分离

向量查询、写入、索引构建和 Compaction 的资源模型不同。存算分离后,持久数据放对象存储,Query/Data/Streaming 节点可以针对不同瓶颈独立扩缩容;计算节点故障后也能从共享存储恢复状态。代价是网络、对象存储依赖和更复杂的调度。

15.4 HNSW 和 IVF 怎么选

HNSW 用图遍历,通常适合低 Top-K、高 Recall、内存充足的在线查询;IVF 先聚类,只搜索部分桶,内存和大 Top-K 场景更灵活。HNSW 重点调 M、efConstruction、ef;IVF 重点调 nlist、nprobe。最终用真实数据比较 Recall-P99 和成本。

15.5 为什么 ANN 会丢 Recall

ANN 为了避免遍历全部向量,只搜索图的一部分、若干聚类桶或压缩后的距离,因此可能错过真正近邻。扩大 ef/nprobe、减弱量化、增加候选后精排可以提高 Recall,但会增加延迟和资源消耗。

15.6 COSINE、IP、L2 的区别

COSINE 看方向,IP 同时受方向和模长影响,L2 看欧氏距离。单位归一化后,COSINE 与 IP 排序等价,L2 也与它们存在单调关系。工程上应遵循 Embedding 模型训练时使用的度量,并保证建索引和查询一致。

15.7 Milvus 中为什么有 Growing 和 Sealed Segment

Growing Segment 负责接收实时写入并保证新数据可查询;Sealed Segment 不再写入,可以构建更高效的 ANN 索引。这样同时兼顾实时性和历史数据查询性能,之后通过 Compaction 管理碎片。

15.8 Flush 越频繁数据越安全吗

不是。写入先进入 WAL,Flush 是推进持久化,不应每条调用。过度 Flush 会产生大量小 Segment,增加索引构建、加载和 Compaction 开销。应根据批量写入、恢复目标和官方机制设计,而不是把 Flush 当成事务 Commit。

15.9 Strong Consistency 是否一定最好

不是。Strong 让搜索等待最新写入可见,适合写后立即读,但可能增加延迟。大多数知识库允许秒级更新延迟,可以使用 Bounded Staleness;同一用户上传后立即问答可考虑 Session 或 Strong。

15.10 为什么不能召回后再做权限过滤

后过滤会截断候选。如果无权限数据占据 Top-K,过滤后结果数量不足,而本来相关且有权限的文档没有机会进入候选。权限条件应进入 Dense、Sparse 和缓存等所有召回路径。

15.11 为什么还要 BM25

Dense 擅长同义表达,但可能模糊错误码、数字、型号和罕见实体;BM25 擅长精确词面匹配。两者并行召回后用 RRF 或加权融合,通常比单路更稳健。

15.12 Milvus 内部 Rerank 与 Cross-Encoder 有什么区别

Milvus 的 RRF/Weighted Ranker 主要融合多路召回结果,通常基于排名或分数;Cross-Encoder 会联合编码 Query 和 Document,做更昂贵但更精细的相关性判断。前者解决融合,后者解决候选内部精排。

15.13 如何保证文档更新不重复

使用稳定主键或内容哈希,Upsert 时记录 document_version、chunk_version 和 embedding_version。推荐先写新版本、验证后切换 active 标记,再异步删除旧版本,避免先删后写造成空窗。还要监控旧版本残留。

15.14 Milvus 为什么比 pgvector 更适合大规模向量

不是简单说“更快”,而是 Milvus 把查询、写入、索引和共享存储拆开,可以独立扩展,并提供更丰富的专用索引、多向量、Sparse/BM25、磁盘和 GPU 路径。pgvector 的优势是 SQL、事务、Join 和复用 PostgreSQL;中小规模时它可能更合适。

15.15 如何评估一个 RAG 检索系统

先标注 Query-Document 相关性,离线测 Recall@K、MRR、NDCG,再测 P95/P99、QPS 和资源。分开评估 Dense、BM25、融合、Rerank,最后测答案正确率、引用正确率、无答案拒答和端到端延迟。

15.16 数据量从 100 万涨到 1 亿怎么办

先重新做容量估算和基准测试:向量原始内存、索引开销、副本、过滤、Top-K、并发。根据内存和延迟选择 HNSW 压缩、IVF_PQ/SQ、DiskANN 或 mmap;再考虑增加 Query/Data 节点、分片、冷热分层和批量导入。不能只增加机器而不检查 Chunk 冗余和索引参数。

15.17 为什么查询慢,怎么定位

分层拆延迟:Query Embedding、网络、Milvus Search、过滤、结果回传、Reranker、LLM。Milvus 内部再看热点 Collection、Load 状态、Segment 数、索引参数、过滤索引、节点 CPU/内存/磁盘、对象存储和多分片归并。先定位阶段,再调参数。

15.18 如何选择向量数据库

我会先定义数据规模、增长率、QPS、P99、Recall、过滤、多租户、更新实时性、合规和预算,再用同一数据、Embedding、Top-K 和硬件压测候选系统。已有 PostgreSQL/ES 时优先评估复用成本;需要大规模专用向量扩缩容时再考虑 Milvus 等独立系统。

16. 容易说错的点

  • “RAG 必须用向量数据库”——错,小规模可以不用;
  • “向量数据库会生成 Embedding”——通常 Embedding 由模型生成,数据库负责存储检索;Milvus 也提供部分 Function/集成,但两层概念不能混淆;
  • “COSINE 距离越小越相似”——在 Milvus 的 COSINE similarity 语义下通常越大越相似;
  • “HNSW 一定比 IVF 好”——取决于内存、Top-K、过滤、Recall 和数据规模;
  • “索引建好后 Search 就一定准确”——ANN、Embedding、Chunking 都会影响 Recall;
  • “Strong Consistency 会提高语义相关性”——它只影响数据新鲜度;
  • “Flush 等于关系数据库 Commit”——两者语义不同;
  • “Partition 越多越快”——过多 Partition 会增加管理和调度成本;
  • “先召回再做租户过滤即可”——会泄漏风险并造成候选截断;
  • “Milvus 一定比 pgvector/ES 快”——没有统一硬件和真实负载的基准,不能这样下结论;
  • “RRF 就是模型 Reranker”——RRF 是基于排名的融合算法,不是 Cross-Encoder;
  • “只监控 QPS/P99 就够了”——检索系统必须同时监控 Recall 和业务答案质量。

17. 项目介绍模板

面试时可以按“背景—设计—难点—指标—取舍”描述:

我们为内部技术文档构建 RAG。文档解析后按标题和段落切块,保留 document_id、page_no、tenant_id、ACL 和版本信息,使用统一 Embedding 模型生成 Dense Vector 并批量写入 Milvus。在线查询同时执行 Dense 和 BM25 召回,权限条件在两路召回阶段下推,之后用 RRF 合并,再用 Cross-Encoder 将 Top-50 精排到 Top-5。索引选择不是拍脑袋,我们用 FLAT 结果作为 Ground Truth,对 HNSW 的 ef 和 IVF 的 nprobe 做 Recall-P99 曲线,最后在目标 Recall 下选择资源成本更低的方案。文档更新采用新版本先写、验证后切换 active 标记、旧版本延迟删除,避免知识库空窗。监控同时覆盖 P99、写入可见延迟、零结果率和 Recall 回归。

如果被追问“为什么选择 Milvus”:

当时向量数据需要独立扩缩容,而且后续要支持更大规模、多租户过滤、Dense+Sparse 和磁盘索引,所以没有把向量工作负载继续放在 OLTP PostgreSQL 中。Milvus 的存算分离和索引选择更适合这个方向。代价是多维护一个分布式系统和数据同步链路,所以小规模阶段我不会直接上 Distributed,而会从 Lite/Standalone 验证,再按指标迁移。

18. 速记总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
RAG:给 LLM 找证据,不是把所有知识塞进 Prompt
Embedding:对象 → 高维向量
Vector Search:按 COSINE/IP/L2 找近邻
ANN:用少量 Recall 换低延迟
Milvus:ANN + 持久化 + 过滤 + 分布式 + 多租户 + 运维

数据模型:Database → Collection → Partition → Segment → Entity
实时数据:Growing Segment
历史索引:Sealed Segment

HNSW:高 Recall、低延迟、吃内存
IVF:聚类分桶,nlist/nprobe 权衡
PQ/SQ:压缩省内存,可能损失精度
DiskANN:SSD 换内存
FLAT:准确但慢,用于小数据和 Ground Truth

一致性:Strong / Session / Bounded / Eventually
混合检索:Dense + BM25/Sparse → RRF → Cross-Encoder
权限:召回阶段过滤,不是召回后过滤
评测:Recall/NDCG + P99/QPS + 端到端答案质量

选型:
小型静态 → NumPy/Faiss/Chroma
已有 PostgreSQL → 先看 pgvector
全文搜索为主 → 先看 Elasticsearch
大规模专用向量与独立扩缩容 → Milvus 候选
不想运维 → 托管服务

参考资料

以下资料均优先使用各项目官方文档;本文以 Milvus 2.6.x 稳定 API 为主,具体参数应以部署版本文档为准。