29 切片的权限隔离(实战篇):Milvus + Spring AI,从 0 搭一条带权限过滤的检索链路

上一篇(#28)把原理讲透了:权限要沉到切片、过滤要下推检索层、多租户要隔离、边角要想全。道理都懂,但面试官最爱追的一句是——"那你具体怎么落地?"

这篇不聊理论,直接从 0 搭起来:用 Milvus + Spring AI,建表、灌数据、带权限过滤地检索,跑一条能用的链路。前端怎么展示先不管,我们只盯后端这一条线。

先看全貌。

整条链路一句话概括:文档切分时给每块打上权限元数据 → 用 Spring AI 的EmbeddingModel算向量 → 存进 Milvus → 检索时把"当前用户能看什么"翻译成一个过滤表达式,和向量相似度一起算 → 命中的切片交给ChatClient生成回答。


一、起一个 Milvus(standalone,一条命令)

开发环境不用搭集群,官方给了独立版一键脚本:

# Linux / macOS,需已装 Docker curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh -o standalone_embed.sh bash standalone_embed.sh start

Windows 上如果跑脚本不方便,用官方给的 standalone docker-compose 起也一样,对外端口不变。起来之后记两个端口:

-19530:SDK / gRPC 端口,Java 端连的是它;

-9091:健康检查与指标端口,排查"起没起来"用它。


二、schema 是这套东西的地基

Milvus 里一个 collection 就是一张"能存向量的表"。Spring AI 的MilvusVectorStore会在initializeSchema=true时帮你把表建好,默认结构就四个字段:

字段默认名存什么
主键doc_id每块切片的唯一 id
正文content切片文本
向量embedding切片的向量
元数据metadataJSON,你想带的字段都塞这儿

关键点在这里:权限字段(tenantid/dept/level/acltags)不用单独建列,全部塞进metadata这个 JSON 字段。检索时 Spring AI 的filterExpression会自动把它们翻译成 Milvus 的过滤条件——这也是这套方案最省事的地方。

配置文件大致长这样:

spring.ai.vectorstore.milvus.client.host=localhost spring.ai.vectorstore.milvus.client.port=19530 spring.ai.vectorstore.milvus.collection-name=kb_chunk spring.ai.vectorstore.milvus.embedding-dimension=1536 spring.ai.vectorstore.milvus.index-type=HNSW # 默认是 IVF_FLAT spring.ai.vectorstore.milvus.metric-type=COSINE # 默认 COSINE spring.ai.vectorstore.milvus.index-parameters={"M":16,"efConstruction":200} # HNSW 参数 spring.ai.vectorstore.milvus.initialize-schema=true # 让 Spring AI 自动建表 spring.ai.vectorstore.milvus.auto-id=true # 主键自增,省得自己生成

几个默认值面试时提一嘴很加分:embedding-dimension默认1536(正好是 OpenAItext-embedding-3-small的维度)、默认索引IVF_FLAT(改成HNSW召回通常更好,但要配M/efConstruction)、默认度量COSINE。


三、灌数据:切分 + 算向量 + 带上权限

入库的难点不在代码本身,而在"权限元数据要从来源文档继承下来"这一步——切片自己不知道它是谁,得从它出处的文档把属性带过来。

@Service public class KbIngestService { private final VectorStore vectorStore; public KbIngestService(VectorStore vectorStore) { this.vectorStore = vectorStore; } // docMeta 来自源文档表;权限属性从这里继承,而不是写死在切片里 public void ingest(DocMeta docMeta, List<String> chunks) { List<Document> docs = new ArrayList<>(); for (String text : chunks) { Map<String, Object> meta = new HashMap<>(); meta.put("tenant_id", docMeta.tenantId()); // 租户:多租户隔离的第一道 meta.put("doc_id", docMeta.docId()); // 回链原文 / 删文联动 meta.put("dept", docMeta.dept()); // 部门 meta.put("level", docMeta.level()); // 密级 meta.put("acl_tags", docMeta.aclTags()); // 白名单标签 List<String> meta.put("status", "active"); // 软删标记 docs.add(new Document(text, meta)); } // add() 内部:把每块 text 交给 EmbeddingModel 算向量 → 连 metadata 一起写进 Milvus vectorStore.add(docs); } }

vectorStore.add(docs)这一句,Spring AI 帮你把"每块的文本拿去算 embedding、再连同元数据一起落库"全做了。Document的第二个参数就是元数据 map,acl_tags直接放List<String>即可——到 Milvus 里就是一份 JSON。

留意status这个字段:它是第六节"先软删后慢清"要用的伏笔。


四、检索:把"用户能看啥"翻译成 filterExpression(全篇核心)

到了最要紧的地方。检索时不只要问"哪块最相似",还要问"哪块这个用户有权限看"。Spring AI 把它写成filterExpression:

@Service public class KbSearchService { private final VectorStore vectorStore; public KbSearchService(VectorStore vectorStore) { this.vectorStore = vectorStore; } // ctx 来自登录态(JWT / SSO),业务层无法省略 public List<Document> search(String query, AuthCtx ctx) { String filter = String.format( "tenant_id == '%s' && level <= %d && status == 'active' && (dept == '%s' || acl_tags in ['public'])", ctx.tenantId(), ctx.level(), ctx.dept()); return vectorStore.similaritySearch( SearchRequest.builder() .query(query) .topK(5) .filterExpression(filter) // 关键:过滤下推给 Milvus .build()); } }

这个表达式就是"且 / 或 + 比较 + in"这一套,跟 Milvus 的过滤语法一一对应:&&是且、==是相等、in [...]是命中集合。上面这句读作:同一租户、没被删、密级不高于用户级别、并且(属于用户部门 或 带 public 标签)——全部满足的切片,才有资格进入相似度排序。

这就是 #28 说的 pre-filter 落地版:过滤没有被搬回应用层,而是和向量相似度在 Milvus 内部一次完成。不相关的切片压根不参与计算,既不会白占 TopK 名额,也不存在"敏感内容先被搜出来再被过滤"的路径。

条件一长,手拼字符串容易在引号转义上翻车,Spring AI 也提供了 DSL:

FilterExpressionBuilder b = new FilterExpressionBuilder(); String expr = b.and( b.eq("tenant_id", ctx.tenantId()), b.lte("level", ctx.level()) ).build();

eq/ne/gt/lte/in/and/or这些都在这个 DSL 里,拼长条件时比裸字符串稳得多。

这里再重复 #28 那条铁律:过滤条件只能来自登录态,绝不能由调用方传进来。上面ctx从SecurityUtils.current()解 JWT / SSO 拿到,任何接口都改不掉——否则只要有个新接口忘了带条件,整套校验就形同虚设。


五、多租户:别指望一个 filter 兜住所有

如果你的知识库是卖给多家公司的 SaaS,只靠tenant_id ==过滤是不够硬的——一旦某个脚本、某个新接口漏传了这个条件,吐出来的就是跨公司的数据。

Milvus 里更稳的做法是分区(partition):一个租户一个分区,检索时只查对应分区。Spring AI 直接支持:

spring.ai.vectorstore.milvus.partition-name=t_acme

加了它之后,insert / delete / search 都会被限定在该分区内。真正做 SaaS 时,这个分区名由租户上下文动态决定——常见做法是用 Milvus 客户端为每个租户建好分区,检索时按当前租户切到对应分区(或按租户各建一个VectorStorebean)。

对应 #28 那张表:collection 每租户一个= 隔离最强、资源最费;partition= 同库内分区,性价比最高,多数企业从这儿起步;单库 + 一个 filter= 最省,但最险。能物理隔离就物理隔离,退一步至少要分区。

六、删文与权限变更,落到代码上

原理篇讲过"先搜不到、再删干净",这里给代码。

删文档。源文档标记删除后,别去拼字符串,用Filter.Expression:

// 立即不可见:把该文档的切片打上软删标记(重新写一遍 status) // 或者直接用表达式删除 vectorStore.delete(new FilterExpressionBuilder() .eq("doc_id", docMeta.docId()) .build());

delete(Filter.Expression)会按条件删掉对应切片,检索立刻搜不到;剩下的物理空间回收交给 Milvus 后台的 compaction 慢慢做。如果你要"软删优先、物理清理放到后面",就更简单:检索时的filterExpression里带上status == 'active'(上面的代码已经带了),删文只是把status改掉,索引层的清理由后台异步完成。

权限变更。分两种:

-文档属性变了(一份内部文档降级为公开):把这份doc_id的切片重新灌一遍(先删后插,或覆盖),新属性立刻生效;

-用户调岗了:什么都不用动。因为用户权限是检索时从登录态实时算的(ctx),切片里只存了文档属性。这正是 #28 强调的那条线——文档属性写进切片,用户权限每次实时算——守住了它,人员变动就不必碰向量库。


七、面试官视角的标准回答

面试官问:"你说权限过滤要下推检索层,具体怎么落地?"

用 Milvus + Spring AI 这套来讲:切分入库时,把权限属性(租户、部门、密级、ACL 标签)写进每块切片的 metadata;检索时把当前登录用户的权限翻译成filterExpression,比如tenantid == '...' && level <= 3 && status == 'active' && (dept == '...' || acltags in ['public']),交给 Milvus 和向量相似度在一次查询里完成,而不是搜完再在应用层过滤——那样既丢召回,又有把敏感内容送进模型的风险。<br><br>条件从登录态强制注入,调用方无权传。多租户用 partition 做隔离,别只押一个tenant_id过滤;删文档用表达式删除加软删标记,检索先排除status != 'active';权限变更靠文档属性重灌 + 用户权限实时计算,两件事分开。<br><br>一句话:过滤在检索层、隔离在分区层、时效在实时计算,三层各管各的。

八、跑起来之后的验证清单

写完不算数,得能自证。造点数据,按下面四条过一遍:

1.造两个租户、两个密级的数据,用不同用户的ctx搜同一个 query,确认结果集不串;

2.故意把 filter 里的tenant_id去掉,确认能搜到别的租户的切片——反过来证明这条条件就是"命门",少一个都不行;

3.删一篇文档再搜,确认立刻搜不到;

4.把用户的level从 3 调到 1,确认高密级切片这次搜不出来(实时生效,不用重建索引)。

四条全过,这套权限隔离就算立住了。


Milvus 的路子走通了。如果你的向量库是 pgvector,思路完全一样,只是把filterExpression换成 SQL 的WHERE(#> 26那篇里写过完整写法)。下一篇可以聊点别的——比如把同一套检索用 LangChain4j 再实现一遍,看看两套框架的写法差在哪。

相关新闻

微信小程序实验:计时器

微信小程序实验:计时器

本案例实现倒计时计时器&#xff0c;页面初始 2 秒空白后展示计时面板&#xff0c;支持开始计时、停止计时、重新计时&#xff1b;倒计时初始值 60&#xff0c;暂停后可继续计时&#xff0c;计数到 0 自动停止。拓展功能使用 CSS 伪元素在计时框增加背景文字。知识点&#xff1…

2026/10/21 2:24:52 阅读更多 →
监控Kubernetes集群的GPU资源(一)

监控Kubernetes集群的GPU资源(一)

对于SRE团队来说&#xff0c;实现监控AI、高性能计算平台上大规模GPU资源&#xff0c;至关重要。SRE团队可以通过GPU指标了解工作负载等相关性能&#xff0c;从而优化资源分配&#xff0c;提升资源利用率及异常诊断&#xff0c;以提高数据中心资源的整体效能。除了SRE及基础设施…

2026/10/21 2:24:52 阅读更多 →
算法复杂度分析笔记:主定理详解

算法复杂度分析笔记:主定理详解

文章目录&#x1f4dd; 算法复杂度分析笔记&#xff1a;主定理详解1. 题目回顾2. 核心工具&#xff1a;主定理 (Master Theorem)&#x1f511; 参数含义⚖️ 比较逻辑3. 解题步骤演示第一步&#xff1a;提取参数第二步&#xff1a;计算临界值第三步&#xff1a;比较 f(n)f(n)f(…

2026/10/21 2:23:52 阅读更多 →

最新新闻

数据获取与处理实战:从数据源选择到数据清洗的完整指南

数据获取与处理实战:从数据源选择到数据清洗的完整指南

这几年做数据相关的工作&#xff0c;被问得最多的一个问题是&#xff1a;“我该从哪儿拿数据&#xff1f;拿到手之后又该怎么处理&#xff1f;”说实话&#xff0c;大多数人在第一步就卡住了&#xff0c;要么不知道怎么稳定地拿到数据&#xff0c;要么千辛万苦拿到一份Excel&am…

2026/10/21 3:08:09 阅读更多 →
C# 调用 QT 带界面 DLL:窗口嵌入与消息循环实战

C# 调用 QT 带界面 DLL:窗口嵌入与消息循环实战

简介&#xff1a;本资源面向需要在 C# 项目中复用 C Qt 界面组件的开发者&#xff0c;重点解决跨语言调用外部 UI DLL 的集成问题。资源以控制台程序调用 Qt 编译生成的界面 DLL 为主线&#xff0c;涵盖 DLL 放置路径与代码引用路径的对应关系&#xff0c;适合具备一定 C# 与 C…

2026/10/21 3:08:09 阅读更多 →
WorkTool:5 步实现无需 Root 的企业微信自动化

WorkTool:5 步实现无需 Root 的企业微信自动化

WorkTool&#xff1a;5 步实现无需 Root 的企业微信自动化 【免费下载链接】worktool 一款安全稳定的Android无障碍服务工具&#xff0c;支持控制企微/微信来运行的无人值守群管理企业微信机器人 项目地址: https://gitcode.com/GitHub_Trending/wo/worktool WorkTool 是…

2026/10/21 3:08:09 阅读更多 →
纯Delphi实现屏幕找图:模板匹配算法与性能优化实战

纯Delphi实现屏幕找图:模板匹配算法与性能优化实战

简介&#xff1a;这份Delphi图像匹配资源面向需要在Windows桌面环境中实现屏幕找图的开发者&#xff0c;适用于自动化测试、游戏辅助、屏幕抓取等场景&#xff0c;帮助解决从屏幕截取到目标定位的完整链路问题。资源包为rar压缩格式&#xff0c;整体约6.63MB&#xff0c;上游未…

2026/10/21 3:08:09 阅读更多 →
DNGM(1,1)离散灰色模型:小样本预测的Python实战与GM(1,1)对比

DNGM(1,1)离散灰色模型:小样本预测的Python实战与GM(1,1)对比

做数据分析的人&#xff0c;大概率都撞上过同一个尴尬&#xff1a;想预测下个月或者下一周期的数值&#xff0c;手里却只有五六个、七八个历史样本。经典统计模型要求大样本&#xff0c;深度学习更是无底洞式地吃数据&#xff0c;这俩在“贫数据”面前基本束手无策。灰色预测就…

2026/10/21 3:08:09 阅读更多 →
无人机航拍小目标检测数据集构建:从场景设计到智慧交通与应急救援落地

无人机航拍小目标检测数据集构建:从场景设计到智慧交通与应急救援落地

我做无人机视觉落地也有几年了&#xff0c;今年被问得最多的问题不是“模型怎么调”&#xff0c;而是“模型怎么到了现场就不灵”。很多朋友拿着公开的航拍检测模型&#xff0c;在桌面演示上mAP能到七八十个点&#xff0c;可一部署到国内的十字路口、老小区、城乡结合部&#x…

2026/10/21 3:07:07 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →