Files

5.0 KiB
Raw Permalink Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week
id title type domain stack tags level prerequisites est_hours related week
ai.vector-db.scenario.multi-tenant-index 多租户知识库的向量索引:隔离、过滤召回和增量更新 scenario ai
vector-db
postgresql
多租户
元数据过滤
增量更新
召回率
SaaS
advanced
ai.vector-db.selection-pgvector
2.5
ai.rag.scenario.permission-aware-kb
ai.vector-db.interview
6

多租户知识库的向量索引:隔离、过滤召回和增量更新

业务背景与问题

一个面向中小企业的 SaaS 客服产品,每个企业(租户)上传自己的知识库。上线三个月后出现:

  1. 一个小租户(2000 个块)提问,经常返回“没有找到相关资料”,但资料明明存在;
  2. 一次代码改动漏了租户条件,A 公司的客服看到了 B 公司的产品价格;
  3. 大租户每天更新上千篇文档,删除的文档还会被检索到;
  4. 产品想换一个效果更好的嵌入模型,不知道怎么切换。

约束

  • 租户数千个,大小差异很大(几千到几百万块)。
  • 数据存储在 PostgreSQL + pgvector,不想引入新的组件。
  • 租户之间的数据绝对不能串。

方案设计

  1. 隔离方式按租户大小分层:
    • 普通租户共用一张表,每行带 tenant_id;
    • 超大租户单独分区(PostgreSQL 声明式分区或单独的表),有自己的 HNSW 索引。
  2. 问题 1(小租户召回不足)的原因:全表 HNSW 索引先找全局近邻,再过滤 tenant_id,小租户的数据很少出现在全局候选中。解决:开启 pgvector 0.8 的迭代索引扫描 SET hnsw.iterative_scan = relaxed_order;或者对小租户直接走精确检索(tenant_id 索引过滤后计算距离,几千条很快)。
  3. 问题 2(越权):租户条件不能依赖每个查询“记得加”。数据访问层封装统一的检索函数,tenant_id 是必填参数;数据库层用 行级安全(RLS) 兜底,连接时 SET app.tenant_id。
  4. 问题 3(增量更新):每个块带 doc_id、version、content_hash;文档更新在一个事务里删除旧块、插入新块;文档删除同步删除块;内容哈希不变的块跳过重新嵌入,节省成本。
  5. 问题 4(换嵌入模型):新增一列(或新表)存新模型的向量,后台批量回填,回填完成后对比评测集,再切换读路径,最后删除旧列。块表记录 embed_model 字段。

关键代码

-- 行级安全:即使查询漏了 WHERE tenant_id,也只能看到当前租户的数据
ALTER TABLE chunks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON chunks
    USING (tenant_id = current_setting('app.tenant_id'));
-- 注意:表的所有者和超级用户默认绕过 RLS,应用要用单独的普通角色连接,
-- 或者对表执行 ALTER TABLE chunks FORCE ROW LEVEL SECURITY。

-- 每个请求(事务)开始时设置租户
BEGIN;
SELECT set_config('app.tenant_id', 'tenant-42', true);   -- true 表示只在当前事务有效
SET LOCAL hnsw.iterative_scan = relaxed_order;            -- pgvector 0.8+,过滤后结果不足时继续扫描
SELECT id, content FROM chunks
ORDER BY embedding <=> $1
LIMIT 10;
COMMIT;

-- 文档更新:在一个事务里替换
BEGIN;
DELETE FROM chunks WHERE tenant_id = $1 AND doc_id = $2;
INSERT INTO chunks (tenant_id, doc_id, version, content, embedding) VALUES (...);
COMMIT;

过滤查询和召回率对比的可运行示例:examples/vector-stores/pgvector_demo.py。

取舍

方案 优点 缺点
共享表 + tenant_id 过滤 简单,租户多时管理成本低 过滤召回问题;越权风险靠 RLS 兜底
每租户一张表 / 一个集合 隔离彻底,索引小 租户上千时表和索引数量爆炸
分层(本方案) 兼顾 有两套路径,代码复杂一点
独立数据库(大客户私有化) 合规要求最高时 成本最高

易错点

  • 用表所有者账号连接,RLS 不生效却以为生效了。
  • set_config(..., false) 设置的是会话级变量,连接池复用连接时会串到下一个请求;应该用事务级(true)或每次请求都重新设置。
  • 删除文档只删了数据库里的记录,没删向量库 / ES 里的块(多存储时)。
  • 换嵌入模型时新旧向量混在一起检索。
  • 只用大租户做测试,没发现小租户的召回问题。评测集要按租户规模分组。

面试怎么讲

“多租户向量检索我遇到过两个问题:一是小租户召回不足,原因是全局 HNSW 先找近邻再过滤,小租户的数据很少进入候选,我用 pgvector 0.8 的迭代扫描和小租户走精确检索解决;二是租户隔离,我在数据访问层强制要求 tenant_id,数据库层用行级安全兜底,并且注意连接池下要用事务级变量。增量更新按 doc_id 在事务里替换,内容哈希没变的块不重新嵌入。”