SKC 知识聚类图谱 设计文档
状态 :Accepted · 2026-07-06(头部规范化 2026-07-10) 归属 :
2-knowledge/· 唯一权威(标签共现图谱建边/查询/双层展示) 读者 :改TagRelationServiceImpl建边/查询逻辑、改WebRelationController接口、排查"某个标签为什么没入图 / 总图看不到某主题 / 下钻拿不到知识"、评估把语义/向量增强层接入的开发者 不重复:
- 标签本体 CRUD / 类别 / 分型枚举
FarmTagLevelEnum语义 → 见 tag-domain.mdgetUserReadKnowids实现与"用户可读集合"心智模型 → 见 know-permission.md、../0-overview/permission-model.mdF2Event事件机制本身(谁发谁收、afterCommit时机)→ 见 ../0-overview/event-bus.md- Lucene 索引与"下钻知识列表"过滤 → 见 ../1-platform/lucene-index-design.md
- 标签选择弹窗(
tag_form.vue)→ 见 tag-select-dialog-design.md- 涉及模块:
farm2-skc-know(标签/类别/知识、分型枚举与缓存)、farm2-view(标签关系/图谱构建)、farm2-service(WebRelationController/EventViewHandle)、vue3 前端relation_graph- 原始需求见 ../../requirement/raw/SKC中基于标签共现图谱需求.md
1. 功能概述
知识聚类图谱基于标签共现(tag co-occurrence)构建,把知识库中"经常一起出现的标签"聚成一张网络。整体为双层图谱:
- 上层 · 知识聚类图谱-总图(主题导航图):全局 Top-N 核心标签及其共现边。用户从宏观视角看整个知识库有哪些主题、主题间如何关联。
- 下层 · 知识关系子图(下钻):点击总图某个标签节点,下钻展示该标签下"当前用户有权访问"的知识,以及知识之间的关系(共享标签 / 同板块)。
核心设计原则:完全无模型、无向量也 100% 可用。全部信号来自结构化数据(标签共现、标签分型、知识板块),不依赖 AI 模型或向量库;模型/向量仅作为未来可插拔增强层预留(见 §11),不影响主体运行。
┌───────────────────────────────────────────────────────┐
│ 上层:知识聚类图谱-总图(宏观,Top-N 核心标签) │
│ │
│ ●云原生 ═══ ●K8s ●客户成功 ── ●onboarding │
│ ╲ ╱ │ │
│ ●微服务 ●续约 │
│ │
│ 节点 = Top-N 核心标签(按累计共现权重) │
│ 边 = 标签共现权重 │
│ 上色 = 标签分型(领域/实体/场景/管理) │
└───────────────────────┬───────────────────────────────┘
点击某标签 ↓ 下钻
┌───────────────────────────────────────────────────────┐
│ 下层:知识关系子图(微观,查询期实时算 + 权限过滤) │
│ │
│ [知识A]──共享标签──[知识B] │
│ │ │
│ 同板块 [知识C] │
│ │ │
│ [知识D]─────────[知识E] │
│ │
│ 节点 = 知识(仅当前用户有权访问的) │
│ 边 = 共享标签 / 同板块 │
└───────────────────────────────────────────────────────┘
Copy
2. 设计决策取舍
本方案是在与业务方多轮讨论后收敛的落地版,对原始需求文档做了关键取舍:
| 议题 | 原始需求方案 | 本方案(最终落地) | 理由 |
|---|---|---|---|
| 标签分级 | 四级逐标签打级 | 四级定义在标签类别 SkcCategory 上,标签经多对多继承 |
治理粒度从"上千标签"降到"十几类别",一线零负担 |
| 合法节点池 | 受控词表 | 只有归属到"已分级类别"的标签才入图 | 天然质量闸门,挡住噪声标签 |
| 领域联合主键 | (标签名,领域) 防同名跨域 |
不做 | 同名跨域问题未经证实,避免过度设计 |
| 图谱形态 | 单层标签共现网络 | 双层:上层主题导航 + 下层知识关系子图(下钻) | 用户要的是知识,标签只是入口 |
| 语义边 | RAG 推断边(第二层) | 降级为可插拔增强层(向量近邻/RAG),本阶段不实现 | SKC 常有无模型部署,核心必须零模型可用 |
| 社区发现 | 自动聚类簇 | 不做 | 簇不稳定、可解释性差,体验负担 |
| 权限 | 展示层过滤 | 下沉到图谱边/节点(上层总图按标签可见性过滤,下层子图按可读知识过滤) | 安全内生 |
3. 标签分型(四级分级)
图谱节点的着色、尺寸、以及"是否入图",都由标签的分型决定。
3.1 四种分型
FarmTagLevelEnum(farm2-skc-know/.../enums/FarmTagLevelEnum.java):
| 枚举 | 标题 | 优先级 | 图谱语义 | 总图角色 | 节点色 | 节点尺寸 |
|---|---|---|---|---|---|---|
DOMAIN |
领域 | 1(最高) | 图谱骨架 | 骨架(唯一) | #5d74c5 深蓝 |
45 |
ENTITY |
实体 | 2 | 具体对象 | 骨架(唯一) | #67c23a 绿 |
35 |
SCENE |
场景 | 3 | 应用场景 | 卫星(可重复) | #e6a23c 橙 |
30 |
MANAGE |
管理 | 4(最低) | 管理类标签 | 卫星(可重复) | #909399 灰 |
25 |
| (无分型 null) | --- | --- | 兜底 | 不入总图 | #8e9ed7 浅蓝 |
30 |
parse(String) 对非法/空值安全返回 null。
3.2 分型定义在"标签类别"上,而非逐标签
分型字段落在 SkcCategory.taglevel (标签类别)上,标签通过多对多关系(SkcTagCategory)继承其所属类别的分型。
- 治理粒度:从"上千标签"降到"十几个类别",一线几乎零负担。管理员给类别打级,下面的标签自动继承入图资格。
- 入图闸门 :只有归属到"已设置分型的类别"的标签才会进入图谱 。未归属任何已分级类别的标签(
resolveTagLevel返回null)天然被挡在图谱之外,作为质量过滤。 - 多分型取优先级最高:一个标签可能归属多个已分级类别,取优先级数值最小者(领域 > 实体 > 场景 > 管理)作为其显示分型。入图宽松(命中任一已分级类别即入图),显示身份唯一。
3.3 分型解析与缓存
SkcTagServiceImpl.resolveTagLevel(tagId, isByCache):
- 查标签关联的全部类别(
SkcTagCategory→SkcCategory)。 - 取其中已分级类别的最高优先级分型(
computeTagLevel)。 - 结果写入缓存
FarmCacheKeys.TAG_LEVEL(30 分钟)。"已算过但不入图"缓存为空串,避免缓存穿透。
缓存在类别分型编辑/删除(SkcCategoryServiceImpl)、标签与类别关系增删改(SkcTagCategoryServiceImpl)时清除,保证分型变更即时生效。
4. 数据模型
4.1 SKC_TAG_RELATION --- 聚合共现边表
每对标签一条无向聚合边 (规范化存储,SOURCEID <= TARGETID)。图谱渲染直接读它。
| 字段 | 含义 |
|---|---|
SOURCEID / SOURCENAME |
边一端标签 id / 名称(规范化后字典序较小者) |
TARGETID / TARGETNAME |
边另一端标签 id / 名称 |
LEVELNUM |
加权后的共现权重(TF-IDF 累加值 × 100 取整存储) |
COOCCURCOUNT |
原始共现知识数(无 TF-IDF 衰减),用于增量维护与运营调试 |
RELATIONMODEL |
边来源,共现图谱固定为 KNOW_TAG |
NODEMODEL |
节点模型,固定 TAG |
注:
NAME/RELATIONKEY/CUSERKEY等为旧图谱遗留字段,共现边统一写NONE,不参与新图谱逻辑。表结构从旧的"每知识每标签对一条明细式"重构为"每标签对全局一条聚合式",LEVELNUM从旧的恒为 1 改为持久化加权共现强度。
4.2 SKC_TAG_COOCCUR_DETAIL --- 共现溯源明细表
记录每篇知识对每一对标签共现边的贡献,是聚合边的可追溯来源与增量维护基础。
| 字段 | 类型 | 含义 |
|---|---|---|
ID |
varchar(32) PK | 主键 UUID32 |
KNOWID |
varchar(32) | 贡献该共现的知识 id;删除/编辑知识时按此列精确扣减 |
TAGA |
varchar(32) | 标签对 A(SkcTag.id),规范化 TAGA < TAGB |
TAGB |
varchar(32) | 标签对 B(SkcTag.id) |
WEIGHT |
double | 该篇对此边的贡献权重(含 TF-IDF 抑制) |
CTIME |
varchar(14) | 生成时间 yyyyMMddHHmmss |
索引:idx_cooccur_know(KNOWID)、idx_cooccur_pair(TAGA, TAGB)。
4.3 两表关系
一对多:一条聚合边 = 多条明细的聚合 。明细支撑"编辑一篇知识时精确撤销其历史贡献",无需全量重算。这同时满足需求文档「每条连线可回溯至具体知识文档」的信任底座要求。DDL 见 versions/v1.3.0-v1.3.1.sql。
4.4 关系枚举
FarmRelationEnum:KNOW_TAG(知识标签共现,当前唯一落地来源)。为增强层预留VECTOR_SIM/RAG_INFER/MANUAL(见 §11)。FarmRelationNodeEnum:TAG(标签节点)。
5. 建边逻辑
5.1 入图标签筛选 getGraphTags(knowId)
取知识的标签,依次过滤:
- 停用词过滤 :命中
farm2.config.graph.stopwords配置(逗号分隔,NONE表示无)的标签名剔除。 - 分级闸门 :
resolveTagLevel == null(未归属任何已分级类别)的标签剔除。
四种分型(含管理类)均正常参与建边;管理类的"卫星化、不当枢纽"由总图渲染层(§6.1)处理,而非在建边阶段跳过。
说明:getKnowTags只返回能在SkcTag表命中的标签,纯自定义未注册标签天然不参与建边------共现锚定在受控词表上。
5.2 单篇建边 initKnowIdRelations(knowId)
幂等重建,步骤:
- 先撤销 :调
delKnowIdRelations(knowId),按明细扣减该知识历史贡献并清明细(保证编辑知识时先撤旧边、再建新边)。 - 取入图标签
getGraphTags,不足 2 个则不建边。 - TF-IDF 抑制权重 :
weight = 1 / log₂(2 + n),n为入图标签数。标签越多,单篇贡献越低,抑制"标签堆砌"式知识的过度影响。 - 无向两两建边 :对入图标签两两配对,规范化
taga < tagb后:- 插一条
SKC_TAG_COOCCUR_DETAIL明细(溯源); increaseEdge把weight累加到聚合边LEVELNUM(× 100 取整),COOCCURCOUNT + 1;不存在则新建边。
- 插一条
5.3 扣减 reduceEdge / 删除 delKnowIdRelations
delKnowIdRelations:按KNOWID查明细,逐条reduceEdge,再删明细。reduceEdge:COOCCURCOUNT - 1;归零则删除该边,否则回退LEVELNUM(不低于 0)。
5.4 全量重建 clearAllRelations
ID != '' 匹配全部,清空两张表。用于建边规则/配置变更后的数据迁移。
6. 图谱查询
6.1 上层总图 getTopicGraph(limit) --- 骨架+卫星结构
总图采用骨架+卫星布局,消除高连接度枢纽造成的"毛线球",语义分层清晰:
- 骨架 (领域
DOMAIN/ 实体ENTITY):图的主干,每个标签唯一一个节点。 - 卫星 (场景
SCENE/ 管理MANAGE):依附在骨架上,允许在不同骨架上重复展示(如"故障排查"可同时挂在"云原生""数据库"下),一眼看出该场景/管理项横跨哪些主题。
步骤:
- 取全部
KNOW_TAG共现边,按LEVELNUM降序。 - 权限过滤 :只保留"当前用户至少有一篇可读知识支撑"的标签,并据此裁剪边(两端标签都可见才保留)。避免总图泄露无权主题的标签名。实现见
getUserVisibleTagIds:按图谱边涉及的标签(数百个)反查其知识绑定行,在内存中与getUserReadKnowids求交,查询量由标签数约束而非知识总量。 - 骨架节点 :按累计共现权重(仅统计骨架端)取 Top-N(默认 50),生成唯一节点;两端都是入选骨架的边作为主干边。
- 卫星节点 :对每个卫星标签,统计它与各入选骨架的共现权重,取最强 Top-3 骨架 ,各生成一个复制节点(
id = 卫星id@骨架id唯一,显示名不变,tagid指向真实SkcTag.id),连一条依附边到该骨架。卫星-卫星边、非入选骨架的边一律丢弃。 - 节点按分型着色/定尺寸(
levelColor/levelSize),category设为分型名。
前端下钻:卫星复制节点 id 是拼接的唯一值,真实标签 id 由
FarmEchartNode.tagid承载。前端tagClickHandle用tagid ?? id下钻,保证卫星节点也能正确解析到真实标签。
6.2 下层知识 getKnowGraph(tagkey, parentTagId, limit)
给定种子标签,取其下知识(前端就地展开为星形知识节点,见 §9.1):
- 种子解析 :入参可能是标签 id(总图节点 id)或 tagkey,先按 id 查(
getSkcTagById)再按 key 兜底(getTagByKey),统一得到resolvedTagKey与seedTagId。 - 权限下沉 :取该标签下知识,用
getUserReadKnowids过滤为当前用户可读 范围,limit(默认 30)截断。 - 父骨架上下文过滤 :展开卫星复制节点时前端传入
parentTagId(父骨架标签 id),仅保留同时含父骨架标签 的知识(交集)。这样故障排查@云原生与故障排查@数据库各自展示所属领域内的知识,复制节点才具语义。骨架节点、tags 模式不传,行为不变。 - 预取每篇知识的标签集、板块集、标题。
- 返回知识节点(
data)与知识间共享标签/同板块边(links)。
前端就地展开只使用返回的知识节点
data,自行构造"知识 → 被点标签"的星形边(见 §9.1),忽略后端返回的知识间links------ 星形结构比知识两两互连更清晰。后端保留知识间边计算,供未来"知识子图"视图或其他调用方复用。卫星复制节点 id 形如
卫星id@骨架id,前端解析@后段作为parentTagId传入,并在标题展示父骨架 › 卫星上下文。
6.3 兼容旧接口 getKnowGraphs(tagKeys, level)
BFS 逐层扩展的标签关系图,被 /api/wrelation/relations(level=2)、/relations1(level=1)沿用。种子 tagkey → id,逐层调 getTagRelationByNodeIds 取邻居边,签名去重 + 交叉边裁剪。节点同样按分型着色。
7. 触发时机(何时刷新图谱)
7.1 自动 --- 知识变更事件驱动
EventViewHandle(farm2-service)监听知识对象事件:
| 动作 | 事件 | 处理 |
|---|---|---|
| 新增知识 | ADD |
initKnowIdRelations |
| 编辑知识 | UPDATE |
initKnowIdRelations(幂等,先撤旧边再建新边) |
| 删除知识 | DELETE |
delKnowIdRelations |
⚠️ 仅走
F2Event事件机制的知识增删改才会自动刷新。绕过事件的批量改库不会触发。
7.2 手动 --- 全量重建
POST /api/tagrelation/rebuildAll:先clearAllRelations清空,再分页遍历全部知识重建,带进度反馈。前端「标签管理」页的「重建图谱」按钮触发。POST /api/tagrelation/loadKnows:按指定知识 id 列表逐个重建(旧入口)。
需手动重建的场景:调整标签分型 / 停用词 / 管理类开关等规则后;首次上线初始化;清理旧图谱残留;怀疑数据不一致。
8. 接口清单
8.1 前台(/api/wrelation,WebRelationController)
| 接口 | 说明 | 入参 | 出参 |
|---|---|---|---|
POST /topic |
上层总图 | TopicGraphDto{limit?} |
GraphRelationDto{data(nodes), links} |
POST /knowgraph |
下层知识(就地展开) | KnowGraphDto{tagkey, parenttagid?, limit?} |
GraphRelationDto |
POST /relations |
兼容:标签关系图 2 层 | IdsAndIdsDto{ids1: tagkeys} |
GraphRelationDto |
POST /relations1 |
兼容:标签关系图 1 层 | 同上 | GraphRelationDto |
8.2 后台(/api/tagrelation,TagRelationController,需权限 tagRelation.*)
标准 CRUD(query/{id} 增删改查)+ POST /rebuildAll(全量重建)+ POST /loadKnows(指定知识重建)。
契约文件:docs/api-contract/farm2-service/know.openapi.yaml。
8.3 下钻知识列表 + 权限过滤
WebSearchController#tagKnow(POST /api/search/relationTagKnow):按标签 Lucene 检索知识,WebSearchService.initPopFilter 按当前用户可读范围过滤(KNOW_DOC 与 getUserReadKnowids 求交等),返回有权知识。
9. 前端实现(vue3 relation_graph)
组件位于 src/views/web_page/relation_graph/:
| 组件 | 职责 |
|---|---|
topic_graph_view.vue |
统一图谱组件。mode='topic'(总图)/ mode='tags'(种子标签图),支持就地展开知识、图例、标题点击放大。prop inDialog 标记自身是否已在放大弹窗内(弹窗内不再显示放大入口,避免弹窗套弹窗) |
graph_full_win.vue |
图谱放大弹窗包装。openWin(isOpen, tags, winMode, opts?),opts={level?, limit?} 透传当前图谱的展开度/数量,保证放大版与原图一致;内部 topic_graph_view 以 :in-dialog="true" 渲染 |
底层图形组件:views/chart/relation/echarts_relation.vue(ECharts 力导向图)。支持 watch 数据变化增量 setOption(原地增删节点);nodeClickHandle 回传完整节点数据供按类型分流(clickHandle 仍回传 id,向后兼容)。
9.1 交互 --- 原地展开(不切换视图)
标签图与知识共用一张力导向图,点击标签节点在其周围就地长出知识节点,而非跳转到独立子图:
- 点标签节点 → 先清空图上已有的知识节点,再调
/knowgraph取该标签知识 Top-10 ,作为灰色小方块 知识节点挂上,每个知识仅连一条边到被点标签(星形展开,知识之间不互连,避免密集交叉)。 - 再点同一个已展开的标签 → 收起它的知识(
nodeType区分标签/知识节点)。 - 点知识节点 → 侧边预览(
Know_view_side)。知识节点 id 带know:前缀避免与标签 id 冲突,预览时还原。 - 卫星复制节点 展开时,解析其 id 的
@后段作为parentTagId传入/knowgraph,按父骨架交集过滤知识;标题展示父骨架 › 卫星上下文。 - 点标题 :有展开知识时收起;否则(非弹窗内)点击标题将当前这张图谱放大 到弹窗展示------透传当前
mode/tags/level/limit,弹窗内是同一张图的放大版而非全局总图。已在放大弹窗内(inDialog)的实例不再显示放大入口,避免弹窗套弹窗。 - 节点按分型着色,图例置于右下角避让标题。放大弹窗去除了 el-dialog 默认 header/body 内边距,使图谱标题紧贴窗口上沿。
9.2 接入点
my_page_home.vue(个人首页,topic 模式)、know_view.vue(知识详情,tags 模式)、my_page_tag.vue(收藏标签)、tag_info_popover.vue(标签浮层,全屏窗口)。
旧组件
min_graph_view.vue/max_graph_win.vue/relation_know_win.vue已删除,全部由新组件替代。手绘嵌入图GraphPart(/api/wgraph)为独立功能,与本图谱无关。后台管理页:views/back_console/view/tagrelation/。
10. 配置项
farm2config.xml 的 graph 分组(version=1.3.1):
| key | 类型 | 默认 | 说明 |
|---|---|---|---|
farm2.config.graph.stopwords |
text | NONE |
图谱建边停用词,逗号分隔;NONE 表示无 |
变更配置后需手动重建图谱才对存量边生效。
11. 后续路线(增强层,本阶段不实现)
架构预留,仅确保接口/枚举/前端渲染为其留位,无模型环境静默跳过:
- 向量语义近邻边 (
VECTOR_SIM):知识-知识用 embedding 余弦近邻补边,解决"没共享标签但内容相关"。复用现有searchByEmbedding/ 向量检索能力。 - RAG 推断边 (
RAG_INFER):LLM 基于原文推断标签间语义关系,附证据原文 + 置信度阈值,虚线区分;人工采纳后升级为MANUAL(人工确认关系)。 - 激活开关:按"是否配置了 embedding / LLM client"自动判定,无模型环境不产生也不渲染增强层边。前端约定:增强层边一律虚线,核心层边实线。
明确暂不做:社区发现聚类、领域联合主键 / 同名跨域隔离(待问题被证实后再评估)。
12. 隐含假设与前提(AI 阅读须知)
以下前提不写在代码里、但影响图谱正确性判断。修改建边/查询/权限过滤逻辑前必读:
| 假设 | 具体含义 | 违反后果 / 排查线索 |
|---|---|---|
共现边是无向的,靠 SOURCEID <= TARGETID 规范化实现 |
TagRelationServiceImpl.initKnowIdRelations 建边时 a.getId().compareTo(b.getId()) <= 0 决定 src/tgt,findEdge / reduceEdge 也按同一规范化查找;SKC_TAG_RELATION 表没有唯一约束,规范化是唯一的重复防御 |
直接手写 SQL 或旁路 Service 插入不规范化的边 → 同一对标签出现两条边、reduceEdge 扣不到、getTopicGraph 权重翻倍。排查:"某对标签边异常" 先看 SOURCEID/TARGETID 字典序是否成立 |
SkcCategory.taglevel 是入图闸门;不打级的标签一律不参与建边 |
getGraphTags → resolveTagLevel(tagId, true) == null 直接 continue;即使标签在 SkcTag 表命中、也在知识上挂载了,只要它归属的所有 SkcCategory.taglevel 都为空/非法枚举,就永远不进 SKC_TAG_RELATION |
上线新分类忘打级 → 该类下所有标签"看得见但连不上图"。排查:"知识里挂了 A/B 两标签为什么没生成 A-B 边" → 查 SkcTagCategory 关联的 SkcCategory.taglevel |
TAG_LEVEL 缓存 30 分钟 TTL,空串代表"已算且不入图" |
FarmCacheKeys.TAG_LEVEL("图谱标签分型缓存", 5000, NONE(), minutes(30));resolveTagLevel 命中缓存后走 FarmTagLevelEnum.parse(cached),空串安全返回 null |
手动改 DB 的 SkcCategory.taglevel 但不通过 Service (SkcCategoryServiceImpl / SkcTagCategoryServiceImpl)→ 缓存不清,30 分钟内新建边仍走旧分型。排查:"改了类别分型但新知识建边没生效" → 手动清缓存或走 Service 入口 |
建边只走 F2Event;批量 DB 改动不会自动更新图谱 |
EventViewHandle.handle 只在 F2EObjectT.KNOW + ADD/UPDATE/DELETE 三个动作时调 initKnowIdRelations / delKnowIdRelations;未发事件的批量 SQL 变动、脚本导入、直接 UPDATE SKC_KNOW_TAG 全都不触发 |
SQL 导入知识后不重建图谱 → SKC_TAG_RELATION 与实际标签绑定不一致。排查线索:任何数据一致性告警先问"这个知识变更是通过 Service 层的吗" |
| 图谱建边不做知识可见性判断;权限只在查询期下沉 | initKnowIdRelations 全库统一建边,边权重 = 全体贡献之和;权限过滤在 getTopicGraph.getUserVisibleTagIds / getKnowGraph.readable 里做 |
想要"每个用户看到自己的私有图谱"?不能靠现有边表,得改建边模型(每用户一份图,成本极高,方案未采纳) |
LEVELNUM 是"权重×100 取整"的累积整数,不是共现次数 |
increaseEdge / reduceEdge 中 Math.round(weight * 100) 累加/回退;共现次数是独立字段 COOCCURCOUNT |
直接把 LEVELNUM 当计数用 → 计算 avg/median 会得到偏离 100 倍的结果。总图排序按 LEVELNUM 降序正确;但"共现了多少次"必须读 COOCCURCOUNT |
总图骨架仅取 DOMAIN / ENTITY,SCENE / MANAGE 一律降级为卫星 |
`isSkeleton(level) = level == DOMAIN | |
卫星复制节点 id 用 @ 分隔,与真实标签 id 不同 |
cloneId = satId + "@" + skId;前端 tagClickHandle 用 FarmEchartNode.tagid ?? id 下钻,并解析 @ 后段作为 parentTagId |
标签 id 里带 @ 字符 → 前端解析错乱。当前 SkcTag.id 走 FarmUUID.getUUID32()(纯十六进制),暂无风险,但若换 id 生成器需重新评估 |
下钻权限依赖 getUserReadKnowids,返回集合可能上万 |
getKnowGraph / getUserVisibleTagIds 都做内存后置过滤 (readable.contains(...)),不把集合塞 SQL IN |
若图谱扩展到"知识板块图"等需要按可读集合联表的场景,直接改成 IN 会挂 ------ 参照 permission-model.md §7.2 走 Lucene Filter 回调 |
getUserVisibleTagIds 假设 SkcKnowTag.TAGKEY 对应 SkcTag.tagkey(不是 id) |
代码走 tag.getTagkey() → SkcKnowTag.TAGKEY → 反查 readable 交集;tagkey 空的标签天然不进入可见集 (StringUtils.isNotBlank(tag.getTagkey()) 过滤) |
历史脏数据 SkcTag.tagkey 为空 → 该标签边在总图消失。排查:"某标签明明有可读知识却不显示" → 先查其 tagkey 是否为空 |
getKnowGraph 前端仅使用 data,忽略后端 links |
前端 §9.1 明确"就地展开只使用返回的知识节点,自行构造星形边";后端 links 保留仅为未来扩展 |
改后端边算法(如换成 Jaccard)时不用担心影响现有交互;但若前端要复用 links,须先测试是否与星形结构冲突 |
rebuildAll / loadKnows 在 new Thread 里跑,无事务包裹 |
TagRelationController.rebuildAll 直接 new Thread(...).start();单篇失败 catch 后继续,不回滚 |
中断/宕机 → 图谱处于"部分重建"状态。生产上跑重建后应检查 FarmProcessUtils 进度是否走到 100,未完成的手动再点一次 |
13. 反例与陷阱
13.1 不要用等值查询查"某个标签的所有共现边"
错误做法 :SELECT * FROM SKC_TAG_RELATION WHERE SOURCEID = ? 问题 :边是规范化存储的(SOURCEID <= TARGETID),单向查询只能查到该标签作为字典序较小端的那一半边,字典序较大端的边全丢。 正确做法 :WHERE SOURCEID = ? OR TARGETID = ?(getTagRelationByNodeIds 就是分两次查再用无向签名 min|max 去重)。
13.2 不要在 SkcCategory.taglevel 上写非枚举字符串
问题 :FarmTagLevelEnum.parse 用 valueOf(name.trim().toUpperCase()),非法值走 IllegalArgumentException 分支返回 null;null 分型的标签不入图 。写 "1" / "级别1" / "domain "(带空格没问题,带全角字符不行)都会静默失效。 正确做法 :DB 里必须是 DOMAIN / ENTITY / SCENE / MANAGE 四个字面量之一;后台管理页 SkcCategoryServiceImpl 保存路径已保证这点,但直接 SQL 补数据必须遵守。
13.3 不要相信"改了停用词/分型/管理开关立刻在图上生效"
问题 :farm2.config.graph.stopwords 只影响建边时 筛选;已经建的边不会因为停用词新增而消失。同理,SkcCategory.taglevel 改动只影响:(a) TAG_LEVEL 缓存清空后新算的分型(用于总图渲染上色/骨架选择),(b) 新增/更新知识时的新建边入选。存量边不会自动重算 。 正确做法 :调整规则后必须点后台的"重建图谱"按钮(POST /api/tagrelation/rebuildAll),或至少对涉及的知识 loadKnows。
13.4 不要在 EventViewHandle 里发同源 KNOW 事件
问题 :EventViewHandle.handle 监听 KNOW.ADD/UPDATE/DELETE 后调 initKnowIdRelations;如果这里再触发一次 F2Event 且对象是 KNOW,会自我雪崩 。参考 permission-model.md 中"Handler 里无用户上下文"的类似警告。 正确做法 :initKnowIdRelations 内部只操作 SKC_TAG_RELATION 与 SKC_TAG_COOCCUR_DETAIL,不要发事件;若确需通知下游,用不同 F2EObjectT(如 TAG_RELATION)避免回环。
13.5 不要期望超级用户能看到"全部标签"的总图
问题 :getUserVisibleTagIds 直接调 getUserReadKnowids ------ 参照 permission-model.md §7.4:getUserReadKnowids 不为超管特殊短路 。超管的可读知识集仍受其岗位授权约束,若超管没归属任何有 KNOW_READ 权限的岗位,readable 为空,getTopicGraph 返回空图。 排查线索:"管理员打开总图空白" → 先查超管账号的岗位授权,不是图谱 bug。
13.6 不要用 SkcTag.id 直接查 SKC_KNOW_TAG
问题 :SKC_KNOW_TAG.TAGKEY 存的是 SkcTag.tagkey(业务 key),不是 SkcTag.id(UUID32)。getUserVisibleTagIds 中间要做一次 id ↔ tagkey 的映射,就是这个原因。 正确做法 :任何"通过标签反查知识"的路径必须先解析到 tagkey 再查 SkcKnowTag;SkcKnowServiceImpl.getTagKnows(tagkey) / getKnowTags(knowId) 已封装这一层。
13.7 不要把 getKnowGraph 的入参 tagkey 当纯 tagkey 用
问题 :getKnowGraph(tagkey, parentTagId, limit) 的第一个参数同时接受 标签 id 和 tagkey ------ getSkcTagById(tagkey) 命中就当 id 用,否则回退到 getTagByKey。这是为了兼容总图节点 id(是标签 id)和 /relations 接口(是 tagkey)。 陷阱 :若某标签的 tagkey 与另一标签的 id 哈希冲突(极小概率但存在),会解析到错的标签。当前 UUID32(十六进制 32 位)与 tagkey(业务生成)不同名空间,冲突未观测到,但重构 id 生成器时须重新评估。
13.8 不要用 SKC_TAG_COOCCUR_DETAIL 做业务展示
问题 :明细表只是"扣减/溯源"的支撑数据,插入频率极高(N*(N-1)/2 条/知识)。走 UI 展示会拖慢查询、并且暴露的信息可能超出用户权限(明细不做权限过滤)。 正确做法 :如需"这条边来自哪些知识"的可视化,先在 Service 层按 getUserReadKnowids 过滤 KNOWID 再返回。
13.9 不要指望孤立标签或"只自共现"的知识产生边
问题 :initKnowIdRelations 在 validTags.size() < 2 时直接 return(§5.2 step 2)。只挂 1 个入图标签的知识既不写明细、也不建边。 陷阱:"发布了 10 篇知识总图还是空" → 极可能每篇知识只挂了 1 个已分级标签;至少要有 2 个都通过分级闸门的标签才会产生一条边。
14. 关键文件索引
| 关注点 | 文件 |
|---|---|
| 分型枚举 | farm2-skc-know/.../enums/FarmTagLevelEnum.java |
| 分型解析 | farm2-skc-know/.../service/impl/SkcTagServiceImpl.java#resolveTagLevel |
| 类别分型字段 | farm2-skc-know/.../domain/SkcCategory.java#taglevel |
| 建边/查询核心 | farm2-view/.../service/impl/TagRelationServiceImpl.java |
| 聚合边实体 / 明细表 DAO | farm2-view/.../domain/TagRelation.java、.../dao/TagCooccurDetailDao.java |
| 关系/节点枚举 | farm2-view/.../service/utils/FarmRelationEnum.java、FarmRelationNodeEnum.java |
| 事件驱动 | farm2-service/.../event/EventViewHandle.java |
| 前台控制器 | farm2-service/.../controller/WebRelationController.java |
| 下钻权限过滤 | farm2-service/.../controller/WebSearchController.java、WebSearchService.initPopFilter |
| 后台控制器 | farm2-view/.../controller/TagRelationController.java |
| 前端组件 | vue3/.../web_page/relation_graph/*.vue、views/chart/relation/echarts_relation.vue |
| DDL | farm2-main/.../versions/v1.3.0-v1.3.1.sql(聚合边重构 + 明细表)、v0_3/v0.3.8-v0.3.9.sql(旧边表初建) |
附录 A. 旧实现回顾(改造前基线)
改造前 SKC 已有一个共现网络雏形,与当前实现的关键差异如下,作为历史参照:
| 维度 | 旧实现 | 当前实现 |
|---|---|---|
| 边存储 | 每知识每标签对一条有向 边(N*(N-1) 条),relationkey=knowId |
每标签对全局一条无向聚合边,明细表溯源 |
| 权重 | LEVELNUM 恒为 1,共现频次靠查询期数记录条数累加 |
LEVELNUM 持久化 TF-IDF 加权共现强度 |
| 标签筛选 | 无分级、无停用词、无 TF-IDF | 类别分型闸门 + 停用词 + 管理类跳过 + TF-IDF 抑制 |
| 图谱形态 | 单层,种子标签 BFS 扩展邻居 | 双层,总图 + 下钻知识子图 |
| 权限 | 仅下钻知识列表 initPopFilter 过滤,图谱拓扑不过滤 |
下层子图节点/边下沉到用户可读范围 |
| 前端 | min_graph_view / max_graph_win / relation_know_win |
topic_graph_view / graph_full_win(旧组件已删除) |
旧边表 DDL 见 v0_3/v0.3.8-v0.3.9.sql;旧建边逻辑为 initKnowIdRelations 内两两全连接写有向边(LEVELNUM=1),已被 §5 的聚合增量逻辑取代。
变更日志
| 日期 | 变更 | 备注 |
|---|---|---|
| 2026-07-06 | 初版 | 合并现状基线 / 改造目标 / 现行实现三份文档 |
| 2026-07-10 | 头部规范化 + 补面向 AI 章节 | 头部改为四要素格式;新增 §12 隐含假设与前提(11 条)、§13 反例与陷阱(9 条);原 §12 "关键文件索引" 顺延为 §14 |
