资讯动态

Vue+Neo4j构建中医方剂知识图谱可视化系统实战

发布时间:2026/10/15 7:55:37 来源:尧图企业网站定制
之前接了一个活儿要把中医方剂那套复杂关系做成一个VueNeo4j的推荐与可视化系统核心就三件事根据症状推荐方剂、把方剂-药物-归经-产地之间的关系画成知识图谱、再做一个中药分布地图和知识图谱大屏。当时我第一反应是中医数据根本就不是表它是一张网一味药出现在几十个方剂里一个方剂又对应多种病症和归经传统关系型数据库表达这种多对多关系非常拧巴。后来把Neo4j图数据库用起来系统一下子就顺了。下面我会从数据建模、Cypher导入、后端推荐服务到前端ECharts大屏完整复现一套可以直接搬走的方案。如果你正在做知识图谱可视化、中医药信息化或者单纯想知道Vue前端怎么跟Neo4j好好配合这篇应该能帮上忙。1. 项目定位与整体思路拆解1.1 为什么用Neo4j而不是MySQL存中医药数据我先讲清楚一件事为什么一定要用Neo4j中医方剂的数据结构天生就是一张网。一味药会出现在多个方剂里比如黄连清湿热、泻火几乎每个清热方里都有它一个方剂又由多味药按君臣佐使配伍而成还对应多个主治病症药物又有四气五味、归经不同产地药材质量还有差异。多对多的关系在关系型数据库里你得建方剂表、药物表、病症表、归经表再来四个中间表等业务要查“哪个方剂同时包含黄连和黄芩、且主治腹胀”时SQL要join五张表以上性能先不说语义已经被拆得稀碎。图数据库的逻辑完全不同节点就是方剂、中药、病症、归经关系就是节点上的一条边。上述问题在Cypher里直接沿着关系匹配就行代码写起来像在描述问题本身而不是先翻译成多张表的连接条件。打个比方关系型数据库像是把所有食材、菜谱、门店都填进几十张Excel表查一道菜要用VLOOKUP来回找图数据库就像是在一张真正的食谱关系网上你沿着“这道菜用了什么食材”这根线直接走过去过程中还能顺便记住用量火候这些细节。我在实际项目里体会很深在MySQL里表达“方剂由药物组成”这个关系不外乎三种方式要么用中间表要么用逗号分隔的字符串要么用JSON字段。中间表查询麻烦字符串没法聚合统计JSON字段更是性能杀手。用Neo4j之后这些都不需要担心节点和关系就是建模的基本单位查询路径天然贴合真实世界的关系网络。1.2 知识图谱可视化与地图大屏到底解决什么问题这两块可视化绝不是为了炫技。我做的这个系统主要给三类人用学方剂的学生、临床医生、还有做中医药科普展示的机构。对学方剂的人来说只看文字方歌很抽象但在知识图谱里点开一个方剂所有组成药物、每味药的归经和现代药理作用都铺开来记忆成本低很多。对医生来说按症状组合推荐几个候选方剂再看它们在图谱里的位置参考价值比直接给答案更大。中药分布地图解决的是另一个问题——道地药材。同一种药材产地不同有效成分含量、药效差异都不小这是中医药里很硬核的常识。地图把“药材-产区-品质”这几个维度放在一起用户选一味药能直接看到它分布在哪些产区、各产区的道地等级如何。信息呈现比表格直观得多比如点击一味药地图上对应几个产区同时亮起来用户立刻明白这味药的产地格局。知识图谱大屏则主要是汇报和展示场景把图谱、地图、统计指标放同一屏一眼能看到整个系统的全貌讲解效率高很多。演示时客户提得最多的问题是“你们数据量有多大”“高频药材有哪些”“推荐逻辑长什么样”大屏上的统计卡片和排行榜正好回答这些。后来我复盘发现可视化这一步做在前面业务方对项目的认可度提升特别明显比单纯做个搜索列表强太多。1.3 为什么最后选了Vue做前端前端选Vue没什么悬念。这套系统的界面逻辑很重首页大屏、图谱交互、地图联动、统计数据面板Vue3的组合式API把状态管理和组件拆分的成本压得很低。生态也齐全Element Plus可以快速搭出后台面板ECharts的封装和Vue配合得很顺。我们项目组对Vue的熟悉度高所以最终选了Vue3TypeScriptVite的组合实测构建速度和类型保护都很舒服。如果你更熟React也没关系后端的图数据接口、Cypher模型都可以原样复用换层壳就行。2. 技术选型与数据模型设计2.1 前端技术栈解析前端技术栈我按实际用到的东西列一下Vue3 TypeScriptVue3的setup语法写起来干净TypeScript主要用来约束图谱节点、关系、推荐结果这些数据结构后端接口一变动前端立刻报错省了很多联调时间。Vite作为开发服务器冷启动和热更新比Webpack时代快一个量级打包配置也够简单。Element Plus提供表格、表单、卡片、下拉选择这些基础组件大屏右侧的筛选面板基本是现成的。ECharts负责所有图形知识图谱用graph地图用map加effectScatter统计指标用bar和gauge。axios统一封装请求拦截器里统一处理错误码和登录态。如果你从零开始搭我建议先搭一个空Vue3工程再按模块引入这些依赖。不要一次把几十个依赖全装上后面升级容易冲突。实际开发中ECharts和Element Plus的体积都不小记得做按需加载否则首屏会明显变慢。2.2 Neo4j数据模型设计全项目最关键的一步数据模型是整个项目里最值得花时间的一步。模型设计不好后面所有推荐查询都会别扭。我最终设计了五类节点、四类关系。节点类型核心属性说明方剂(FangJi)name, chu_chu(出处), gong_xiao(功效), zhu_zhi(主治)各种经典方、时方中药(YaoCai)name, si_qi(四气), wu_wei(五味), gui_jing(冗余归经), gong_xiao冗余归经为了列表搜索方便病症(ZhengZhuang)name, categorycategory表示内科、外科等分科归经(GuiJing)name肝、心、脾、肺、肾、胆、胃、大肠、小肠、膀胱、三焦产地(ChanDi)name, lon, lat经纬度坐标用于地图打点关系起止节点关系属性COMPOSE 组成方剂-中药dose(剂量), role(君臣佐使)TREAT 主治方剂-病症weightBELONG_TO 归经中药-归经无ORIGINATE_FROM 产于中药-产地quality(道地等级)为什么要给关系加属性因为剂量和君臣佐使这两个信息是方剂配伍的核心语义放在关系字段上直接就能查。比如查“所有以黄连为君药的方剂”一条Cypher就能搞定。如果把这堆信息塞到方剂节点的JSON或字符串属性里查询时要么全文匹配要么反序列化效率低还容易出错。MATCH (f:FangJi)-[c:COMPOSE]-(y:YaoCai {name: 黄连}) WHERE c.role 君 RETURN f.name, c.dose我在建模时还做了一个小决策把归经冗余到中药节点上。原因很简单列表页和详情页频繁要展示“这味药走哪些经”每次查归经关系当然也行但字段冗余能省一次图遍历前端渲染也更快。数据更新时只要保证来源一致这两个位置就不会冲突。2.3 后端服务选型与API设计思路后端其实有两条常见路线我这边因为团队技术栈偏Java所以用了Spring Boot加Spring Data Neo4j。如果你只是想快速验证Node官方driver也没问题。两种方案做个简单对比方案优点适合场景Spring Boot Spring Data Neo4j和微服务体系衔接好事务管理成熟SDN映射少样板代码有Java基础系统后续要接权限、更多业务Node neo4j-driver轻量前端团队可全栈Cypher直接以字符串传给驱动快速原型、中小型工具系统API层面我总共设计了五个接口。推荐接口接收症状列表返回排好序的方剂详情接口返回以指定方剂为中心的一跳邻居包括关联药物、病症、归经药材接口返回药材的基本信息、归经和产区地图接口返回各产区的药材分布统计大屏统计接口返回节点数、关系数、高频药材排行榜等。返回结构统一是{code: 0, data: ..., msg: ...}。图谱接口的data固定为{nodes: [], links: []}前端不用做任何二次转换拿过去就能灌给ECharts。这个约定一定要在前后端联调前定好省得后面各改各的。3. 知识图谱构建与数据导入实操3.1 数据源整理与实体对齐数据整理我花了两周时间。这步看起来不性感却是整个项目最容易翻车的地方。网上能拿到的中医药数据大多是从教材和公开资料整理的Excel或CSV列名不统一内容还带各种脏数据。同一味药可能有别名比如大黄、川大黄、酒大黄同一个病症也有不同叫法感冒和伤风基本算一回事。我的处理流程是这样先做别名表把所有别名都映射到唯一的规范名再做属性归一比如“生地黄”和“地黄”同样处理产地名称也要统一最后导成三张CSV一个是方剂基本信息一个是药材基本信息一个是方剂与药物的组成关系。所有清洗都抽成独立脚本源数据更新后直接重跑。我建议别把清洗逻辑写在SQL里因为药材名同一性判断需要维护一个词典不是简单replace能解决的。还有一个小坑Excel里经常出现全角空格、换行符导入Neo4j后节点名会带着诡异字符Cypher查不到。所以清洗脚本里统一做trim把空白字符全部规整掉。我实测下来这步能解决后面一大半“为什么查不到节点”的困扰。3.2 通过Cypher脚本把CSV导入Neo4j导入我用Neo4j原生的LOAD CSV不需要额外插件。注意CSV文件要放在Neo4j安装目录下的import文件夹里。先把约束建好保证重跑脚本不会生成重复节点。CREATE CONSTRAINT fangji_name IF NOT EXISTS FOR (f:FangJi) REQUIRE f.name IS UNIQUE; CREATE CONSTRAINT yaocai_name IF NOT EXISTS FOR (y:YaoCai) REQUIRE y.name IS UNIQUE; CREATE CONSTRAINT zhengzhuang_name IF NOT EXISTS FOR (z:ZhengZhuang) REQUIRE z.name IS UNIQUE; CREATE CONSTRAINT chandi_name IF NOT EXISTS FOR (c:ChanDi) REQUIRE c.name IS UNIQUE;然后按顺序导入节点和关系LOAD CSV WITH HEADERS FROM file:///fangji.csv AS row MERGE (f:FangJi {name: row.name}) SET f.chu_chu row.chu_chu, f.gong_xiao row.gong_xiao; LOAD CSV WITH HEADERS FROM file:///yaocai.csv AS row MERGE (y:YaoCai {name: row.name}) SET y.si_qi row.si_qi, y.wu_wei row.wu_wei, y.gong_xiao row.gong_xiao; LOAD CSV WITH HEADERS FROM file:///compose.csv AS row MATCH (f:FangJi {name: row.fangji}) MATCH (y:YaoCai {name: row.yaocai}) MERGE (f)-[c:COMPOSE {dose: row.dose, role: row.role}]-(y);数据量大的时候在LOAD CSV前加USING PERIODIC COMMIT 1000可以避免内存暴涨。我第一次导两万条关系时跑了好久后来发现是没开PERIODIC COMMIT加上以后速度上来了。另外关系导入时如果遇到找不到方剂或药材脚本会直接跳过那行所以导入完成后一定要做校验确认没有因为数据对不上导致关系丢失。3.3 图谱质量校验与关系方向约定导入完别急着开发先做一次质量校验下面是几个常用查询MATCH (f:FangJi) RETURN count(f) AS fangjiCnt; MATCH (y:YaoCai) RETURN count(y) AS yaocaiCnt; MATCH (:FangJi)-[c:COMPOSE]-(:YaoCai) RETURN count(c) AS composeCnt; MATCH (n) WHERE NOT (n)--() RETURN n LIMIT 20;孤立节点排查后如果发现某些药材没有挂到任何方剂上大概率是组成关系表里的药材名和药材表对不上回到清洗脚本里做一次别名归一就行。关系方向我们统一约定方剂指向药物、方剂指向病症、药物指向归经和产地。别小看这个约定团队配合时如果没有统一方向Cypher里到底是INCOMING还是OUTGOING会把人逼疯。我还把这些约定写进了项目文档里给每个关系起了清晰的名字避免后面出现两个含义模糊的关系名。4. 后端推荐服务与搜索实现4.1 从症状到方剂的推荐核心逻辑后端推荐逻辑是最核心的部分。我的第一版实现就一个Cypher根据用户选择的症状找到同时匹配这些症状的方剂按匹配数排序。这个思路简单直接也给用户留出了解释空间。MATCH (s:ZhengZhuang)-[:TREAT]-(f:FangJi) WHERE s.name IN [感冒, 发热, 头痛] WITH f, count(DISTINCT s) AS matched WHERE matched 2 MATCH (f)-[:COMPOSE]-(y:YaoCai) RETURN f.name AS fangji, matched AS score, collect(DISTINCT y.name) AS yaocai ORDER BY score DESC, size(collect(DISTINCT y.name)) DESC LIMIT 10解释一下排序逻辑matched是命中的症状数分数越高说明方剂覆盖面越广如果分数相同再用组成药物数量做次级排序多数情况下药物多的方剂信息量更大展示效果更丰富。实测这套规则比单纯返回所有包含某症状的方剂要准很多。有个容易踩坑的点如果用户只选了一个症状matched 2会把结果直接过滤成空所以接口层要做一个处理症状选择数小于等于1时去掉过滤条件。我一开始没处理测试时单选一个“头疼”返回空列表还以为是数据问题查了半天才发现是阈值卡得太死。为什么不做复杂协同过滤数据规模摆在这几万条关系而已规则可解释反而更适合医疗场景。用户不信任一个黑盒模型但你说“这个方剂匹配了你选的三个症状中的两个还覆盖了头痛和发热”信任感完全不同。当然如果后面数据量大了可以把这套规则结果作为召回再用模型精排。4.2 基于药对和归经的扩展推荐沿着相同思路药对推荐也很快。输入一味药找出和它共同出现在最多方剂里的其他药物等于在做最朴素的关联规则挖掘。MATCH (y1:YaoCai {name: 黄连})-[:COMPOSE]-(f:FangJi)-[:COMPOSE]-(y2:YaoCai) WHERE y2.name 黄连 RETURN y2.name AS drug, count(DISTINCT f) AS coCount ORDER BY coCount DESC LIMIT 10这个接口在临床辅助上还挺常用医生想看看某味药跟哪些药搭配最频繁相当于给了一个配伍建议。归经推荐是另一个维度某个部位或者某条经的病症多就优先推荐走这条经的药物。比如用户选了“肝经病变”查询就沿着中药到归经的BELONG_TO关系反向匹配再跟病症做交叉。这个接口查询频率不高我一般直接在Neo4j Driver层执行Cypher字符串不额外写ORM映射省事。4.3 搜索接口与缓存策略全站搜索我统一用了CONTAINS做模糊匹配数据量不大时完全够用。如果后续条目到十万级建议给Neo4j配全文索引或者把搜索词同步到搜索组件里。这里我刻意没做过早的架构设计先把功能跑通数据量上去再优化。缓存方面我一开始用Redis后来发现推荐结果的变化频次很低换成Caffeine本地缓存就够了。图谱接口加一层基于节点id的缓存点击同一个方剂时响应时间能压到几十毫秒。缓存时一定要带上版本号或数据更新时间否则清洗完数据刷新了缓存还是旧图排查起来很痛苦。这个问题我真实遇到过问了半天同事才发现是缓存没失效。5. 前端可视化大屏与地图实现5.1 知识图谱关系可视化用ECharts把Neo4j的图画出来前端第一个难点是知识图谱怎么画。ECharts的graph系列我用得最顺没有额外引入D3或G6。后端返回的节点和关系直接映射到series数据node里带上id、name、category、symbolSizelink里带着source、target。const option { tooltip: {}, legend: [ { data: [方剂, 中药, 病症, 归经, 产地] } ], series: [{ type: graph, layout: force, roam: true, draggable: true, data: nodes.map(n ({ id: n.id, name: n.name, category: n.category, symbolSize: n.category 方剂 ? 40 : 24 })), links: links.map(l ({ source: l.source, target: l.target })), categories: [ { name: 方剂 }, { name: 中药 }, { name: 病症 }, { name: 归经 }, { name: 产地 } ], force: { repulsion: 150, edgeLength: 80 }, label: { show: true, position: right, formatter: {b} }, emphasis: { focus: adjacency } }] } myChart.setOption(option)有个经验要分享别一次性把全图所有节点发给ECharts500个节点以上的强制定位布局就开始卡了。我的做法是后端详情接口只返回以目标方剂为中心的一跳邻居比如一个方剂带10个药物每个药物带归经和产地一次大概几十个节点界面很流畅。用户点击某个药物节点时再请求以这个药物为中心的二跳扩展。这样交互更自然每次点击都有新信息出现而不是一开始就把所有内容倒给用户。5.2 中药分布地图从GeoJSON到涟漪散点图中药分布地图我用的还是ECharts配合一张区域GeoJSON。注意ECharts从5.0开始不再内置地图数据需要自己用registerMap注册。GeoJSON可以找现成的区域边界也可以自己简化。开发阶段我用的示例坐标全部是占位数据实际部署时一定要替换成真实经纬度并且跟GeoJSON里的区域编码对应好否则点会飘到不知所踪。import areaJSON from ../assets/area.json echarts.registerMap(area, areaJSON) const option { tooltip: { formatter: p { if (p.seriesType effectScatter) { return p.data.name : p.data.value[2] } return } }, geo: { map: area, roam: true, itemStyle: { areaColor: #e6e6e6 } }, series: [{ type: effectScatter, coordinateSystem: geo, data: points.map(p ({ name: p.place, value: [p.lon, p.lat, p.count] })), symbolSize: val Math.max(8, Math.min(28, val[2] / 10)), rippleEffect: { scale: 3 } }] } myChart.setOption(option)地图和高亮联动时可以点击气泡把同产区药材筛选出来再联动图谱高亮相关节点。这一步前后端配合不难地图点击事件触发一个自定义事件图谱组件监听事件后重新请求接口。实测在高分屏上涟漪效果很讨喜汇报演示特别加分。5.3 大屏布局与适配实战大屏布局我说点实操的东西。大屏一般是16比9我先按1920乘1080设计所有组件尺寸都写死在这个基准上。适配屏幕时用一个外层div计算缩放比例内层固定1920乘1080用transform scale处理。div classscreen-wrapper div classscreen-container :stylescreenStyle !-- 中央图谱、地图、统计面板 -- /div /div.screen-wrapper { width: 100vw; height: 100vh; overflow: hidden; } .screen-container { width: 1920px; height: 1080px; transform-origin: left top; display: grid; grid-template-columns: 400px 1fr 400px; grid-template-rows: 120px 1fr 80px; gap: 12px; padding: 12px; box-sizing: border-box; background: linear-gradient(135deg, #0f1c2e 0%, #1c2e4a 100%); }布局上我采用三列网格左边放统计卡片和药材榜单中间放主图谱右边放中药分布地图和推荐结果底部是实时日志或滚动消息。主图谱占最大面积因为它是整个系统内容最丰富的一块。定时刷新有两个坑一定要记住。一个是Vue组件销毁后定时器还在跑要在onBeforeUnmount里clearInterval另一个是ECharts实例在setOption前要用nextTick确保DOM已经更新。我遇到过多次页面切换后图表没重新渲染检查最后都是这两个问题。6. 常见问题与排查技巧实录6.1 高频问题速查表整理一个速查表基本是开发过程中反复出现的坑现象可能的根源解决办法图谱只显示出少量孤立节点接口只查了一跳邻居前端没有扩展机制增加点击节点的动态扩展接口地图一片空白连边界都不显示没有registerMap或GeoJSON格式不对检查registerMap调用确认GeoJSON是标准FeatureCollection推荐接口返回空列表matched阈值过滤把所有方剂排掉了症状少于2个时调整匹配逻辑LOAD CSV中文乱码CSV编码不是UTF-8用编辑器另存为UTF-8无BOM大屏在笔记本上变形scale方案基准尺寸算错了用动态计算比例不要写死Cypher查询偶尔超时节点name没建唯一约束补上约束和索引点击节点后连线方向不对关系方向约定不统一统一在文档里约定INCOMING/OUTGOING6.2 几条独家避坑经验第一数据清洗的时间预算要充足。这个项目开发周期里我大概一半时间花在了清洗和核对数据上而不是写代码。如果后面有人告诉我他做知识图谱很快我会先问他的数据是怎么来的。第二推荐接口一定要保留解释信息。医生和患者都希望看到推理链路我在接口返回里加了matchedSymptoms数组前端直接展示“匹配到症状发热、头痛”用户接受度立刻提升。第三地图坐标别用假的。演示系统无所谓生产环境如果坐标偏移用户第一眼就会觉得不专业。第四把Neo4j的查询日志打开。开发阶段能直接看到每条推荐接口的Cypher和耗时很多性能问题一眼就定位出来。第五节点和关系的命名规范。我试过用中文做节点标签前端图表里显示确实方便但Cypher写起来引号太多最后还是改成拼音存储展示时再映射成中文。这里建议一开始就做好映射层别后期返工。这个项目做完我最大的体会是知识图谱的价值不在于存了多少节点而在于关系怎么建模、怎么被查询。中医方剂里“君臣佐使”这种层级关系一旦在Neo4j里建模对了后面所有推荐和可视化都是水到渠成的事情。如果你也想做类似的系统不妨从这套模型开始先把数据导进去再谈界面。界面是锦上添花模型才是骨架。最后再分享一个小技巧开发阶段把Neo4j的查询日志打开每一条推荐接口都能看到对应的Cypher和耗时调优的时候心里会特别有底。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑