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

向量数据库生产运维:摄取、版本、评测、备份、权限和成本

向量数据库进入生产环境后,难点通常不在“如何把一段文本转成向量”,而在于如何持续回答以下问题:

  • 一批数据是否已经完整、可查询、可重放?
  • 查询结果的变化来自模型、索引、数据库版本,还是过滤条件?
  • 当前的召回率是否真实,延迟是否满足尾延迟目标?
  • 数据库损坏或误删后,能否恢复到可验证的状态?
  • 哪些用户可以搜索、写入、删除或管理索引?
  • 向量维度、索引副本、内存和备份对象的成本如何估算?

本文以两类常见部署为边界:

  • pgvector:PostgreSQL 扩展。事务、权限、备份和 SQL 生命周期主要由 PostgreSQL 提供。
  • Milvus:专门的向量数据库。其集合、分区、Segment、索引、加载状态、对象存储和元数据服务共同决定数据可见性与恢复方式。

两者都可以实现近似最近邻搜索,但生产运维模型不同。不能把 pgvector 的事务语义、备份语义或权限模型直接套到 Milvus 上。


一、先定义生产中的“向量数据”

一条可生产运维的向量记录,不应只有一个浮点数组。至少需要以下几类信息:

字段 作用
id 稳定、幂等的业务主键
tenant_id 或权限范围 多租户隔离和过滤
source_idchunk_id 追溯原始文档和分块
content 或内容引用 返回上下文、重新嵌入和审计
embedding 向量本身
embedding_model 生成该向量的模型标识
embedding_dim 向量维度
metric 余弦、内积或 L2 等距离定义
preprocess_version 分词、清洗、截断、归一化等规则
source_version 原始文档版本或内容哈希
created_atupdated_at 生命周期管理
status 是否已发布、删除或待重试

其中,embedding_modelembedding_dimmetric 不是普通的描述字段,而是检索语义的一部分。相同文本用不同模型生成的向量通常不能直接混搜;不同维度的向量不能放入固定维度的向量列;使用内积建立的索引也不应被当作余弦距离索引解释。

1. 距离、归一化和排序方向

设查询向量为 qq,文档向量为 xx

余弦相似度为:

cos(q,x)=qxq2x2\operatorname{cos}(q,x)= \frac{q\cdot x}{\|q\|_2\|x\|_2}

余弦距离常写成:

dcos(q,x)=1cos(q,x)d_{\cos}(q,x)=1-\operatorname{cos}(q,x)

L2 距离为:

d2(q,x)=i=1d(qixi)2d_2(q,x)=\sqrt{\sum_{i=1}^{d}(q_i-x_i)^2}

内积为:

sIP(q,x)=qxs_{\mathrm{IP}}(q,x)=q\cdot x

数据库的排序方向必须与度量一致:

  • 距离越小越相似;
  • 相似度或内积越大越相似。

如果先对所有向量做 L2 归一化,则:

q2=x2=1\|q\|_2=\|x\|_2=1

此时:

qx22=q22+x222qx=22qx\|q-x\|_2^2 = \|q\|_2^2+\|x\|_2^2-2q\cdot x =2-2q\cdot x

因此,归一化向量上的 L2 排序、余弦相似度排序和内积排序具有相同的顺序。但这只是经过归一化后的数学等价,不能据此认为三种度量在原始向量上等价。

生产记录应保存实际采用的度量和归一化规则,而不是只依赖应用代码中的默认值。


二、摄取:从源数据到可查询数据

“摄取”是把外部数据变成数据库中可查询、可追踪、可重试的数据流。它至少包含:

  1. 读取原始对象或业务表;
  2. 清洗、分块和生成内容版本;
  3. 调用嵌入模型;
  4. 校验维度、数值和元数据;
  5. 写入向量数据库;
  6. 建立或更新索引;
  7. 确认数据可见;
  8. 记录成功、失败和重试状态。

这几个阶段不一定在同一个事务中完成。尤其是外部模型调用和向量数据库写入之间,通常不存在跨系统事务,因此必须设计幂等和恢复机制。

1. 幂等摄取的核心

一个可靠的摄取任务应能够重复执行而不产生重复业务记录。常见做法是:

id=H(tenant,source_id,chunk_id,source_version,model_version)\text{id} = H(\text{tenant}, \text{source\_id}, \text{chunk\_id}, \text{source\_version}, \text{model\_version})

其中 HH 是稳定哈希函数。这样,当原文或模型版本不变时,重复任务会得到同一个 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 的倒排列表需要训练数据。索引创建时,表中已有的数据会影响聚类;如果在几乎空表上创建索引,后续批量导入并不会自动让初始聚类变得理想。因此常见流程是:

  1. 导入具有代表性的一批数据;
  2. 创建 IVFFlat 索引;
  3. 继续导入;
  4. 在数据分布发生明显变化时重新评估或重建索引。
CREATE INDEX document_chunks_embedding_ivf
ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

lists 不是普适最优值。它需要结合数据规模、过滤选择性、查询并发和召回目标,通过离线评测确定。

4. Milvus 中的摄取状态

Milvus 的数据生命周期与 PostgreSQL 表不同。一个集合中的数据通常会经历类似以下状态:

  1. 客户端提交插入;
  2. 数据进入可写的 growing segment;
  3. 数据被持久化到后端存储;
  4. Segment sealed;
  5. 为 sealed segment 建立索引;
  6. 集合加载索引和数据;
  7. 查询服务读取已加载的数据。

这意味着“插入接口返回成功”不必然等于“所有查询都已经以相同一致性看到该数据”。可见性还与请求使用的 consistency level、刷新和加载状态有关。具体行为应以目标 Milvus 版本和客户端的接口定义为准。

Milvus 的生产摄取通常要明确以下状态:

  • 写入批次 ID;
  • 业务主键范围;
  • 是否已经 flush;
  • 是否已经完成索引构建;
  • 是否已经加载到查询节点;
  • 失败记录是否可以重试;
  • 删除是否已进入后续 compaction 和查询路径。

写入端可以使用固定主键来实现幂等。若一次批量写入中部分请求失败,不能简单地假设整批都失败或整批都成功,应根据客户端返回结果和业务主键重新核对。

Milvus 不提供与外部关系数据库之间的通用分布式事务。若原始文档在 PostgreSQL、对象存储或消息队列中,常见的可靠模式是:

  1. 原始系统先提交文档版本和待处理状态;
  2. 通过 outbox 或消息队列发布嵌入任务;
  3. 嵌入任务写入 Milvus;
  4. 读取回写状态,确认行数、主键和版本;
  5. 最后把原始系统中的版本标为可发布。

这样,即使 Milvus 写入成功但状态回写失败,也可以依据幂等主键重试,而不会重复生成业务记录。


三、版本:数据库、扩展、索引和嵌入模型必须分开管理

生产中的“版本”至少有四层:

  1. 数据库服务版本:PostgreSQL 或 Milvus 服务端版本;
  2. 客户端版本:SQL 驱动、pgvector 客户端库或 Milvus SDK;
  3. 索引和模式版本:字段、维度、度量、索引类型和参数;
  4. 数据与模型版本:嵌入模型、分块规则、预处理规则和原始数据版本。

只记录数据库版本而不记录模型和索引版本,无法解释检索结果变化。

1. pgvector 的扩展版本

pgvector 是 PostgreSQL 扩展。数据库中可以查询扩展版本:

SELECT extname, extversion
FROM pg_extension
WHERE extname = 'vector';

SELECT version();

升级前需要确认:

  • 目标 pgvector 版本已安装在服务器;
  • 当前数据库中的扩展可以升级到目标版本;
  • 客户端是否使用了新版本支持的类型或操作符;
  • 现有索引是否需要重建;
  • 升级是否会触发长时间索引构建或锁等待。

扩展升级通常由数据库管理员执行,例如:

ALTER EXTENSION vector UPDATE;

但可用的升级路径取决于实际安装的扩展包和目标版本。不能在没有检查扩展升级脚本的情况下,直接把生产环境的 PostgreSQL 大版本升级、扩展升级和索引重建合并成一个不可回滚的操作。

如果只是调整 HNSW 或 IVFFlat 的参数,通常需要新建索引或重建索引,而不是期待旧索引自动采用新参数。生产环境可采用:

  1. 在同一表上创建新索引;
  2. 观察构建资源和锁影响;
  3. 通过 EXPLAIN 验证查询计划;
  4. 在低峰期删除旧索引;
  5. 记录索引参数和构建时间。

2. Milvus 的服务端、SDK 和协议版本

Milvus 的服务端、客户端 SDK、管理工具和备份工具需要按目标版本的兼容矩阵验证。不能仅因为 SDK 可以建立连接,就认为所有集合、索引和一致性参数都具有相同语义。

升级计划应包含:

  • 当前服务端和 SDK 版本;
  • 集合 schema 和索引类型;
  • 各类数据类型和动态字段的使用情况;
  • 当前部署模式;
  • 元数据和对象存储的位置;
  • 备份工具是否支持目标版本;
  • 回滚时是否可以恢复旧版本服务;
  • 升级后索引是否需要重新构建或重新加载。

应先在与生产数据规模、索引类型、过滤条件接近的环境中执行升级演练。演练不仅要测试“服务能启动”,还要比较:

  • 精确基线与 ANN 的 Recall@k;
  • 过滤查询是否仍然返回足够结果;
  • 插入、删除和查询的可见性;
  • Segment、索引和加载状态;
  • 备份恢复后的行数、主键和抽样向量。

3. 嵌入模型迁移不能原地覆盖

假设旧模型为 M1M_1,新模型为 M2M_2,其维度分别为 d1d_1d2d_2。即使两者维度相同,向量空间的坐标含义也可能不同。直接把 M2M_2 生成的向量覆盖到使用 M1M_1 建立的集合或索引中,会导致:

  • 旧文档和新文档不在同一个可比较空间;
  • 查询向量与文档向量的模型不一致;
  • 召回率下降但数据库层面没有报错;
  • 线上问题难以从距离值判断。

稳妥的迁移方式是双写或双集合:

documents
 ├── embedding_model_a / collection_a
 └── embedding_model_b / collection_b

迁移过程可以是:

  1. 固化模型、分块和归一化配置;
  2. 为全部历史数据生成 M2M_2 向量;
  3. 在新集合或新列中建立索引;
  4. 用固定评测集比较质量和延迟;
  5. 小流量切换查询;
  6. 观察错误率、召回代理指标和业务指标;
  7. 最后停止旧集合的写入并保留回滚窗口。

四、评测:先建立精确基线,再谈近似索引

向量检索的评测必须区分三个层次:

  1. 距离计算是否正确
  2. 近似索引是否找回了精确结果
  3. 最终应用是否返回了有用答案

不能只测“接口返回 200”或“距离看起来很小”。

1. Exact Search 和 Recall@k

对测试查询 qq,在完整数据集上计算所有距离并排序,得到精确结果集:

Gk(q)G_k(q)

使用 ANN 索引得到:

Ak(q)A_k(q)

Recall@k 定义为:

Recall@k(q)=Gk(q)Ak(q)k\operatorname{Recall@k}(q) = \frac{|G_k(q)\cap A_k(q)|}{k}

整个测试集的平均召回率为:

Recall@k=1QqQRecall@k(q)\operatorname{Recall@k} = \frac{1}{|Q|} \sum_{q\in Q} \operatorname{Recall@k}(q)

其中 QQ 是查询集合。

完整算例:

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. 过滤条件下的召回率

过滤会改变候选集合。设过滤后的真实候选集合为:

Df={xDf(x)=true}D_f=\{x\in D\mid f(x)=\text{true}\}

精确基线必须在 DfD_f 上计算,而不是先在全库取 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 归档。恢复流程大致是:

  1. 从基础备份恢复数据目录;
  2. 配置 WAL 归档读取;
  3. 指定恢复目标时间或事务位置;
  4. 启动恢复;
  5. 检查数据库一致性;
  6. 执行向量和业务数据抽样校验。

WAL 能恢复 PostgreSQL 记录的数据库变化,但不能替代嵌入模型和外部原始文件的版本管理。若数据库中的内容来自对象存储,而对象存储文件已经被覆盖,数据库 PITR 可能恢复了引用,却无法恢复对应的原始文档。

备份期间的常见风险包括:

  • 在高峰期创建大型逻辑备份,增加 I/O 和网络压力;
  • 备份中包含敏感文本和向量,未做加密或访问隔离;
  • 只备份主库,不验证从备份恢复;
  • 只备份数据,不保存扩展包、配置和角色定义;
  • 只恢复数据,不重跑查询和权限验证。

2. Milvus 备份

Milvus 的数据通常涉及:

  • 元数据服务;
  • Segment 数据;
  • 向量和标量字段;
  • 索引文件;
  • 对象存储;
  • 部署配置和认证配置。

因此,不能把“复制某个本地目录”当成通用的 Milvus 备份方案。应使用目标 Milvus 版本支持的备份工具或云服务备份机制,并确认其覆盖范围、兼容版本和一致性边界。

恢复验证至少包括:

  1. 集合和 schema 是否存在;
  2. 字段类型、主键和向量维度是否一致;
  3. 记录数与源快照一致;
  4. 随机抽样主键和向量内容一致;
  5. 索引状态是否完成;
  6. 集合是否已加载;
  7. 查询是否返回结果;
  8. 过滤、删除和权限行为是否符合预期;
  9. 恢复后的延迟是否满足要求。

“恢复后能查询”仍然不充分。如果恢复过程只恢复了原始数据而没有恢复索引,查询可能暂时走较慢的路径,或者在索引未加载时无法达到原生产性能。恢复计划必须明确索引是:

  • 从备份直接恢复;
  • 根据备份数据重建;
  • 由系统重新生成;
  • 还是在恢复后由运维脚本显式创建。

不同方案对 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 暴露敏感信息。字段设计、租户过滤、结果数量和日志脱敏仍然必要。

备份权限尤其容易被忽略。备份通常包含全部租户的数据,即使普通查询账号无法读取其他租户,备份操作员也可能获得更大的数据范围。备份对象应使用单独的存储权限、加密和审计策略。


七、成本:先拆资源,再谈优化

向量数据库成本不只是“每月多少实例费用”。至少要拆成:

Ctotal=Ccompute+Cmemory+Cstorage+Cnetwork+Cembedding+Cbackup+CoperationsC_{\text{total}} = C_{\text{compute}} + C_{\text{memory}} + C_{\text{storage}} + C_{\text{network}} + C_{\text{embedding}} + C_{\text{backup}} + C_{\text{operations}}

其中:

  • CcomputeC_{\text{compute}}:查询、写入、索引构建和压缩消耗的 CPU;
  • CmemoryC_{\text{memory}}:向量、索引、缓存和查询工作集;
  • CstorageC_{\text{storage}}:原始向量、标量、文本、日志和对象存储;
  • CnetworkC_{\text{network}}:跨节点查询、跨可用区访问和备份流量;
  • CembeddingC_{\text{embedding}}:模型调用、GPU 或推理服务;
  • CbackupC_{\text{backup}}:备份副本、快照、跨区域复制;
  • CoperationsC_{\text{operations}}:监控、升级、恢复演练和人工维护。

1. 向量存储的第一阶估算

若有 NN 条向量,每条维度为 dd,每个元素占 bb 字节,则仅原始向量大小近似为:

Sraw=N×d×bS_{\text{raw}}=N\times d\times b

例如:

N = 100,000,000
d = 1,536
b = 4(float32)

则:

Sraw=100,000,000×1,536×4=614,400,000,000S_{\text{raw}} =100{,}000{,}000\times1{,}536\times4 =614{,}400{,}000{,}000

约为 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 中则应按业务主键导出或查询核对,不能只看批次成功数。

恢复:

  1. 停止继续摄取;
  2. 按源版本和模型版本确定正确记录;
  3. 删除重复记录;
  4. 重新执行幂等任务;
  5. 检查索引和查询结果。

如果没有稳定业务 ID,只能通过内容哈希、时间和批次推断重复记录,恢复风险更高。

2. 写入成功但查询不到

pgvector 可能原因:

  • 事务尚未提交;
  • 查询连接未看到预期事务状态;
  • is_published 或租户条件把记录过滤掉;
  • 查询操作符与索引度量不匹配;
  • 查询计划没有使用预期索引;
  • 连接池中残留了错误的会话参数。

Milvus 可能原因:

  • 请求使用的 consistency level 尚未达到预期;
  • 数据尚未 flush 或对应 Segment 尚未可查询;
  • 集合或分区未加载;
  • 索引仍在构建;
  • 查询使用了错误的集合、分区或过滤条件。

诊断不能只重复调用查询接口,应同时检查:

业务主键是否存在
写入返回状态
数据可见性状态
索引构建状态
加载状态
过滤条件
查询度量和维度

3. 召回率下降但数据库没有报错

常见原因包括:

  • 查询模型与文档模型不一致;
  • 归一化规则发生变化;
  • IVF 的 nprobe 或类似搜索范围过小;
  • HNSW 的搜索宽度过小;
  • 过滤条件选择性变高;
  • 只评测了未过滤数据;
  • 索引没有覆盖最新 Segment;
  • Top-K 在重排或应用过滤后被截断;
  • 文档分块规则改变。

诊断顺序应是:

  1. 用同一数据快照执行精确搜索;
  2. 比较 ANN 的 ID 集合;
  3. 检查模型、维度、度量和预处理版本;
  4. 去掉过滤条件做对照;
  5. 调大搜索范围做对照;
  6. 检查索引和加载状态;
  7. 再判断是否需要重建索引或重新嵌入。

如果精确搜索结果本身也变了,问题不在 ANN 索引,而在数据、模型、度量或过滤逻辑。

4. 删除和更新的误解

向量数据库中的删除通常不是立刻物理擦除所有底层空间。删除可能先记录删除标记,之后由 compaction 或后台过程回收空间。于是会出现:

  • 查询可见性已经改变,但磁盘空间暂时未下降;
  • 备份在不同时间点包含不同删除状态;
  • 大量删除后索引或 Segment 需要整理;
  • 更新操作的写放大高于预期。

如果业务要求“删除后立即不可见”,必须验证所用引擎和一致性级别是否满足该要求;如果业务要求“删除后不可恢复”,还要考虑备份、日志、对象存储版本和副本,而不能只执行一次数据库删除。


九、上线前的最小闭环

一个向量检索服务至少应完成以下闭环,而不是只完成建表和插入:

数据闭环

原始数据版本
→ 分块版本
→ 嵌入模型版本
→ 稳定主键
→ 写入确认
→ 索引完成
→ 查询可见
→ 发布状态

质量闭环

固定数据快照
→ 精确 Top-K
→ ANN Top-K
→ Recall@k
→ 过滤召回
→ P50/P95/P99
→ RAG 端到端验证

恢复闭环

备份
→ 隔离环境恢复
→ 数据校验
→ 索引恢复或重建
→ 查询校验
→ 权限校验
→ 记录 RTO/RPO

安全闭环

身份认证
→ 最小权限
→ 租户隔离
→ 返回字段控制
→ 备份保护
→ 审计与脱敏

成本闭环

向量原始大小
+ 索引大小
+ 副本
+ 内存驻留
+ 构建与查询 CPU
+ 网络
+ 备份
+ 嵌入模型调用

其中任何一环缺失,系统都可能在功能测试中正常,却在模型升级、批量导入、误删、租户扩展或流量高峰时失控。

向量数据库的生产运维,本质上是维护一个带有模型语义、近似算法和异步状态的数据系统。摄取解决“数据是否可重放”,版本解决“结果变化是否可解释”,评测解决“质量和延迟是否可证明”,备份解决“故障后是否可恢复”,权限解决“谁能看到什么”,成本解决“这种质量是否长期可承受”。只有把这些问题放在同一条数据生命周期中,向量检索才不只是一个能返回相似结果的接口,而是一个可验证、可升级和可运营的数据库服务。


系列导航与关联阅读

官方资料

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