首页 / 资讯中心 / 文章详情

基于SpringBoot+Vue+Neo4j的中医方剂知识图谱构建实战

基于SpringBoot+Vue+Neo4j的中医方剂知识图谱构建实战 ★ FEATURED ARTICLE
做中医方剂知识图谱这个选题的时候很多同学第一反应是“这不就是个毕业设计嘛”。但真正上手之后你会发现它其实是理解“如何用现代工程手段去组织传统知识体系”的一个绝佳载体。方剂知识图谱不像普通的管理系统CRUD它涉及数据建模、自然语言处理、图数据库、可视化展示、前后端联调一整条链路。这篇文章我就以“SpringBoot Java Vue 前后端分离”这套组合为例把整个项目的构建思路、核心实现和踩坑记录完整拆开讲清楚拿到源码却看不懂的人跟着这篇文章走一遍基本就能自己复现了。1. 项目价值与整体链路拆解1.1 为什么选“中医方剂”作为知识图谱的载体知识图谱本质上解决的是“实体之间的关系查询”。传统关系型数据库适合存“一行一条”的字典式数据但如果要问“哪些方剂能治疗失眠同时其中又包含甘草这味药”SQL得写多层JOIN维护起来并不直观。知识图谱把方剂、药材、功效、疾病、归经这些概念抽成节点把“组成”“主治”“归经”“功效”抽成关系一个查询就能跨多级跳转非常符合中医“辨证论治、药物配伍”这种天然带有网状结构的知识特性。我当时选择这个选题还有一层考虑中医方剂数据在公开渠道相对丰富比如《伤寒论》《金匮要略》等经典方剂都有公开的电子文本整理成结构化数据不算太难。而且这个题目的“技术深度”刚好卡在本科毕设或面试项目展示需要的层级——既不会太难到失控又有足够多的技术点可以聊。1.2 整体功能链路整个项目从用户视角来看分三个核心模块方剂浏览与检索支持按方剂名称、主治功效、药材组成进行组合搜索。知识图谱可视化以图谱形式展示方剂、药材、功效、疾病之间的关联网络支持点击节点下钻。数据管理后台管理员可以维护方剂、药材、关系数据前端通过Vue页面调用后端接口完成增删改查。从技术链路来看数据流向是外部结构化数据 - Java后端解析入库 - Neo4j图数据库存储 - 后端接口封装 - Vue前端通过ECharts/Vis.js渲染图谱。2. 技术选型与架构设计2.1 SpringBoot版本与Java版本的选择这个项目踩的第一个坑就是版本适配。SpringBoot 3.x要求Java 17起步如果你本机装的是Java 8或者学校机房还是JDK 1.8那就老老实实用SpringBoot 2.7.x对应的Java版本为8或11都支持。我当时用的是SpringBoot 2.7.18 Java 8这是目前兼容性最稳妥的搭配不管是本机调试还是部署到服务器都不会因为版本问题莫名其妙报错。还有一点要注意热词里提到“springboot版本太高”这确实是很多人在导入项目之后遇到的第一道坎。如果你的pom.xml里引的是SpringBoot 3.0以上的版本而你的JDK是8那么Maven会在编译阶段直接报UnsupportedClassVersionError。与其去降级代码不如在创建项目之前就锁定版本组合。2.2 为什么选Neo4j作为知识图谱的存储引擎知识图谱的存储方案主要有三种基于RDF的Jena、基于图数据库的Neo4j、基于关系型数据库的自建表结构。我最终选了Neo4j理由很简单——它用起来最贴近“图”的直觉Cypher查询语法学习成本低而且Neo4j Community版免费对单机项目来说性能完全够用。你只需要启动一个Neo4j服务定义好节点标签Label和关系类型Relationship Type数据就能以“点-边-点”的形式存储和查询。对比之下基于RDF的方案需要写SPARQL查询门槛高且生态偏学术自建表结构虽然不需要额外引入数据库但查询跨层关系时SQL会非常啰嗦。用Neo4j来做这个项目往简历上写也更拿得出手。2.3 前后端分离的架构思路前后端分离在这类项目里的意义不仅是“把页面和接口拆开”更重要的是让后端只需要关心数据建模和业务逻辑前端只需要关心交互和可视化呈现。后端跑在8080端口前端通过Vite/Webpack代理把/api开头的请求转发到后端开发时两边可以独立启动、独立调试。这么做的好处我在实际开发中感受很深知识图谱的可视化调试非常耗时如果前后端耦合在一起每次调整前端样式都要重启整个项目。分离之后前端改完代码热更新基本秒刷后端接口用Postman或Apifox先验证通过再和前端联调排错效率提高非常多。3. 核心数据建模与知识图谱构建3.1 实体与关系的设计知识图谱建模的核心是确定“节点”和“边”。我设计的是四类实体节点和七类关系实体节点节点标签含义主要属性Formula方剂方剂名称、出处、用法、功效描述Herb药材单味药材名称、性味、归经、功效Disease疾病主治疾病名称、证型Symptom症状临床症状名称、描述关系类型关系起始节点结束节点含义COMPOSED_OFFormulaHerb方剂包含该药材TREATSFormulaDisease方剂主治该疾病RELIEVESFormulaSymptom方剂缓解该症状HAS_FUNCTIONFormulaFunction方剂有此功效Function可以建节点也可以做属性我建了节点方便扩展BELONGS_TOHerbCategory药材所属分类INTERACTS_WITHHerbHerb药材间配伍/相反关系这里有一个重要的设计决策方剂的“功效”到底是做成属性还是节点我当时纠结了很久。做成属性查询简单但没法按功效反向搜索所有方剂做成节点查询灵活但数据建模复杂。最终我选择了“功效节点”方案因为需求里明确要求支持“按功效查方剂”的反向检索这个操作在关系模型里只需要Match (f:Formula)-[:HAS_FUNCTION]-(fn:Function) WHERE fn.name 解表 RETURN f非常直接。3.2 数据清洗与结构化处理数据是整个项目的根基。我最初从公开网站上爬了约300首方剂但原始数据非常“脏”方剂名称有全角和半角混用、药材剂量格式不统一比如“三两”“3两”“9g”混在一起、主治描述里夹带标点和注释。清洗策略我分三步走统一编码和格式将所有文本转成UTF-8编码去除空格、全角括号转半角、删除无意义的换行符。实体对齐药材名称存在异名同物的情况比如“山萸肉”和“山茱萸”“桂心”和“肉桂”我用一个别名映射表做了归一化。结构化拆分把“方剂-药材”的关系从文本中拆出来形成独立的CSV列。比如“麻黄9g桂枝6g杏仁9g甘草3g”拆成四条记录存到公式_组成表中。这一步看起来不起眼但如果不做干净后面导入Neo4j时会出现大量重复节点图谱会乱到没法看而且查询结果也会不准确。我在实际操作中大概花了3天时间在清洗上别嫌慢这一步值得。3.3 Neo4j数据导入与Cypher实践Neo4j导入数据有两种主流方式一是通过APOC插件配合CSV文件批量导入二是通过Java后端逐条写入。前者导入效率高适合数据量大的情况后者适合需要走业务逻辑校验的场景。我当时收到源码之后先把CSV文件整理到Neo4j的import目录然后用Cypher的LOAD CSV语句完成初始化导入LOAD CSV WITH HEADERS FROM file:///formulas.csv AS row CREATE (f:Formula {id: row.id, name: row.name, source: row.source, usage: row.usage})注意一个坑LOAD CSV的file路径是相对于Neo4j安装目录的import文件夹的Windows上路径分隔符要用正斜杠否则会一直报“Couldnt load the external resource”错误。我当时卡在这里小半天最后发现是路径写成了反斜杠。药材关系导入可以这样处理LOAD CSV WITH HEADERS FROM file:///formula_herb.csv AS row MATCH (f:Formula {id: row.formula_id}) MATCH (h:Herb {id: row.herb_id}) MERGE (f)-[:COMPOSED_OF {dosage: row.dosage}]-(h)日常查询用Cypher非常顺手。比如“找出包含甘草且主治咳嗽的所有方剂”一句话就能查出来MATCH (f:Formula)-[:COMPOSED_OF]-(h:Herb {name: 甘草}) MATCH (f)-[:TREATS]-(d:Disease {name: 咳嗽}) RETURN f.name, f.usage3.4 后端Java操作Neo4j的两种姿势后端代码操作Neo4j主要有两种方式Spring Data Neo4j和Neo4j Java Driver。我推荐初学者用Spring Data Neo4j因为它和SpringBoot集成度最好实体类注解和JPA风格很像学习曲线平滑。比如定义一个方剂实体Node(Formula) public class Formula { Id GeneratedValue private Long id; Property(name) private String name; Property(source) private String source; Relationship(type COMPOSED_OF, direction Relationship.Direction.OUTGOING) private ListHerb herbs; }如果不是所有查询都能用Repository方法名表达那就直接用Query注解写CypherQuery(MATCH (f:Formula)-[:TREATS]-(:Disease {name: $diseaseName}) RETURN f) ListFormula findByDiseaseName(String diseaseName);这里要特别提醒Neo4j 4.x之后的Driver要求显式指定数据库名称Spring Boot 2.7 Spring Data Neo4j 6.x的配置文件中必须写spring.neo4j.uribolt://localhost:7687 spring.neo4j.authentication.usernameneo4j spring.neo4j.authentication.passwordyour_password spring.neo4j.databaseneo4j如果不指定database项高版本Neo4j会默认连接neo4j库但有些破解版或改装版默认库名可能叫graph.db连不上时看控制台日志会提示“Database does not exist”加上配置就好了。4. 前后端分离实操后端接口与前端可视化4.1 后端接口设计接口设计遵循RESTful风格核心接口我归纳成下面几大块模块方法接口路径功能说明方剂GET/api/formula/list?page1size10分页查询方剂方剂GET/api/formula/detail/{id}查询方剂详情含药材组成方剂GET/api/formula/search?keywordxx按关键词模糊搜索图谱GET/api/graph/formula/{id}查询某方剂为中心的子图图谱GET/api/graph/expend?nodeIdxxtypeHerb点击节点后扩展下钻数据管理POST/api/admin/formula新增方剂数据管理PUT/api/admin/formula/{id}更新方剂数据管理DELETE/api/admin/formula/{id}删除方剂及关联关系我建议图谱接口的返回格式统一定义成前端直接可用的结构不要让前端再自己组装。比如子图接口返回{ nodes: [ { id: F001, label: 麻黄汤, category: Formula }, { id: H001, label: 麻黄, category: Herb } ], links: [ { source: F001, target: H001, relation: COMPOSED_OF } ] }这个结构可以直接喂给ECharts的graph系列或Vis.js的DataSet前端几乎不用再做二次加工。4.2 Vue项目初始化与环境配置很多人在这一步就卡住了。热词里出现“vue安装及环境配置”“vue安装依赖失败”这些是高频问题。我先说一下我们项目里前端用的技术栈Vue 3 Vite Element Plus Axios ECharts。用Vite不用Vue CLI是因为Vite启动速度更快而且Vite 4 Node 16以上配合得很稳定。初始化步骤npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios element-plus echarts如果执行npm install时卡在node-gyp或者报ETIMEDOUT大概率是网络问题。这里有两个解决方向一是切换成淘宝镜像源npm config set registry https://registry.npmmirror.com二是如果公司或校园网有HTTPS拦截可以临时用yarn替代npm或者设置代理环境变量。我当时就是在换了npmmirror之后才顺利装完依赖。Element Plus建议全量引入就好不要做按需引入省得再配unplugin-vue-components插件。毕设项目性能不是瓶颈全量引入的开发效率最香。4.3 图谱可视化核心实现图谱可视化是本项目的重头戏。我用的是ECharts的graph类型先把后端返回的nodes和links塞进配置项const chartOption { tooltip: {}, series: [{ type: graph, layout: force, roam: true, draggable: true, data: graphData.nodes.map(node ({ id: node.id, name: node.label, category: node.category, symbolSize: node.category Formula ? 60 : 40, itemStyle: { color: getColorByCategory(node.category) } })), links: graphData.links.map(link ({ source: link.source, target: link.target, label: { show: true, formatter: link.relation } })), categories: [ { name: Formula }, { name: Herb }, { name: Disease } ], force: { repulsion: 300, edgeLength: 100 } }] };这里有几个调优心得force布局的repulsion节点斥力如果太小节点会挤成一团成语义不清的毛线球如果太大图谱又散得没法看。我调下来发现300左右比较合适配合edgeLength 100整体疏密适中。节点颜色按category区分方剂用暖色药材用冷色疾病用中性色视觉上一眼就能分清层次。roam: true这个配置很重要不然图谱不能拖拽和缩放数据一多就看不完了。点击节点下钻的功能也很关键。比如点击药材节点“麻黄”前端需要再次请求后端接口获取以“麻黄”为中心的所有关联方剂再更新图谱数据。这一步做成了图谱不会是“死图”能让人真正操作起来。4.4 Axios封装与跨域处理Axios封装看起来是基础功夫但接口统一管理能让联调效率高很多。我习惯在src/api目录建一个request.js作为统一封装import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络异常) return Promise.reject(error) } ) export default service然后每个业务模块建一个独立文件比如formula.jsimport service from ./request export function getFormulaList(params) { return service.get(/formula/list, { params }) } export function getGraphData(formulaId) { return service.get(/graph/formula/${formulaId}) }跨域问题通过Vite代理解决在vite.config.js中配置export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })注意如果后端Controller里映射的是/formula/list那么前端请求路径写/api/formula/list代理会把/api前缀去掉再转发到8080这样两边不用额外写跨域注解。后端如果想双保险也可以加一个CorsFilter但一般来说代理已经能搞定。5. 常见问题与排查技巧实录5.1 Neo4j连接失败的排查清单这个项目报错最多的就是Neo4j连不上。我把常见原因整理成一张速查表报错信息可能原因解决办法Connection refusedNeo4j服务没启动在Neo4j Desktop或neo4j console启动服务Authentication failed密码不对在Desktop里重置密码改配置Database does not exist配置文件未指定库名添加spring.neo4j.databaseneo4jbolt://localhost:7687 failed端口被占用或驱动版本不匹配检查7687端口Spring Data Neo4j 6.x需Neo4j 4.x以上Couldnt load the external resourceCSV路径问题确认文件放在import目录路径用正斜杠你要是用Neo4j Desktop得注意Desktop里的数据库必须在running状态。另外浏览器打开http://localhost:7474能进Neo4j Browser但后端连的是bolt端口7687这两个端口不要混淆。5.2 SpringBoot启动失败的典型场景SpringBoot项目导入后启动失败第一个检查点就是Maven依赖是否全部下载成功。用命令行执行mvn clean install -DskipTests如果报Failed to read artifact descriptor for xxx说明有的依赖没下全把本地仓库C:\Users\xxx\.m2\repository里对应文件夹删掉再重新拉取一次。第二个高发问题是目标版本冲突。pom.xml里的Java版本参数和IDE编译级别不一致会报各种奇奇怪怪的错比如bad constant pool tag或cannot resolve symbol。我统一在pom里这样锁定properties java.version8/java.version maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties如果还是报版本错误在IDEA里按CtrlAltShiftS打开项目结构把Project SDK和Project language level改成一致。5.3 Vue端调试时的避坑心得前端调试一个重要坑是ECharts的容器宽度问题。初始化图表时如果容器还没渲染完成拿到的宽度是0图表就会挤成一团或什么都不显示。解决办法是在nextTick里初始化或者给容器设定固定宽高。另一个高频问题是下拉框或传参类型不对。比如后端接口要求formulaId是Long但前端口号控件传给后端的是字符串SpringBoot在解析时会报类型不匹配。我习惯在后端Controller的RequestParam上加DateTimeFormat类比的方式但更稳妥的是直接用Long类型接收前端传参时用Number()强制转一下。热词里提到vue播放m3u8这种问题其实和本项目无关但如果你是第一次接触Vue看到类似报错不要慌先分清是哪一层出的问题——Vue的报错通常在浏览器控制台后端的问题在IDEA的控制台接口数据结构的问题在Network面板里看响应体。5.4 中文乱码的处理方案知识图谱项目里中文乱码主要出现在两个位置一是接口返回的JSON里中文变成???二是Neo4j Browser里中文数据显示乱码。JSON乱码八成是SpringBoot的响应编码问题。在application.yml中加server.servlet.encoding.charsetUTF-8 server.servlet.encoding.enabledtrue server.servlet.encoding.forcetrueNeo4j导入CSV时乱码通常是CSV文件本身不是UTF-8编码。Windows里用记事本另存为时选择UTF-8编码再导入。如果你用Excel编辑过CSV一定要看文件编码Excel默认可能存成ANSI这是坑中坑。5.5 项目调试与源码走读建议很多同学拿到“源码文档”之后习惯性先运行跑不起来就慌。我自己的经验是先花一小时把项目结构看明白再启动。以SpringBoot项目为例先看pom.xml确定依赖范围再看application.yml确认数据库配置然后找到启动类从Controller入口逆着往Service、Mapper捋一遍代码基本流程就能在脑子里画出来。调试的时候我强烈推荐用Apifox或Postman先测接口。比如直接请求GET http://localhost:8080/formula/list?page1size5如果返回正常JSON说明后端没问题剩下的只需要前端联调。如果接口都通了但页面不对那问题在Vue组件的逻辑和样式上。这样分段排查能节省大量时间。6. 项目扩展与后续优化思路6.1 引入HanLP做方剂文本实体识别如果想把项目做得更有亮点可以考虑引入HanLP分词工具。原始方剂文本中有大量非结构化的描述比如“治太阳病头痛发热身疼腰痛”如果我们能自动抽取“头痛”“发热”作为症状实体就能大幅扩充图谱内容。HanLP在SpringBoot中的集成很简单引入依赖后调用分词接口HanLP.segment(头痛发热)不过要注意HanLP默认词典是通用领域中医症状词表需要自定义加载。可以从症状节点表里导出词汇表加到自定义词典路径中。6.2 把静态图谱升级为动态推荐现在的图谱是“查什么展示什么”进阶一点可以做“方剂推荐”用户选择几个症状后端用Cypher查询同时匹配这些症状的方剂作为推荐结果。这种功能更贴近知识图谱的真实应用场景也更容易在答辩时讲出深度。实现思路是将用户输入的多个症状作为查询条件在Neo4j中找所有覆盖这些症状的方剂节点按覆盖数量排序。当然真正的推荐系统比这个复杂但做展示和演示足够有说服力了。6.3 代码规范与文档沉淀项目做到最后我觉得最重要的一点是代码命名规范。写知识图谱项目很容易出现“图数据对象”和“前端展示对象”混用的现象比如直接拿Neo4j的Node对象返回给前端。建议定义VO对象专门用来做接口传输举例来说public class GraphVO { private ListGraphNodeVO nodes; private ListGraphLinkVO links; }这样就算后面换存储引擎前端代码也不用动。文档方面README里应该写清楚环境要求、启动步骤、数据库初始化脚本说明这些是答辩时老师重点看的。最后说几句实在的我最初做这个项目的时候一个人从零开始折腾了将近三周。前一周踩在环境配置和数据清洗上后两周才真正进入编码状态。你如果拿到的是一份现成的源码我建议也不要急着双击运行先把上面的链路自己走一遍——建库、导数据、起后端、启前端、一步步看请求怎么发、数据怎么返回、图谱怎么渲染。这个过程走完一遍你收货的东西要比单纯“运行成功”多得多。另外想单独提一句知识图谱项目最容易让人觉得“水”的点是逻辑简单、纯展示。想让它有分量一定要把“关系”这个核心价值体现出来。千万不要只做到“点点节点、看看详情”能跨关系查询、能做下钻分析这个项目的含金量立刻不一样。如果后续有精力可以考虑继续做实体链接、别名归一化、症状权重计算这些方向天花板很高。先把眼前的这版跑通、跑稳比什么都有用。
阅读完成 · 觉得有帮助?
咨询建站