数据库基础体系 · 第 67/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
向量数据库生产运维:摄取、版本、评测、备份、权限和成本
向量数据库进入生产环境后,难点通常不在“如何把一段文本转成向量”,而在于如何持续回答以下问题:
- 一批数据是否已经完整、可查询、可重放?
- 查询结果的变化来自模型、索引、数据库版本,还是过滤条件?
- 当前的召回率是否真实,延迟是否满足尾延迟目标?
- 数据库损坏或误删后,能否恢复到可验证的状态?
- 哪些用户可以搜索、写入、删除或管理索引?
- 向量维度、索引副本、内存和备份对象的成本如何估算?
本文以两类常见部署为边界:
- pgvector:PostgreSQL 扩展。事务、权限、备份和 SQL 生命周期主要由 PostgreSQL 提供。
- Milvus:专门的向量数据库。其集合、分区、Segment、索引、加载状态、对象存储和元数据服务共同决定数据可见性与恢复方式。
两者都可以实现近似最近邻搜索,但生产运维模型不同。不能把 pgvector 的事务语义、备份语义或权限模型直接套到 Milvus 上。
一、先定义生产中的“向量数据”
一条可生产运维的向量记录,不应只有一个浮点数组。至少需要以下几类信息:
| 字段 | 作用 |
|---|---|
id |
稳定、幂等的业务主键 |
tenant_id 或权限范围 |
多租户隔离和过滤 |
source_id、chunk_id |
追溯原始文档和分块 |
content 或内容引用 |
返回上下文、重新嵌入和审计 |
embedding |
向量本身 |
embedding_model |
生成该向量的模型标识 |
embedding_dim |
向量维度 |
metric |
余弦、内积或 L2 等距离定义 |
preprocess_version |
分词、清洗、截断、归一化等规则 |
source_version |
原始文档版本或内容哈希 |
created_at、updated_at |
生命周期管理 |
status |
是否已发布、删除或待重试 |
其中,embedding_model、embedding_dim 和 metric 不是普通的描述字段,而是检索语义的一部分。相同文本用不同模型生成的向量通常不能直接混搜;不同维度的向量不能放入固定维度的向量列;使用内积建立的索引也不应被当作余弦距离索引解释。
1. 距离、归一化和排序方向
设查询向量为 ,文档向量为 。
余弦相似度为:
余弦距离常写成:
L2 距离为:
内积为:
数据库的排序方向必须与度量一致:
- 距离越小越相似;
- 相似度或内积越大越相似。
如果先对所有向量做 L2 归一化,则:
此时:
因此,归一化向量上的 L2 排序、余弦相似度排序和内积排序具有相同的顺序。但这只是经过归一化后的数学等价,不能据此认为三种度量在原始向量上等价。
生产记录应保存实际采用的度量和归一化规则,而不是只依赖应用代码中的默认值。
二、摄取:从源数据到可查询数据
“摄取”是把外部数据变成数据库中可查询、可追踪、可重试的数据流。它至少包含:
- 读取原始对象或业务表;
- 清洗、分块和生成内容版本;
- 调用嵌入模型;
- 校验维度、数值和元数据;
- 写入向量数据库;
- 建立或更新索引;
- 确认数据可见;
- 记录成功、失败和重试状态。
这几个阶段不一定在同一个事务中完成。尤其是外部模型调用和向量数据库写入之间,通常不存在跨系统事务,因此必须设计幂等和恢复机制。
1. 幂等摄取的核心
一个可靠的摄取任务应能够重复执行而不产生重复业务记录。常见做法是:
其中 是稳定哈希函数。这样,当原文或模型版本不变时,重复任务会得到同一个 ID;原文或模型改变时,会产生新版本。
另一种做法是使用业务主键,并把历史向量放在单独的版本表或集合中。无论采用哪一种,必须明确“更新”到底意味着:
- 原地替换当前向量;
- 新增一个版本并切换发布指针;
- 删除旧向量后插入新向量;
- 在查询时按版本过滤。
这些语义不能由“重新插入一条向量”自动推断。
2. PostgreSQL/pgvector 中的摄取
下面的示例使用 PostgreSQL 和 pgvector,假定:
- PostgreSQL 已安装;
- pgvector 扩展已安装;
- 当前数据库用户有创建扩展和表的权限;
- 向量维度为 3;
- 使用余弦距离。
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE document_chunks (
id text PRIMARY KEY,
tenant_id text NOT NULL,
source_id text NOT NULL,
source_version text NOT NULL,
chunk_no integer NOT NULL,
content text NOT NULL,
embedding_model text NOT NULL,
embedding_dim integer NOT NULL,
embedding vector(3) NOT NULL,
embedding_norm double precision NOT NULL,
is_published boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (tenant_id, source_id, source_version, chunk_no,
embedding_model)
);
CREATE INDEX document_chunks_tenant_idx
ON document_chunks (tenant_id);
CREATE INDEX document_chunks_published_idx
ON document_chunks (tenant_id, is_published);
插入数据时,应用应在写入前检查向量长度和数值。若应用计算出:
embedding = [0.1, 0.2, 0.3]
norm = sqrt(0.1^2 + 0.2^2 + 0.3^2)
则 embedding_norm 应与实际向量一致。它不是 PostgreSQL 的约束自动计算值,除非额外使用生成列或触发器;因此更常见的做法是在摄取程序中校验。
INSERT INTO document_chunks (
id, tenant_id, source_id, source_version, chunk_no,
content, embedding_model, embedding_dim,
embedding, embedding_norm, is_published
)
VALUES (
'tenant-a:doc-17:v3:chunk-0:model-a',
'tenant-a', 'doc-17', 'v3', 0,
'向量索引需要在召回率和资源之间折中。',
'model-a', 3,
'[0.26726124,0.53452248,0.80178373]',
1.0,
false
)
ON CONFLICT (id) DO UPDATE
SET content = EXCLUDED.content,
source_version = EXCLUDED.source_version,
embedding_model = EXCLUDED.embedding_model,
embedding_dim = EXCLUDED.embedding_dim,
embedding = EXCLUDED.embedding,
embedding_norm = EXCLUDED.embedding_norm,
updated_at = now(),
is_published = false;
这里的 ON CONFLICT 使任务可以安全重试,但它不会自动解决“旧版本是否应继续可见”的问题。示例选择在更新后将 is_published 设为 false,表示应用需要在校验完成后显式发布。
发布动作可以在同一个 PostgreSQL 事务中完成:
BEGIN;
UPDATE document_chunks
SET is_published = false,
updated_at = now()
WHERE tenant_id = 'tenant-a'
AND source_id = 'doc-17';
UPDATE document_chunks
SET is_published = true,
updated_at = now()
WHERE id = 'tenant-a:doc-17:v3:chunk-0:model-a';
COMMIT;
如果一个文档包含多个分块,应在“所有分块写入并校验成功”后一次性切换发布状态,否则查询可能看到新旧版本混合的文档。
3. pgvector 索引建立和查询
pgvector 支持精确搜索,也支持常见的近似索引,例如 HNSW 和 IVFFlat。精确搜索可以作为评测基线:
SELECT id, content, embedding <=> '[0.2,0.4,0.8]'::vector AS distance
FROM document_chunks
WHERE tenant_id = 'tenant-a'
AND is_published = true
ORDER BY embedding <=> '[0.2,0.4,0.8]'::vector
LIMIT 5;
<=> 是余弦距离操作符。结果中的 distance 越小越相似。
创建 HNSW 索引时,操作符类必须与查询度量匹配:
CREATE INDEX document_chunks_embedding_hnsw
ON document_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
查询时可以调节搜索宽度:
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, content, embedding <=> '[0.2,0.4,0.8]'::vector AS distance
FROM document_chunks
WHERE tenant_id = 'tenant-a'
AND is_published = true
ORDER BY embedding <=> '[0.2,0.4,0.8]'::vector
LIMIT 5;
COMMIT;
SET LOCAL 只在当前事务内生效,适合按请求或按工作负载调节。搜索宽度增大通常有机会提高召回率,但也可能增加 CPU 和延迟;不能把它当作固定的质量保证。
IVFFlat 的倒排列表需要训练数据。索引创建时,表中已有的数据会影响聚类;如果在几乎空表上创建索引,后续批量导入并不会自动让初始聚类变得理想。因此常见流程是:
- 导入具有代表性的一批数据;
- 创建 IVFFlat 索引;
- 继续导入;
- 在数据分布发生明显变化时重新评估或重建索引。
CREATE INDEX document_chunks_embedding_ivf
ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
lists 不是普适最优值。它需要结合数据规模、过滤选择性、查询并发和召回目标,通过离线评测确定。
4. Milvus 中的摄取状态
Milvus 的数据生命周期与 PostgreSQL 表不同。一个集合中的数据通常会经历类似以下状态:
- 客户端提交插入;
- 数据进入可写的 growing segment;
- 数据被持久化到后端存储;
- Segment sealed;
- 为 sealed segment 建立索引;
- 集合加载索引和数据;
- 查询服务读取已加载的数据。
这意味着“插入接口返回成功”不必然等于“所有查询都已经以相同一致性看到该数据”。可见性还与请求使用的 consistency level、刷新和加载状态有关。具体行为应以目标 Milvus 版本和客户端的接口定义为准。
Milvus 的生产摄取通常要明确以下状态:
- 写入批次 ID;
- 业务主键范围;
- 是否已经 flush;
- 是否已经完成索引构建;
- 是否已经加载到查询节点;
- 失败记录是否可以重试;
- 删除是否已进入后续 compaction 和查询路径。
写入端可以使用固定主键来实现幂等。若一次批量写入中部分请求失败,不能简单地假设整批都失败或整批都成功,应根据客户端返回结果和业务主键重新核对。
Milvus 不提供与外部关系数据库之间的通用分布式事务。若原始文档在 PostgreSQL、对象存储或消息队列中,常见的可靠模式是:
- 原始系统先提交文档版本和待处理状态;
- 通过 outbox 或消息队列发布嵌入任务;
- 嵌入任务写入 Milvus;
- 读取回写状态,确认行数、主键和版本;
- 最后把原始系统中的版本标为可发布。
这样,即使 Milvus 写入成功但状态回写失败,也可以依据幂等主键重试,而不会重复生成业务记录。
三、版本:数据库、扩展、索引和嵌入模型必须分开管理
生产中的“版本”至少有四层:
- 数据库服务版本:PostgreSQL 或 Milvus 服务端版本;
- 客户端版本:SQL 驱动、pgvector 客户端库或 Milvus SDK;
- 索引和模式版本:字段、维度、度量、索引类型和参数;
- 数据与模型版本:嵌入模型、分块规则、预处理规则和原始数据版本。
只记录数据库版本而不记录模型和索引版本,无法解释检索结果变化。
1. pgvector 的扩展版本
pgvector 是 PostgreSQL 扩展。数据库中可以查询扩展版本:
SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';
SELECT version();
升级前需要确认:
- 目标 pgvector 版本已安装在服务器;
- 当前数据库中的扩展可以升级到目标版本;
- 客户端是否使用了新版本支持的类型或操作符;
- 现有索引是否需要重建;
- 升级是否会触发长时间索引构建或锁等待。
扩展升级通常由数据库管理员执行,例如:
ALTER EXTENSION vector UPDATE;
但可用的升级路径取决于实际安装的扩展包和目标版本。不能在没有检查扩展升级脚本的情况下,直接把生产环境的 PostgreSQL 大版本升级、扩展升级和索引重建合并成一个不可回滚的操作。
如果只是调整 HNSW 或 IVFFlat 的参数,通常需要新建索引或重建索引,而不是期待旧索引自动采用新参数。生产环境可采用:
- 在同一表上创建新索引;
- 观察构建资源和锁影响;
- 通过
EXPLAIN验证查询计划; - 在低峰期删除旧索引;
- 记录索引参数和构建时间。
2. Milvus 的服务端、SDK 和协议版本
Milvus 的服务端、客户端 SDK、管理工具和备份工具需要按目标版本的兼容矩阵验证。不能仅因为 SDK 可以建立连接,就认为所有集合、索引和一致性参数都具有相同语义。
升级计划应包含:
- 当前服务端和 SDK 版本;
- 集合 schema 和索引类型;
- 各类数据类型和动态字段的使用情况;
- 当前部署模式;
- 元数据和对象存储的位置;
- 备份工具是否支持目标版本;
- 回滚时是否可以恢复旧版本服务;
- 升级后索引是否需要重新构建或重新加载。
应先在与生产数据规模、索引类型、过滤条件接近的环境中执行升级演练。演练不仅要测试“服务能启动”,还要比较:
- 精确基线与 ANN 的 Recall@k;
- 过滤查询是否仍然返回足够结果;
- 插入、删除和查询的可见性;
- Segment、索引和加载状态;
- 备份恢复后的行数、主键和抽样向量。
3. 嵌入模型迁移不能原地覆盖
假设旧模型为 ,新模型为 ,其维度分别为 和 。即使两者维度相同,向量空间的坐标含义也可能不同。直接把 生成的向量覆盖到使用 建立的集合或索引中,会导致:
- 旧文档和新文档不在同一个可比较空间;
- 查询向量与文档向量的模型不一致;
- 召回率下降但数据库层面没有报错;
- 线上问题难以从距离值判断。
稳妥的迁移方式是双写或双集合:
documents
├── embedding_model_a / collection_a
└── embedding_model_b / collection_b
迁移过程可以是:
- 固化模型、分块和归一化配置;
- 为全部历史数据生成 向量;
- 在新集合或新列中建立索引;
- 用固定评测集比较质量和延迟;
- 小流量切换查询;
- 观察错误率、召回代理指标和业务指标;
- 最后停止旧集合的写入并保留回滚窗口。
四、评测:先建立精确基线,再谈近似索引
向量检索的评测必须区分三个层次:
- 距离计算是否正确;
- 近似索引是否找回了精确结果;
- 最终应用是否返回了有用答案。
不能只测“接口返回 200”或“距离看起来很小”。
1. Exact Search 和 Recall@k
对测试查询 ,在完整数据集上计算所有距离并排序,得到精确结果集:
使用 ANN 索引得到:
Recall@k 定义为:
整个测试集的平均召回率为:
其中 是查询集合。
完整算例:
k = 3
精确结果 G3 = {doc-2, doc-7, doc-9}
ANN 结果 A3 = {doc-2, doc-9, doc-11}
交集 = {doc-2, doc-9}
Recall@3 = 2 / 3 = 0.667
如果 ANN 返回了相同的三个 ID,但顺序不同,Recall@3 仍然是 1;如果业务对名次敏感,还要评估 MRR、NDCG 或 Recall@1,而不能只看集合交集。
2. 过滤条件下的召回率
过滤会改变候选集合。设过滤后的真实候选集合为:
精确基线必须在 上计算,而不是先在全库取 Top-K 再在应用层过滤。后者可能得到少于 K 条结果,并且无法测量数据库过滤执行路径的真实质量。
对于租户过滤:
SELECT id, content, embedding <=> $1::vector AS distance
FROM document_chunks
WHERE tenant_id = $2
AND is_published = true
ORDER BY embedding <=> $1::vector
LIMIT $3;
评测时要分别测试:
- 无过滤;
- 高选择性过滤,例如只剩 0.1% 数据;
- 低选择性过滤,例如保留 80% 数据;
- 多租户并发;
- 过滤字段与向量字段分布不均的情况。
近似索引在过滤条件下的召回表现,可能明显不同于无过滤查询。原因是 ANN 索引先找到的候选不一定包含足够多满足过滤条件的记录,系统可能需要扩大搜索范围、迭代扫描,或者由应用层补查。具体能力和参数以所用引擎版本为准,必须实测。
3. 端到端 RAG 评测
如果向量数据库用于 RAG,数据库层 Recall@k 还不够。至少应拆分:
- 检索召回:标注答案所在的文档或分块是否进入 Top-K;
- 检索排序:相关分块是否排在前面;
- 上下文有效性:返回内容是否足够回答问题;
- 生成正确性:答案是否符合上下文;
- 引用正确性:答案中的引用是否真的支持结论;
- 拒答能力:没有证据时是否避免编造。
例如,一个问题的标注相关分块为:
R = {chunk-3, chunk-8}
检索返回:
T5 = [chunk-1, chunk-3, chunk-10, chunk-8, chunk-12]
则:
Recall@5 = |R ∩ T5| / |R| = 2 / 2 = 1
但如果重排器只把 chunk-3 放入上下文,而截断掉 chunk-8,生成阶段仍可能缺少完整证据。因此应分别记录数据库 Top-K、重排后 Top-K 和最终送入模型的上下文。
4. 延迟指标必须看分位数
向量搜索的平均延迟容易掩盖尾延迟。至少应记录:
- P50:典型请求;
- P95:大多数请求;
- P99:尾部请求;
- 查询并发;
- 返回 K;
- 过滤选择性;
- 索引加载状态;
- 缓存命中或冷启动状态。
评测矩阵应固定查询集、向量模型、度量、K、过滤条件和并发量。否则,改变查询分布后得到的“性能提升”可能只是测试条件变了。
5. 评测结果的最小记录格式
一次可复现评测至少记录:
database_server_version
client_version
extension_or_engine_version
embedding_model
embedding_dimension
normalization_rule
metric
index_type
index_parameters
search_parameters
dataset_snapshot
query_set_version
filter_distribution
concurrency
recall_at_k
p50_ms
p95_ms
p99_ms
没有这些信息,后续无法判断召回率变化来自模型、索引参数、数据快照还是查询计划。
五、备份:备份对象不等于可恢复系统
“备份成功”只说明某个备份命令成功,不等于系统可以恢复。恢复能力必须通过实际恢复演练验证。
需要区分:
- 逻辑备份:导出 schema、数据、角色或集合内容;
- 物理备份:复制底层数据文件或存储快照;
- 增量或时间点恢复:依赖 WAL、日志或对象存储版本;
- 索引重建:恢复数据后重新生成索引;
- 配置备份:连接地址、TLS、权限、参数和部署清单。
1. PostgreSQL/pgvector 备份
逻辑备份
典型逻辑备份:
pg_dump \
--format=custom \
--file=app-$(date +%F).dump \
--dbname="$DATABASE_URL"
恢复到新数据库:
createdb restored_app
pg_restore \
--exit-on-error \
--dbname=restored_app \
app-2025-01-01.dump
逻辑备份通常包含表数据、表结构、约束、索引定义等数据库对象,但索引通常是在恢复过程中重新创建,而不是把索引内部结构当作独立数据恢复。因此恢复时间可能主要消耗在索引构建上。
恢复前必须确保目标 PostgreSQL 已安装兼容的 pgvector 扩展包。备份中的:
CREATE EXTENSION vector;
并不负责把扩展二进制文件安装到目标服务器。
验证恢复结果:
SELECT count(*) FROM document_chunks;
SELECT
embedding_model,
embedding_dim,
count(*)
FROM document_chunks
GROUP BY embedding_model, embedding_dim;
SELECT id
FROM document_chunks
WHERE is_published = true
ORDER BY embedding <=> '[0.2,0.4,0.8]'::vector
LIMIT 5;
不能只检查行数。还应检查:
- 主键是否重复;
- 向量维度和模型版本是否正确;
- 过滤字段是否完整;
- 索引是否存在并被查询计划使用;
- 恢复后的查询结果是否与备份前抽样一致。
物理备份和 PITR
如果要求恢复到某个时间点,通常需要 PostgreSQL 的物理基础备份和 WAL 归档。恢复流程大致是:
- 从基础备份恢复数据目录;
- 配置 WAL 归档读取;
- 指定恢复目标时间或事务位置;
- 启动恢复;
- 检查数据库一致性;
- 执行向量和业务数据抽样校验。
WAL 能恢复 PostgreSQL 记录的数据库变化,但不能替代嵌入模型和外部原始文件的版本管理。若数据库中的内容来自对象存储,而对象存储文件已经被覆盖,数据库 PITR 可能恢复了引用,却无法恢复对应的原始文档。
备份期间的常见风险包括:
- 在高峰期创建大型逻辑备份,增加 I/O 和网络压力;
- 备份中包含敏感文本和向量,未做加密或访问隔离;
- 只备份主库,不验证从备份恢复;
- 只备份数据,不保存扩展包、配置和角色定义;
- 只恢复数据,不重跑查询和权限验证。
2. Milvus 备份
Milvus 的数据通常涉及:
- 元数据服务;
- Segment 数据;
- 向量和标量字段;
- 索引文件;
- 对象存储;
- 部署配置和认证配置。
因此,不能把“复制某个本地目录”当成通用的 Milvus 备份方案。应使用目标 Milvus 版本支持的备份工具或云服务备份机制,并确认其覆盖范围、兼容版本和一致性边界。
恢复验证至少包括:
- 集合和 schema 是否存在;
- 字段类型、主键和向量维度是否一致;
- 记录数与源快照一致;
- 随机抽样主键和向量内容一致;
- 索引状态是否完成;
- 集合是否已加载;
- 查询是否返回结果;
- 过滤、删除和权限行为是否符合预期;
- 恢复后的延迟是否满足要求。
“恢复后能查询”仍然不充分。如果恢复过程只恢复了原始数据而没有恢复索引,查询可能暂时走较慢的路径,或者在索引未加载时无法达到原生产性能。恢复计划必须明确索引是:
- 从备份直接恢复;
- 根据备份数据重建;
- 由系统重新生成;
- 还是在恢复后由运维脚本显式创建。
不同方案对 RTO 有很大影响。
3. RPO、RTO 和恢复演练
- RPO 是允许丢失的数据时间范围。例如 RPO 为 15 分钟,意味着故障后最多接受最近 15 分钟的数据丢失。
- RTO 是恢复到可用状态所需的最长时间。
向量系统的 RTO 不仅包括服务启动,还包括:
元数据恢复
+ 对象数据恢复
+ Segment 识别
+ 索引重建或加载
+ 权限恢复
+ 查询校验
应定期执行隔离环境恢复演练,并记录:
备份时间
恢复开始和结束时间
恢复后的记录数
恢复后的索引状态
抽样查询差异
P95/P99 延迟
权限测试结果
若一次恢复演练没有实际执行查询、过滤和权限测试,就只能证明“文件可以解压”,不能证明系统可以恢复服务。
六、权限:向量本身也可能泄露信息
向量通常不可直接读懂,但不代表没有敏感性。向量可能被用于:
- 推断文档主题;
- 判断某个用户是否存在特定类型的记录;
- 结合相似搜索结果泄露原文片段;
- 通过元数据和距离结果推断租户或业务状态。
因此权限必须覆盖数据、查询、管理和备份四个层面。
1. PostgreSQL/pgvector 权限
pgvector 不创建独立的认证体系。连接认证、角色、对象权限和行级安全由 PostgreSQL 提供。
一个常见的最小权限设计是:
CREATE ROLE rag_reader LOGIN PASSWORD 'use-a-secret-manager';
CREATE ROLE rag_writer LOGIN PASSWORD 'use-a-different-secret';
CREATE ROLE rag_admin LOGIN PASSWORD 'admin-secret';
REVOKE ALL ON TABLE document_chunks FROM PUBLIC;
GRANT SELECT (
id, tenant_id, source_id, chunk_no,
content, embedding
)
ON document_chunks TO rag_reader;
GRANT SELECT, INSERT, UPDATE
ON document_chunks TO rag_writer;
GRANT USAGE, SELECT
ON SEQUENCE some_sequence
TO rag_writer;
如果表使用了自增序列,写入角色还需要对应序列权限;如果主键由应用生成,则不需要该序列权限。权限应根据实际 schema 验证,而不是照抄固定授权。
对于多租户数据,列权限不够,还需要行级安全策略。示例:
ALTER TABLE document_chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_reader_policy
ON document_chunks
FOR SELECT
USING (
tenant_id = current_setting('app.tenant_id', true)
);
请求事务中设置租户上下文:
BEGIN;
SET LOCAL app.tenant_id = 'tenant-a';
SELECT id, content
FROM document_chunks
WHERE is_published = true
ORDER BY embedding <=> '[0.2,0.4,0.8]'::vector
LIMIT 5;
COMMIT;
这里有几个重要边界:
current_setting的值必须由可信连接层设置,不能让最终用户任意传入数据库连接;- 应用连接池在事务结束后必须清理会话状态,
SET LOCAL比永久SET更安全; - 表所有者和具有绕过 RLS 权限的角色可能不受普通策略约束,必须检查实际角色;
- 管理员、备份角色和应用读角色不应共用同一身份。
也可以把租户过滤写进 SQL 的 WHERE 条件,但仅依赖应用代码容易因某个查询漏写过滤条件而越权。RLS 的作用是提供数据库层的最后一道约束,不能替代应用鉴权。
2. Milvus 权限
Milvus 的访问控制通常包括:
- 网络层可达性;
- TLS 或其他传输保护;
- 用户和角色;
- 数据库、集合或相关对象的访问权限;
- 插入、查询、删除、索引和管理操作;
- 管理员权限与普通服务账号隔离。
具体角色、权限名称和授权命令会随 Milvus 版本、部署方式和管理接口变化,应以目标版本的 RBAC 文档和权限列表为准。生产中至少建立以下身份边界:
query-service 只能查询和读取必要字段
ingest-service 只能写入、必要时执行受限更新
delete-service 只能执行经过审计的删除
index-operator 可以构建或加载索引
backup-operator 只能执行备份和恢复流程
admin 仅用于管理,不进入应用配置
同时要注意,数据库权限并不自动限制返回内容。例如查询角色有权读取某个集合,仍可能通过返回的文本字段、元数据或过大的 Top-K 暴露敏感信息。字段设计、租户过滤、结果数量和日志脱敏仍然必要。
备份权限尤其容易被忽略。备份通常包含全部租户的数据,即使普通查询账号无法读取其他租户,备份操作员也可能获得更大的数据范围。备份对象应使用单独的存储权限、加密和审计策略。
七、成本:先拆资源,再谈优化
向量数据库成本不只是“每月多少实例费用”。至少要拆成:
其中:
- :查询、写入、索引构建和压缩消耗的 CPU;
- :向量、索引、缓存和查询工作集;
- :原始向量、标量、文本、日志和对象存储;
- :跨节点查询、跨可用区访问和备份流量;
- :模型调用、GPU 或推理服务;
- :备份副本、快照、跨区域复制;
- :监控、升级、恢复演练和人工维护。
1. 向量存储的第一阶估算
若有 条向量,每条维度为 ,每个元素占 字节,则仅原始向量大小近似为:
例如:
N = 100,000,000
d = 1,536
b = 4(float32)
则:
约为 614.4 GB(十进制),还未包含:
- 行或实体的元数据;
- PostgreSQL tuple、页和索引开销;
- Milvus Segment 和对象存储组织开销;
- HNSW 图;
- IVF、PQ 或其他压缩索引;
- WAL、日志、临时构建空间;
- 副本和备份。
因此,“向量维度乘 4 字节”只能用于第一阶估算,不能作为磁盘采购容量。
2. 索引参数如何影响资源
HNSW
HNSW 为向量维护多层图结构。参数 M 大致控制每个节点的连接数量:
M增大,图通常更容易找到高质量邻居;- 索引内存和构建时间增加;
- 构建阶段 CPU、临时内存和写放大也可能增加。
搜索参数如 ef_search 控制搜索时维护的候选宽度:
- 增大通常提高召回率;
- 查询 CPU 和延迟可能增加;
- 不同数据分布下收益并不线性。
IVF
IVF 先把向量分配到若干倒排列表,查询时只探测其中一部分:
lists增大,聚类更细,单列表可能更小;- 训练和管理成本增加;
- 如果搜索探测的列表数量不够,召回率会下降;
- 如果探测过多,性能接近扫描更多数据。
PQ
PQ 用较短的编码近似原始向量,通常可显著节省内存和存储,但会产生量化误差。压缩比例越激进,距离估计越粗糙,召回率越需要通过真实数据评测。
因此,不能单独追求“索引占用最小”或“查询最快”。应绘制至少三条曲线:
Recall@k
P95/P99 latency
memory or storage per million vectors
在满足质量约束后,再选择成本较低的点。
3. pgvector 与 Milvus 的成本差异
pgvector 的优势是把向量和关系数据放在同一 PostgreSQL 中:
- 可以在一次事务中更新业务字段和向量;
- 可以复用 PostgreSQL 的备份、权限、监控和运维体系;
- 适合需要复杂 SQL、强事务和中等规模向量检索的场景。
成本压力通常来自:
- 向量和索引占用数据库内存;
- HNSW 构建对 CPU 和内存有较高要求;
- 向量查询与 OLTP 共享资源;
- 大量 ANN 查询可能影响普通事务;
- 主从复制会放大写入和存储成本。
Milvus 更强调向量检索的独立扩展:
- 计算、存储和查询服务可以按部署架构拆分;
- Segment、索引和对象存储具有自己的生命周期;
- 适合较大规模或需要独立扩展的向量工作负载。
成本压力通常来自:
- 查询节点、副本和索引加载所需内存;
- 对象存储和元数据服务;
- Segment compaction、索引构建和重加载;
- 跨组件网络;
- 为高可用设置的多副本。
无论选择哪一种,必须把“索引构建成本”和“在线查询成本”分开统计。一次模型迁移可能产生完整重嵌入、双写、双索引和双存储成本,不能只按单次查询价格估算。
八、故障路径:从摄取失败到查询异常
1. 摄取任务重复执行
表现:
- 记录数量持续增长;
- 同一文档出现多个相同分块;
- 查询结果重复;
- 删除一个文档后仍能检索到旧版本。
诊断:
SELECT source_id, chunk_no, embedding_model, count(*)
FROM document_chunks
GROUP BY source_id, chunk_no, embedding_model
HAVING count(*) > 1;
检查是否缺少稳定主键或唯一约束。Milvus 中则应按业务主键导出或查询核对,不能只看批次成功数。
恢复:
- 停止继续摄取;
- 按源版本和模型版本确定正确记录;
- 删除重复记录;
- 重新执行幂等任务;
- 检查索引和查询结果。
如果没有稳定业务 ID,只能通过内容哈希、时间和批次推断重复记录,恢复风险更高。
2. 写入成功但查询不到
pgvector 可能原因:
- 事务尚未提交;
- 查询连接未看到预期事务状态;
is_published或租户条件把记录过滤掉;- 查询操作符与索引度量不匹配;
- 查询计划没有使用预期索引;
- 连接池中残留了错误的会话参数。
Milvus 可能原因:
- 请求使用的 consistency level 尚未达到预期;
- 数据尚未 flush 或对应 Segment 尚未可查询;
- 集合或分区未加载;
- 索引仍在构建;
- 查询使用了错误的集合、分区或过滤条件。
诊断不能只重复调用查询接口,应同时检查:
业务主键是否存在
写入返回状态
数据可见性状态
索引构建状态
加载状态
过滤条件
查询度量和维度
3. 召回率下降但数据库没有报错
常见原因包括:
- 查询模型与文档模型不一致;
- 归一化规则发生变化;
- IVF 的
nprobe或类似搜索范围过小; - HNSW 的搜索宽度过小;
- 过滤条件选择性变高;
- 只评测了未过滤数据;
- 索引没有覆盖最新 Segment;
- Top-K 在重排或应用过滤后被截断;
- 文档分块规则改变。
诊断顺序应是:
- 用同一数据快照执行精确搜索;
- 比较 ANN 的 ID 集合;
- 检查模型、维度、度量和预处理版本;
- 去掉过滤条件做对照;
- 调大搜索范围做对照;
- 检查索引和加载状态;
- 再判断是否需要重建索引或重新嵌入。
如果精确搜索结果本身也变了,问题不在 ANN 索引,而在数据、模型、度量或过滤逻辑。
4. 删除和更新的误解
向量数据库中的删除通常不是立刻物理擦除所有底层空间。删除可能先记录删除标记,之后由 compaction 或后台过程回收空间。于是会出现:
- 查询可见性已经改变,但磁盘空间暂时未下降;
- 备份在不同时间点包含不同删除状态;
- 大量删除后索引或 Segment 需要整理;
- 更新操作的写放大高于预期。
如果业务要求“删除后立即不可见”,必须验证所用引擎和一致性级别是否满足该要求;如果业务要求“删除后不可恢复”,还要考虑备份、日志、对象存储版本和副本,而不能只执行一次数据库删除。
九、上线前的最小闭环
一个向量检索服务至少应完成以下闭环,而不是只完成建表和插入:
数据闭环
原始数据版本
→ 分块版本
→ 嵌入模型版本
→ 稳定主键
→ 写入确认
→ 索引完成
→ 查询可见
→ 发布状态
质量闭环
固定数据快照
→ 精确 Top-K
→ ANN Top-K
→ Recall@k
→ 过滤召回
→ P50/P95/P99
→ RAG 端到端验证
恢复闭环
备份
→ 隔离环境恢复
→ 数据校验
→ 索引恢复或重建
→ 查询校验
→ 权限校验
→ 记录 RTO/RPO
安全闭环
身份认证
→ 最小权限
→ 租户隔离
→ 返回字段控制
→ 备份保护
→ 审计与脱敏
成本闭环
向量原始大小
+ 索引大小
+ 副本
+ 内存驻留
+ 构建与查询 CPU
+ 网络
+ 备份
+ 嵌入模型调用
其中任何一环缺失,系统都可能在功能测试中正常,却在模型升级、批量导入、误删、租户扩展或流量高峰时失控。
向量数据库的生产运维,本质上是维护一个带有模型语义、近似算法和异步状态的数据系统。摄取解决“数据是否可重放”,版本解决“结果变化是否可解释”,评测解决“质量和延迟是否可证明”,备份解决“故障后是否可恢复”,权限解决“谁能看到什么”,成本解决“这种质量是否长期可承受”。只有把这些问题放在同一条数据生命周期中,向量检索才不只是一个能返回相似结果的接口,而是一个可验证、可升级和可运营的数据库服务。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:混合检索与 RAG 数据层:全文、向量、融合、重排和引用
- 下一篇:SQL 查询逻辑处理顺序:FROM、WHERE、GROUP、HAVING、SELECT 和 ORDER
- 延伸:向量索引原理:Flat、HNSW、IVF、PQ 的精度、内存和延迟
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论