简介:这是一份基于Neo4j构建《水浒传》人物关系图谱与智能问答系统的毕业设计源码包,面向计算机、人工智能等专业的高校学生与从业者,可用于课程实践、学期项目或毕业设计参考。整套代码采用Python开发,模块化划分数据层、算法层与Web服务层:数据层用Cypher完成关系路径检索,算法层封装共现分析、紧密度计算与社区发现,前端基于ECharts展示交互式关系网络,后端以Flask提供RESTful API,并借助Docker实现便捷部署。资源共237个文件,约23.56MB,以jpg图片(界面截图/演示素材)、py源码、css/js前端资源、html页面等为主,另含pptx答辩演示文稿和部署说明。已有72人学习下载。除了可直接运行的完整项目外,包内还提供毕业答辩PPT、部署指南与接口说明,便于快速理解架构并二次扩展,适合初次接触图数据库的初学者跟进完整流程,也适合进阶者在此基础上增加时序分析、动态可视化等能力。
1. 基于 Neo4j 的水浒人物关系图谱与智能问答系统:一份能跑到 98 分的毕设源码
拿到这份基于 Neo4j 的水浒传人物关系图谱与智能问答系统源码时,我第一反应是去翻知识抽取逻辑,而不是先跑前端页面——这类毕设项目,很多套了个 ECharts 壳,本质还是拿 SQL 查关联表。这个项目不一样的地方在于,人物属性、社会关系、事件关联是真实落进图数据库的,Cypher 负责路径检索,APOC 插件做社区发现和关系紧密度计算,问答模块把自然语言问题解析成 Cypher 去执行,不是关键词匹配。它适合正在做图数据库方向毕业设计、又不想纯写理论的同学,也适合想快速看懂 Flask + Neo4j + ECharts 这条技术栈怎么串成闭环的开发者。整套资源是完整 Python 源码加答辩演示文稿,毕业设计评审给了 98 分,这个分数说明评委认可的是这条可运行的图分析链路,不只是页面效果。
2. 实体关系抽取与数据建模:把《水浒传》文本拆成节点和关系边
2.1 先定实体边界:人物主名、别名与属性字段
任何图谱项目的第一步都不是写代码,而是确定「什么能成为一个节点」。《水浒传》的难点在于人物别名极多:宋江又叫及时雨、呼保义、宋公明,武松也叫武二郎、行者。如果不做别名归一化,后面的共现统计会把宋江和及时雨当成两个实体,图谱里冒出大量重复节点,问答模块也会出现「宋江的绰号是什么」答不出来的翻车现场。
我一般建议先把人物实体表建成一份 Python 字典,主名做键,别名列表做值。这份源码里也是这么处理的,常见做法是:
CHARACTERS = { "宋江": ["宋江", "及时雨", "呼保义", "宋公明", "孝义黑三郎"], "武松": ["武松", "武二郎", "行者"], "鲁智深": ["鲁智深", "鲁达", "花和尚"], "林冲": ["林冲", "豹子头"], "李逵": ["李逵", "黑旋风", "铁牛"], # 其余人物按需补充 } ALIAS_TO_NAME = {} for name, aliases in CHARACTERS.items(): for alias in aliases: ALIAS_TO_NAME[alias] = name这段代码解决的问题是清洗:所有别名统一映射到主名,后续无论从文本里抽出的是「及时雨」还是「呼保义」,都归到宋江节点。注意名单要覆盖主要人物和配角,只放一百单八将容易漏掉高俅、潘金莲这类推动情节的关键角色,关系图谱会缺一大块。
节点属性方面,原著给了很规整的半结构化信息,可以直接落成字段:
| 属性名 | 含义 | 示例 |
|---|---|---|
| name | 人物主名 | 宋江 |
| alias | 绰号 | 及时雨 |
| star | 星号 | 天魁星 |
| rank | 座次 | 1 |
| camp | 阵营 | 梁山 / 朝廷 / 方腊 |
| ending | 结局 | 被毒杀 / 善终 |
关系类型要控制粒度,太细会稀疏,太粗没信息量。常见粒度是共现、结义、师徒、兄弟、主仆、敌对、同僚、夫妻这几类,外加一条可选的共现关系作为默认边。关系表定义如下,字段weight在 2.2 节的共现统计里会用到:
| 关系类型 | 方向性 | 权重含义 |
|---|---|---|
| CO_OCCUR | 无向 | 共现次数 |
| BROTHER | 无向 | 结义亲密度 |
| ENEMY | 有向 | 敌对强度 |
| SERVE | 有向 | 主仆关系 |
这里有一个常见的认知误区:关系类型不是越多越好。新手容易把「认识」「见过」「打过」「帮过」全部拆开,最后图上一堆稀疏边,社区发现算法跑出来全是单点社区。高频关系收敛到八类左右,是经过调参验证的合理粒度。
2.2 共现分析算关系权重:滑动窗口统计的 Python 实现
图谱的边有了类型,还要有权重。权重从哪来?原著文本里没有现成的数字,常见做法是人物共现分析:同一回里同时出现的两个人,大概率存在某种关联,出现次数越多,关系越强。这是后续关系紧密度计算的基础输入。
核心实现是滑动窗口统计。思路可以拆成三步:先把全书按「第 X 回」切成章节,再在每章里找出所有人物名出现的位置,最后统计同一章节内距离小于阈值的两个人共现次数。
import re from collections import defaultdict def load_chapters(path="shuihu.txt"): """按『第X回』切分原著,过滤过短的片段。""" with open(path, encoding="utf-8") as f: text = f.read() chapters = re.split(r"第[一二三四五六七八九十百零]+回", text) return [c for c in chapters if len(c) > 200] def count_cooccurrence(chapters, alias_to_name, window=30): """统计人物共现:同章节内,两个名字出现位置的距离小于 window 个字符,记一次共现。""" co = defaultdict(int) for chapter in chapters: positions = [] # (位置, 主名) for alias, name in alias_to_name.items(): for m in re.finditer(alias, chapter): positions.append((m.start(), name)) positions.sort() for i in range(len(positions)): for j in range(i + 1, len(positions)): dist = positions[j][0] - positions[i][0] if dist < window: key = tuple(sorted([positions[i][1], positions[j][1]])) co[key] += 1 return co逻辑说明:load_chapters先把全书拆成章回,避免整本书一次性遍历导致内存和匹配噪声失控。count_cooccurrence对每个别名做正则匹配,命中后统一映射成主名,位置排序后两两比较距离。window=30的含义是两个人名在原文中相隔不超过 30 个字符(约一到两个分句长度)就算一次共现,小于这个值会把同回但无关的人物强行连线,大于这个值会丢失短距离强关联。
这个阈值取舍很影响图谱质量。window设太大,鲁智深和林冲这种同回配角都会被连成强关系;设太小,跨章回的老关系又捕捉不到。我通常先跑一遍输出共现次数分布,看中位数再回来调,不要一上来就信默认参数。
2.3 写入 Neo4j:MERGE、UNWIND 与唯一约束
共现矩阵算出来后,下一步就是写入图数据库。这一步最容易翻车:先用一条CREATE测试没问题,批量导入就开始刷重复节点——因为CREATE不看节点是否已存在,跑两遍数据就翻倍。正确的做法是先建唯一约束,再用MERGE幂等写入。
// 先建约束:name 唯一,这是 MERGE 不产生重复节点的前提 CREATE CONSTRAINT ON (p:Person) ASSERT p.name IS UNIQUE; // 批量写入节点:UNWIND 把 Python 传来的列表展开成多行 UNWIND $nodes AS row MERGE (p:Person {name: row.name}) SET p.alias = row.alias, p.star = row.star, p.rank = row.rank, p.camp = row.camp, p.ending = row.ending;逻辑说明:UNWIND是 Cypher 里的列表展开语法,配合 Python 驱动传参,一次请求写入几百条数据,比逐条CREATE快一个量级。MERGE会先按name查节点,存在就跳过,不存在才创建,配合唯一约束保证幂等。注意MERGE匹配的属性越少越好,只匹配name,其他属性用SET追加,否则后续要更新ending字段时,MERGE会去匹配整个属性快照,匹配不上又复制一份。
边写入是同样的套路,只是需要先确定两端节点存在:
UNWIND $edges AS row MATCH (a:Person {name: row.source}) MATCH (b:Person {name: row.target}) MERGE (a)-[r:CO_OCCUR {type: row.type}]->(b) SET r.weight = coalesce(r.weight, 0) + row.weight;MATCH两个端点是保证外键合法,如果源数据里有没录入的人物名,这里会直接跳过而不是创建悬空节点——这一点很重要,悬空节点是后期社区发现结果里出现无名社区的常见来源。coalesce处理首次写入时weight为空的情况,累加逻辑让多次导入的数据可以合并。
提示:导入前检查一下驱动用的是不是 Neo4j 官方 Python 驱动
neo4j包,而不是只支持 HTTP 的旧版 py2neo。前者能传$nodes这种参数化列表,后者对 UNWIND 传参支持比较别扭,会出现参数类型对不上的玄学报错。
3. Cypher 查询与 APOC 图算法:路径检索、社区发现怎么落地
3.1 深度路径查询:宋江和武松之间隔了几层关系
图数据库相比关系型数据库的核心优势,就是多跳关系查询不需要反复 JOIN。《水浒传》人物关系里最典型的提问是「宋江和武松是怎么产生关联的」,在关系型表结构里你要把边表自连接好几次,而在 Neo4j 里一条 Cypher 就能拿结果。
// 找宋江到武松的最短路径,边数不超过 4 MATCH p = shortestPath( (a:Person {name: "宋江"})-[*1..4]-(b:Person {name: "武松"}) ) RETURN p, length(p) AS depth;逻辑说明:shortestPath返回两点间经过边数最少的路径,[*1..4]限定可变长度关系的范围是 1 到 4 跳。不写这个上限的话,全图都是连通的,任何两点都能找到路径,查询也会随图规模膨胀变慢。length(p)用来显示路径深度,前端展示时可以按深度染色。
如果要看所有路径而不是最短路径,把shortestPath去掉直接MATCH p=(a)-[*1..4]-(b)即可,但注意结果集是组合爆炸的,宋江和武松这种核心人物之间的 4 跳路径可能有几十上百条,建议加LIMIT 20再看。实际项目里我更常用的是限定关系类型的方向版本:
MATCH p = (a:Person {name: "宋江"})-[:BROTHER|CO_OCCUR*1..3]-(b:Person {name: "武松"}) RETURN p LIMIT 10;这段表达的含义是只走BROTHER和CO_OCCUR两类边,路径最多 3 跳。这样能把「敌对—师徒」这种语义上不算亲近的路径过滤掉,返回的路径更有解释力。做问答系统时,这个查询就是「宋江和武松是什么关系」这类问题的最终执行体。
另一个值得提的点是查询性能。4 跳以内的小图查询几乎秒回,但如果你把上限放大到 6 跳甚至 10 跳,核心人物的路径会指数增长。我一般先跑PROFILE MATCH p = (a:Person {name:"宋江"})-[*1..4]-(b) RETURN count(p)看一眼行数,超过几千条就缩跳数或者加关系类型过滤,不要硬查。
3.2 APOC 接入社区发现与关系紧密度计算
图谱建好只是第一步,这个项目能拿 98 分,很大程度靠 APOC 插件提供的图算法。社区发现能回答「水浒传里有哪些派系」,关系紧密度计算能回答「宋江最铁的兄弟是谁」,这两个问题都是纯 Cypher 很难高效完成的,需要算法库支撑。
Neo4j 3.5 / 4.x 时代常见的做法是调用 APOC 的社区发现过程。以标签传播算法为例:
CALL apoc.algo.community( 25, // 迭代次数 'MATCH (n:Person) RETURN id(n) AS id', // 全量节点 'MATCH (a:Person)-[r:CO_OCCUR]->(b:Person) RETURN id(a) AS source, id(b) AS target, r.weight AS weight', // 带权边 'out' ) YIELD communityId, nodeId RETURN communityId, collect(nodeId) AS members;参数说明:第一个参数25是标签传播的迭代上限,一般 20 到 30 次就能收敛;第二个和第三个参数分别是节点集和边集的 Cypher 查询,注意这里传的是「查询字符串」,不是直接执行;最后一个'out'表示只按边的出方向传播。跑完之后,同一个communityId里的节点就是算法判定的一伙人。
这里我要特别提醒:apoc.algo.community在新版本 Neo4j 里被逐步废弃,官方重心移到了 GDS(Graph Data Science)库。如果打开的是 Neo4j 5.x,直接调用 APOC 会报Procedure not found,需要换成gds.labelPropagation.stream()的写法。这份源码大概率用的是 3.5 或 4.x 时代的 APOC 接口,跑不起来时先检查版本,不要怀疑逻辑。
关系紧密度计算更直接,本质是加权度数:一个节点连接的所有边的weight之和。Cypher 原生就能算,适合放在问答接口里做实时查询:
MATCH (a:Person {name: "宋江"})-[r]-() RETURN a.name AS person, sum(r.weight) AS closeness, count(r) AS degree ORDER BY closeness DESC;这段查询统计的是宋江所有关系边的权重总和closeness,以及关联人数degree。为什么用权重和而不是直接用count?因为共现次数能体现相处密度,武松和宋江共现 50 次、杨志和宋江共现 5 次,前者关系更紧密,更符合直觉。这个值喂给前端就能渲染「关系热力」效果,答辩时展示出来比静态图有说服力得多。
4. Flask 问答接口与 ECharts 可视化:把图谱变成能对话的系统
4.1 意图解析:从自然语言问题到 Cypher 模板
智能问答模块是这个项目的另一个核心亮点。原理不复杂:先把用户问题归类成几种意图,每种意图对应一个 Cypher 查询模板,再用正则从问题里抽出人物名和属性名,填进模板执行。模板匹配方案比训练一个 NER 模型轻量得多,对毕业设计来说性价比最高,也不容易失控。
意图分类表是核心配置,结构如下:
| 意图类型 | 问题示例 | 生成的 Cypher 模式 |
|---|---|---|
| attribute | 宋江的绰号是什么 | MATCH (n:Person {name:'X'}) RETURN n.alias |
| relation | 宋江和武松什么关系 | MATCH (a:Person {name:'X'})-[r]-(b:Person {name:'Y'}) RETURN type(r) |
| path | 宋江和武松怎么认识的 | MATCH p=shortestPath((a)-[*1..4]-(b)) RETURN p |
| rank | 武松排第几 | MATCH (n:Person {name:'X'}) RETURN n.rank |
| group | 梁山有哪些天罡 | MATCH (n:Person {camp:'梁山'}) WHERE n.rank<=36 RETURN n.name |
意图解析的 Python 实现写成规则列表,逐个匹配:
import re INTENTS = [ { "type": "attribute", "pattern": r"(.+?)的(绰号|星号|座次|阵营|结局)是什么", "cypher": "MATCH (n:Person {{name:'{0}'}}) RETURN n.{1} AS value", }, { "type": "relation", "pattern": r"(.+?)和(.+?)什么关系", "cypher": ("MATCH (a:Person {{name:'{0}'}})-[r]-(b:Person {{name:'{1}'}}) " "RETURN type(r) AS relation, r.weight AS weight"), }, { "type": "path", "pattern": r"(.+?)怎么(认识|联系).*?(.+?)", "cypher": ("MATCH p=shortestPath((a:Person {{name:'{0}'}})-[*1..4]-" "(b:Person {{name:'{1}'}})) RETURN p"), }, ] def parse(question): for intent in INTENTS: m = re.search(intent["pattern"], question) if m: groups = [ALIAS_TO_NAME.get(g, g) for g in m.groups()] return intent["type"], intent["cypher"].format(*groups) return "unknown", None逻辑说明:INTENTS里每条规则由pattern和cypher组成,pattern用正则捕获人物名和属性名,cypher字符串里的{0}、{1}是占位符,用format填充。填充前先经过ALIAS_TO_NAME做别名归一化,用户问「及时雨的绰号」和「宋江的绰号」走同一条查询。注意 Cypher 里的花括号需要写成双花括号{{name:'{0}'}}转义,否则format会跟 Cypher 的参数语法打架,这个细节很容易踩。
正则方案的上限是固定的,但封闭式问题的准确率足够高。实际测试里,用户问「宋江的座次是多少」「李逵的星号是什么」这类问题基本能命中,开放式问题会落到 unknown,返回兜底话术即可。另外一个值得注意的点是:如果用户问「宋江的结局」而pattern里没有匹配结局字段,解析会失败,所以属性类规则要覆盖所有字段,别漏。
4.2 RESTful API 与前端关系图联动
后端采用 Flask 搭建 RESTful API 服务,整体是 B/S 架构:前端 ECharts 渲染,后端 Flask 执行 Cypher 并返回 JSON。这个分层让前后端可以独立测试,也是答辩时容易被提问的点。
from flask import Flask, request, jsonify from neo4j import GraphDatabase app = Flask(__name__) driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password")) @app.route("/api/qa", methods=["POST"]) def qa(): data = request.get_json() question = data.get("question", "") intent, cypher = parse(question) if not cypher: return jsonify({"intent": "unknown", "answer": "暂时答不上来,换个问法试试"}) with driver.session() as session: result = session.run(cypher) records = [record.data() for record in result] return jsonify({"intent": intent, "answer": records[:5]})参数说明:GraphDatabase.driver连接的是 Bolt 端口 7687,不是 HTTP 端口 7474,这个区分在本地调试时经常让人卡住。auth里填的是 Neo4j 初始账号密码,默认neo4j/neo4j,首次登录会强制要求修改。每个请求打开一个session,用完自动关闭,避免连接泄漏。records[:5]限制返回条数,防止路径类问题的结果集把响应体撑爆。
这里补充一个常见的工程坑:如果你用npm start起前端 dev server 在 5000 端口,Flask 也在 5000 端口,会直接冲突。我一般把 Flask 改成 5001,前端请求加跨域头:
from flask_cors import CORS CORS(app)前端 ECharts 关系图的数据结构是固定三件套:nodes、links、categories。后端额外提供一个图谱全量接口,前端拿到后直接填充:
fetch("/api/graph") .then((res) => res.json()) .then((data) => { option = { series: [{ type: "graph", layout: "force", data: data.nodes, links: data.links, categories: data.categories, force: { repulsion: 300, edgeLength: 80 }, label: { show: true, fontSize: 10 }, }], }; myChart.setOption(option); });layout: "force"表示力导向布局,节点之间根据边的权重互相排斥或吸引,权重大的两个人会被拉得更近,视觉效果上直接能看出派系聚集。repulsion控制排斥力,值越大图越散,节点多的时候要往大调,否则挤成一团看不清标签。
提示:前端返回的
links里一定要带value字段,ECharts 在 force 布局下会用value做连线的粗细映射,没带value的图所有边一样粗,看不出亲疏远近,答辩时视觉效果大打折扣。
5. Neo4j 避坑指南:安装、乱码、约束、APOC、Docker 五个翻车现场
这套源码我前后跑了两遍,第一遍从零搭环境,第二遍补数据导入和问答调试,踩过的坑基本都在 Neo4j 这一层。下面五条按「现象 → 原因 → 解决」整理,对照检查能省下至少半天。
5.1 Neo4j 安装后本地连不上:JDK 版本与端口认证
现象:neo4j start后进程退出,浏览器打开 7474 端口白屏;或者 7474 能开,Python 驱动连 Bolt 7687 报Service unavailable。
原因:Neo4j 3.5 这个常见教学版本强依赖 JDK 8,装成 JDK 11 之后服务虽然能起,但 Web 管理界面和 Bolt 协议部分接口会异常;另外 Neo4j 4.x 之后默认开启强认证,首次连接要用初始密码neo4j修改后才放行。
解决:先执行java -version确认当前 JDK 版本,3.5 就老老实实配 JRE 8。本地调试用 Neo4j Desktop 最省事,它会自动管理对应版本的 JDK;社区版 bin 目录下手动neo4j start也可以,但注意neo4j.conf里dbms.connectors.default_listen_address如果被改成局域网地址,本地localhost反而连不上。如果 7474 能开而 7687 连不上,检查防火墙和端口映射,Docker 部署时最常见。
5.2 导入后全是中文乱码
现象:Python 读原著文本正常,写入 Neo4j 后浏览器里看到的节点名变成江湖一类乱码。
原因:经典编码问题。Neo4j 导入 CSV 时会按系统默认编码解析,Linux 默认 UTF-8 没问题,Windows 下默认 GBK,而源文件通常是 UTF-8,两边对不上就乱码。Python 侧如果open()没显式指定encoding="utf-8",在 Windows 上同样会读错。
解决:Python 读写文件一律显式encoding="utf-8"。导入 CSV 前检查有没有 BOM 头,有 BOM 的话 Neo4j 会把\ufeff带进第一个字段,节点名开头多个不可见字符,用utf-8-sig重新写一遍即可。最稳妥的办法是绕过 CSV,直接用 Python 驱动传 UNWIND 参数,全程走内存字符串,从源头避免编码问题。
5.3 MERGE 导入两遍数据翻倍
现象:同一批节点数据导入两次,查询count(n)从 108 变成 216,节点全部重复。
原因:没建唯一约束之前,MERGE的匹配逻辑会退化成全属性匹配,属性快照稍有差异就会新建节点;更隐蔽的是如果两条记录写入顺序不同,MERGE匹配不到就各自创建一个,产生「看起来一样但 id 不同」的重复节点。
解决:导入前先执行CREATE CONSTRAINT ON (p:Person) ASSERT p.name IS UNIQUE;,约束建立后MERGE才会真正按name去重。如果已经产生了重复数据,需要先清理再建约束,清理脚本长这样:
MATCH (p:Person) WITH p.name AS name, collect(p) AS nodes WHERE size(nodes) > 1 FOREACH (n IN tail(nodes) | DELETE n);这段的逻辑是:按name分组,收集该组所有节点,只保留第一个,其余全部删除。注意执行前先备份数据库,这个操作不可逆。
5.4 APOC 插件不生效:Procedure not found
现象:调用CALL apoc.algo.community报There is no procedure with the name apoc.algo.community,但插件目录里明明放了 jar 包。
原因:APOC 的版本必须和 Neo4j 主版本严格对应,Neo4j 3.5 要用 3.5 系列的 APOC 包,4.4 要用 4.4 系列。放错版本不会报安装失败,只是过程找不到。另外有些 APOC 过程被dbms.security.procedures.whitelist黑名单拦截,需要显式放行。
解决:按 Neo4j 版本号去 APOC 官方仓库选对应包,下载后放进plugins/目录,重启 Neo4j 生效。手动部署模式下检查neo4j.conf里有没有dbms.security.procedures.whitelist=apoc.*配置,没有就补上。验证是否加载成功,执行一句RETURN apoc.version(),能返回版本号就说明装对了。
5.5 Docker 部署后数据丢失
现象:按 Docker 方式部署 Neo4j,容器每次重启后图谱数据全空,之前导入的几百条关系不见了。
原因:容器是无状态的,官方neo4j镜像默认把数据写在容器内/data,没有挂载宿主目录时,容器删除或重建,数据就一起没了。
解决:启动时加卷挂载参数,把/data和/logs都挂到宿主机:
docker run -d \ --name neo4j \ -p 7474:7474 -p 7687:7687 \ -v $PWD/neo4j_data:/data \ -v $PWD/neo4j_logs:/logs \ neo4j:3.5端口方面,两个端口都要映射,少映射 7687 会导致 Python 驱动连不上而浏览器正常,这种「页面能开但接口报错」的现象最容易让人误判。
6. 进阶技巧:把关系图升级成带权图,跑一次真正的最短加权路径
6.1 权重写入与迪杰斯特拉查询
图建好、问答能跑通之后,如果想在答辩里多一个加分点,把 2.2 节算出来的共现次数正式写成边的weight,然后跑迪杰斯特拉算法查「宋江认识武松的最铁路径」——注意是最铁,不是最短。这两个概念的区别是,普通shortestPath只数边数,带权最短路径综合了每条边的亲密度,能回答「宋江通过谁认识武松最顺理成章」这种更有解释力的问题。
先把权重显式更新到边上。虽然导入时weight已经累加过,但为了演示完整流程,补一次聚合写入:
MATCH (a:Person)-[r:CO_OCCUR]->(b:Person) WITH a, b, sum(r.weight) AS w MERGE (a)-[r2:CO_OCCUR]-(b) SET r2.weight = w;这段的含义是重新按端点聚合所有权重,写回同一条边。执行前先确认没有重复边,否则MERGE会把两条平行边合并成一条,weight只保留一份,数据会变少。稳妥做法是先查MATCH (a)-[r:CO_OCCUR]->(b) RETURN a.name, b.name, count(*)看有没有大于 1 的组。
然后跑加权最短路径。Neo4j 3.5 时代用 APOC 的迪杰斯特拉实现,4.x 之后建议用 GDS:
CALL apoc.algo.dijkstra( 'MATCH (a:Person {name:"宋江"}) RETURN a', // 起点 'MATCH (b:Person {name:"武松"}) RETURN b', // 终点 'CO_OCCUR', // 关系类型 'weight' // 权重属性 ) YIELD path, weight RETURN path, weight;参数说明:前两个参数是返回单节点的 Cypher 查询,不能用MATCH ... RETURN a, b这种多行写法;第三个参数限定走CO_OCCUR边,避免路径里混入ENEMY这类语义相反的关系;第四个参数指定权重字段名。返回的weight是路径总权重,数值越小代表路径越「近」。如果想让亲密度高反而数值小,可以在写入时用1.0 / r.weight做一次倒数变换。
跑出来的路径喂给前端,把路径上的节点和边高亮,其他节点做透明度区分。这个展示效果很直观:宋江 → 李逵 → 武松 和 宋江 → 柴进 → 武松 两条路摆在一起,评委一眼就能看出图数据库的价值,比口头解释「多跳查询」有说服力得多。
最后补一个习惯:从那以后我每次导入数据都会强制走一遍「约束检查 → 幂等重导 → 权重聚合 → 路径验证」四步流程,前两步保数据不脏,后两步保查询有结果。这套流程本身也是答辩时能讲清楚的内容。这份源码本来就是网络分享出来供学习交流用的,拿来当毕设参考正合适。希望这份实战拆解能帮到你,下载后对照第 5 章的五个坑先过一遍,能少走不少弯路。
本文还有配套的精品资源,点击获取