图数据库

引例

假设我们要做一个”社交网络”系统,需要存储用户之间的关注关系,用来存储数据的数据库表如下:

```
-- 用户表
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    name VARCHAR(64)
);

-- 关注关系表
CREATE TABLE follow (
    id BIGINT PRIMARY KEY,
    from_user_id BIGINT,
    to_user_id BIGINT
);
```

数据大概长这样:

user 表:

idname
1张三
2李四
3王五
4赵六
5钱七

follow 表:

idfrom_user_idto_user_id
112
223
334
415
554

即:张三关注了李四、李四关注了王五、王五关注了赵六、张三关注了钱七、钱七关注了赵六。

现在问题来了:如果我们要查询张三到赵六之间,最短的关注链路是什么?,这个关注链路如果要从MySQL中查出来,我们大概只能使用穷举的思路:

  • 查询,张三是否直接关注了赵六,查到结束,否则继续——> 查询直接关注的情况
```
SELECT * FROM follow WHERE from_user_id = 1 AND to_user_id = 4;
```
  • 若1没查到,在查询张三—>x, x—>赵六的情况,查到结束,否则继续 ——> 查询关注路径中间隔一人的情况
```
SELECT * FROM follow f1
JOIN follow f2 ON f1.to_user_id = f2.from_user_id
WHERE f1.from_user_id = 1 AND f2.to_user_id = 4;
```
  • 若2没查到,在查询张三—>x, x—>y, y—>赵六的情况,查到结束,否则继续 ——>查询关注路径中间隔两人的情况
```
SELECT * FROM follow f1
JOIN follow f2 ON f1.to_user_id = f2.from_user_id
JOIN follow f3 ON f2.to_user_id = f3.from_user_id
WHERE f1.from_user_id = 1 AND f3.to_user_id = 4;
```

….

看出问题了吗?

  1. 每多一层关系,就要多一次 JOIN。如果要查 6 层关系,就需要 6 次 JOIN,SQL 写起来极其复杂。
  2. 性能急剧下降。每次 JOIN 都是一次全表扫描或索引查找,层数越多性能越差。
  3. 层数不确定。我们事先不知道两个人之间隔了几层,所以无法写出一个固定的 SQL。

所以总结一下,问题的核心是什么呢? 核心在于关系数据库中存储的数据之间的关系仅仅存在于逻辑上

  • 虽然表面上看,关注表存储了用户之间的关注关系,但是它只是一种逻辑上的表示,从物理上讲关注表和用户表之间完全独立没有任何联系
  • 单看用户表或者单看关注表,你根本无法真正的知道用户之间的关注关系,必须通过Join运算将具有关系的数据查询出来(在同一张表中),才算是真的将有关系的数据关联在了一起
  • 正因为如此,在上面的查询中多次用到了Join运算,也就是说,真正的数据之前的联系,是被计算出来的
  • 因此,关系越复杂,计算量越大

那么问题来了,加入我们就是需要快速获取这种数据之间的复杂关系,怎么办呢?其实思路很简单,我们把数据以及其关系物理存储即可,于是我们就引出了一种全新的数据库——图数据库!

图数据库

图数据库天生就是为”关系遍历”设计的。因为图数据库的底层存储结构就是按照”节点-关系”来组织和存储的,遍历关系就像沿着指针走链表一样高效,这样一来无论关系有多少层,性能都不会像 MySQL 那样急剧下降

同样的关注数据,在图数据库中的存储结构如下:

```
graph LR
    subgraph Neo4j 存储结构
        ZS((张三)) -->|FOLLOW| LS((李四))
        LS -->|FOLLOW| WW((王五))
        WW -->|FOLLOW| ZL((赵六))
        ZS -->|FOLLOW| QQ((钱七))
        QQ -->|FOLLOW| ZL
    end
```

每个圆圈是一个节点(Node),每条箭头是一条关系(Relationship)。和MySQL这样的关系数据库的关键区别在于:在 Neo4j 这样的图数据库中,每个节点通过物理指针直接指向它的邻居节点,关系不是被计算出来的,而是直接挂在节点上

所以当我们要查”张三到赵六的最短路径”时:

  • 从张三节点出发,直接沿着指针走到李四、钱七(不需要搜索任何表)
  • 从李四沿指针走到王五,从钱七沿指针走到赵六(找到了!)
  • 每一跳都是 O(1) 的指针访问,整个过程不涉及任何 JOIN 运算

用 Cypher(图数据库的查询语言)一行就能搞定:

```
MATCH path = shortestPath((a:User {name:'张三'})-[:FOLLOW*]-(b:User {name:'赵六'}))
RETURN path
```

图数据库中的基本概念

在关系型数据库中,数据存储涉及:数据库、表、字段、数据行。

在图数据库中,也有一套对应的概念体系:

关系型数据库图数据库说明
数据库数据库一样的概念,一个独立的数据存储空间
标签(Label)用来对节点分类,类似于”这个节点属于哪张表”
数据行节点(Node)图中的一个实体,比如一个人、一部电影
字段/列属性(Property)节点或关系上的键值对数据
外键关联关系(Relationship)两个节点之间的连接,有方向、有类型

下面逐一介绍:

标签(Label)

标签就是一个纯粹的名称,用来给节点分类。你可以把它理解为一个”类型名称”——它本身没有任何其他东西,不像 MySQL 的表那样还要定义列名、列类型、约束等。

比如在一个电影数据库中,我们可能有这些标签:

  • Person —— 表示这个节点是一个人
  • Movie —— 表示这个节点是一部电影

和 MySQL 的表有什么区别?

MySQL 的表 = 名称 + 结构定义(列名、类型、约束),而图数据库的标签 = 只有名称。

具体来说:

  • 不需要预先定义:MySQL 中必须先 CREATE TABLE 才能插入数据。但在图数据库中,标签不需要提前创建,你在创建节点时直接指定标签即可,标签会自动生成
  • 不需要定义结构:同一个标签下的不同节点,可以有完全不同的属性,图数据库不做任何限制
  • 一个节点可以属于多个标签:比如一个节点可以同时是 Person 和 Director(既是人,又是导演)

节点(Node)

节点是图数据库中的基本数据单位,代表一个实体。类比 MySQL 中的一行数据。

比如:

  • “周星驰” 是一个 Person 节点
  • “功夫” 是一个 Movie 节点

节点的唯一标识:

每个节点在创建时,图数据库会自动分配一个内部 ID(类似 MySQL 的自增主键)。但在实际使用中,我们通常通过节点的属性来标识它(比如通过 name 属性查找节点),而不是直接使用内部 ID。

创建节点的语法:==

```

```

和标签一样,节点不需要提前定义结构。你可以随时创建一个节点,给它任意的标签和属性:

```
# 创建一个具有name,born属性的节点,节点的标签为Person
CREATE (n:Person {name: '周星驰', born: 1962})
# 创建一个具有title,released属性的节点,节点的标签为Movie
CREATE (m:Movie {title: '功夫', released: 2004})
```

同样是 Person 标签的节点,一个可以有 born 属性,另一个可以没有——图数据库不强制要求同一标签下的节点具有相同的属性。

属性(Property)

属性是节点或关系上的键值对数据,类比 MySQL 中的字段值。

比如:

  • Person 节点的属性:{name: "周星驰", born: 1962}
  • Movie 节点的属性:{title: "功夫", released: 2004, genre: "动作喜剧"}

属性是完全自由的:

  • 不需要提前声明有哪些属性
  • 不需要指定属性的数据类型
  • 同一标签下的不同节点可以有不同的属性集合

这和 MySQL 完全不同——MySQL 中你必须先 ALTER TABLE ADD COLUMN 才能给数据加新字段,而图数据库中直接写就行了。

属性支持的数据类型:

虽然属性不需要预先声明类型,但属性值本身是有类型的。Neo4j 支持以下数据类型:

类型说明Cypher 中的写法示例
INTEGER整数123-5
FLOAT浮点数3.14-0.5
STRING字符串'hello'"world"
BOOLEAN布尔值truefalse
DATE日期date('2024-03-15')
TIME时间time('12:30:00')
DATETIME日期时间datetime('2024-03-15T12:30:00')
DURATION时间段duration('P1Y2M3D') 表示1年2月3天
LIST列表(同类型元素)[1, 2, 3]['a', 'b']

使用示例:

```
CREATE (m:Movie {
  title: '功夫',
  released: 2004,
  rating: 8.5,
  isClassic: true,
  releaseDate: date('2004-12-23'),
  tags: ['动作', '喜剧', '功夫']
})
```

注意:LIST 中的元素必须是同一种类型,不能混合(比如不能 [1, 'abc', true])。

关系(Relationship)

关系连接两个节点,表示它们之间的联系。关系有三个要素:

  • 方向:从哪个节点指向哪个节点
  • 类型:这是什么关系(用一个名称标识)
  • 属性(可选):关系本身也可以携带数据

举例:

```
(周星驰)-[:ACTED_IN {role: '阿星'}]->(功夫)
```

这条关系表示:周星驰出演了功夫这部电影关系的类型为ACTED_IN,并且关系上有一个属性 role(角色名)= “阿星”。

```
(周星驰)-[:DIRECTED]->(功夫)
```

这条关系表示:周星驰导演了功夫这部电影。这条关系没有额外属性。

关系的特点:

  • 关系必须有方向(从谁指向谁)
  • 关系必须有类型(是什么关系)
  • 关系的属性是可选的,可以有也可以没有
  • 两个节点之间可以有多条不同类型的关系(比如周星驰既是功夫的演员,又是功夫的导演)

关于标签,结点,属性的命名,习惯上采用驼峰命名方式

关于关系的命名,习惯上采用JAVA语言的常量命名方式

图数据库中所有的命名都是大小写敏感的

用一张图来理解

```
graph LR
    P1((周星驰<br/>Person)) -->|"ACTED_IN<br/>{role:'阿星'}"| M1[功夫<br/>Movie]
    P1 -->|DIRECTED| M1
    P2((吴孟达<br/>Person)) -->|"ACTED_IN<br/>{role:'包租公'}"| M1
    P3((元华<br/>Person)) -->|"ACTED_IN<br/>{role:'包租婆老公'}"| M1
    P1 -->|"ACTED_IN<br/>{role:'至尊宝'}"| M2[大话西游<br/>Movie]
```

从这张图可以看出:

  • 一个人可以出演多部电影(一对多)
  • 一部电影可以有多个演员(多对多)
  • 同一个人和同一部电影之间可以有多种关系(周星驰既演了功夫又导了功夫)
  • 关系可以带属性,也可以不带

Neo4j 图数据库

Neo4j 是目前最流行的图数据库,就像 MySQL 是最流行的关系型数据库一样。

  • 开源图数据库,社区版免费
  • 使用 Cypher 查询语言(类比 MySQL 的 SQL)
  • 提供 Web 管理界面(类比 MySQL 的 Navicat / phpMyAdmin)
  • 支持 ACID 事务

Cypher 是 Neo4j 的查询语言,就像 SQL 是 MySQL 的查询语言一样。接下来,我们按照类似 SQL 的分类方式来学习 Cypher:

数据操作 — 创建(类比 INSERT)

创建节点

```
CREATE (变量:标签 {属性名1: 值1, 属性名2: 值2, ...})
```

语法解释:

  • () 表示一个节点
  • 变量 是变量名(类比 SQL 中的别名,可省略),表示当前正在床架你的节点
  • :标签 是节点的标签
  • {...} 是属性(键值对)

怎么理解变量名?

变量名就是给匹配到的节点或关系起一个临时名字,方便在同一条 Cypher 语句的后续部分引用它。

比如:

```
// 查询具有Person标签,且name属性值为周星驰的节点,查询其born属性即出生年份
MATCH (p:Person {name: '周星驰'})
RETURN p.born
```

这里:p 指向匹配到的 Person 节点,所以可以用 p.name 取出节点的 name 属性

本质上,它就像 SQL 里的别名,比如:

```
SELECT u.name FROM user u;
```

如果后面不需要用到这个节点或关系,变量名是可以省略的:

```
MATCH (:Person {name: '周星驰'})-[:ACTED_IN]->(m:Movie)
RETURN m.title
```

创建节点举例:

```
CREATE (p:Person {name: '周星驰', born: 1962})  // 或者可以省略变量 CREATE (:Person {name: '周星驰', born: 1962})
CREATE (m:Movie {title: '功夫', released: 2004}) // 或者可以省略变量 CREATE (:Movie {title: '功夫', released: 2004})
```

创建关系

```
-- 先找到两个已有的节点,然后创建关系
MATCH (a:标签A {条件}), (b:标签B {条件})
CREATE (a)-[:关系类型 {属性}]->(b)

-- 也可以在创建节点的同时创建关系
CREATE (a:标签A {属性})-[:关系类型]->(b:标签B {属性})
```

语法解释:

  • MATCH 关键字用来查找已存在的节点或关系,类比 SQL 中的 SELECT。它按照给定的模式(标签、属性、关系等)在图中匹配出符合条件的节点或关系,并把它们赋值给变量,供后续使用
  • 上面第一种写法中,MATCH 先把符合条件的两个节点 a 和 b 找出来,然后 CREATE 在它们之间创建关系
  • -[:关系类型]-> 表示一条有方向的关系
  • -> 表示方向:从左边节点指向右边节点
  • <- 表示反方向
  • 关系的属性是可选的

举例:

```
-- 找到周星驰和功夫,创建出演关系(带属性)
MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '功夫'})
CREATE (p)-[:ACTED_IN {role: '阿星'}]->(m)

-- 创建节点的同时创建关系(不带属性)
CREATE (p:Person {name: '元华'})-[:ACTED_IN]->(m:Movie {title: '功夫'})
```

当然,如果创建关系时MATCH (a:标签A {条件}), (b:标签B {条件})查询出的不是两个节点,而是两个节点集合,那么neo4j会为满足MATCH (a:标签A {条件})的m个节点,和满足MATCH (b:标签B {条件})添加的n个节点创建都创建关系,一共是m x n个关系

数据操作 — 查询(类比 SELECT)

数据准备:

```
// 创建演员节点
CREATE (:Person {name: '周星驰', born: 1962});
CREATE (:Person {name: '吴孟达', born: 1952});
CREATE (:Person {name: '元华', born: 1950});
CREATE (:Person {name: '黄圣依', born: 1983});
CREATE (:Person {name: '张柏芝', born: 1980});

// 创建电影节点
CREATE (:Movie {title: '功夫', released: 2004});
CREATE (:Movie {title: '少林足球', released: 2001});
CREATE (:Movie {title: '喜剧之王', released: 1999});
CREATE (:Movie {title: '大话西游', released: 1995});
CREATE (:Movie {title: '长江七号', released: 2008});

// 创建电影类型节点
CREATE (:Type {name: '喜剧'});
CREATE (:Type {name: '动作'});
CREATE (:Type {name: '爱情'});
CREATE (:Type {name: '科幻'});

// 创建演员出演电影的关系
MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '功夫'})
CREATE (p)-[:ACTED_IN {role: '阿星'}]->(m);

MATCH (p:Person {name: '元华'}), (m:Movie {title: '功夫'})
CREATE (p)-[:ACTED_IN {role: '包租公'}]->(m);

MATCH (p:Person {name: '黄圣依'}), (m:Movie {title: '功夫'})
CREATE (p)-[:ACTED_IN {role: '芳儿'}]->(m);

MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '少林足球'})
CREATE (p)-[:ACTED_IN {role: '五师兄'}]->(m);

MATCH (p:Person {name: '吴孟达'}), (m:Movie {title: '少林足球'})
CREATE (p)-[:ACTED_IN {role: '明锋'}]->(m);

MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '喜剧之王'})
CREATE (p)-[:ACTED_IN {role: '尹天仇'}]->(m);

MATCH (p:Person {name: '张柏芝'}), (m:Movie {title: '喜剧之王'})
CREATE (p)-[:ACTED_IN {role: '柳飘飘'}]->(m);

MATCH (p:Person {name: '吴孟达'}), (m:Movie {title: '喜剧之王'})
CREATE (p)-[:ACTED_IN {role: '卧底'}]->(m);

MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '大话西游'})
CREATE (p)-[:ACTED_IN {role: '至尊宝'}]->(m);

MATCH (p:Person {name: '吴孟达'}), (m:Movie {title: '大话西游'})
CREATE (p)-[:ACTED_IN {role: '二当家'}]->(m);

MATCH (p:Person {name: '周星驰'}), (m:Movie {title: '长江七号'})
CREATE (p)-[:ACTED_IN {role: '周铁'}]->(m);

// 创建电影所属类型的关系
MATCH (m:Movie {title: '功夫'}), (t:Type {name: '喜剧'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '功夫'}), (t:Type {name: '动作'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '少林足球'}), (t:Type {name: '喜剧'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '少林足球'}), (t:Type {name: '动作'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '喜剧之王'}), (t:Type {name: '喜剧'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '喜剧之王'}), (t:Type {name: '爱情'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '大话西游'}), (t:Type {name: '喜剧'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '大话西游'}), (t:Type {name: '爱情'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '长江七号'}), (t:Type {name: '喜剧'})
CREATE (m)-[:BELONGS_TO]->(t);

MATCH (m:Movie {title: '长江七号'}), (t:Type {name: '科幻'})
CREATE (m)-[:BELONGS_TO]->(t);
```

通用语法:

```
MATCH (变量:标签 {条件})
WHERE 额外条件
RETURN 变量.属性, ...
```

MATCH 是查询的核心关键字,用来描述你要匹配的”图模式”(哪些节点、什么关系、什么方向),match中所使用的匹配条件只能是等值条件

WHERE也是查询的核心关键字,用来针对满足match”图模式”匹配的结果,根据指定条件匹配, where中可以使用的条件就多了>, <, =,!=,AND OR NOT,IN等等都可以使用

查询所有某类节点:

```
-- 通用:查询某个标签的所有节点
MATCH (变量:标签)
RETURN 变量.属性

-- 举例:查询所有电影
MATCH (m:Movie)
RETURN m.title, m.released
```

条件查询:

```
-- 通用:在属性中直接写条件
MATCH (变量:标签 {属性名: 值})
RETURN 变量

-- 通用:用 WHERE 子句写条件
MATCH (变量:标签)
WHERE 变量.属性名 = 值
RETURN 变量

-- 举例
MATCH (p:Person {name: '周星驰'})
RETURN p

MATCH (m:Movie)
WHERE m.released > 2000
RETURN m.title
```

关系查询(图数据库的核心优势):

```
-- 通用:查询从 a 出发、通过某种关系、到达 b 的模式
MATCH (a:标签A {条件})-[:关系类型]->(b:标签B)
RETURN b.属性

-- 通用:反方向查询
MATCH (b:标签B {条件})<-[:关系类型]-(a:标签A)
RETURN a.属性

-- 通用:多跳查询(链式关系)
MATCH (a:标签A {条件})-[:关系1]->(b:标签B)-[:关系2]->(c:标签C)
RETURN a, b, c
```

可能有人会问为什么可以实现反方向查询?因为 Neo4j 在关系的两端节点上都存了指针。可以用下图理解:

```
graph LR
    ZXC["周星驰节点<br/>出边指针: →功夫"]
    GF["功夫节点<br/>入边指针: ←周星驰"]
    ZXC -->|ACTED_IN| GF
```
  • 周星驰节点上记录着:”我有一条 ACTED_IN 出去,指向功夫”
  • 功夫节点上记录着:”有一条 ACTED_IN 进来,来自周星驰”

虽然这条关系只有一条、方向只有一个(周星驰 → 功夫),但两端节点都能直接通过指针访问到对方。所以:

  • (周星驰)-[:ACTED_IN]->(功夫) 从周星驰节点出发,沿”出边指针”走到功夫
  • (功夫)<-[:ACTED_IN]-(周星驰) 从功夫节点出发,沿”入边指针”走到周星驰

两个方向的遍历都是 O(1) 指针访问,性能完全相同。

<- 并不代表”反向存储了一条关系”,它只是告诉查询引擎:”我现在要从指向当前节点的那一侧遍历过去”。

举例:

```
-- 查询周星驰出演了哪些电影
MATCH (p:Person {name: '周星驰'})-[:ACTED_IN]->(m:Movie)
RETURN m.title

-- 查询功夫这部电影有哪些演员
MATCH (m:Movie {title: '功夫'})<-[:ACTED_IN]-(p:Person)
RETURN p.name

-- 两跳查询:查询周星驰出演的电影属于哪些类型
-- 假设有 Movie -[:BELONGS_TO]-> Type 这种关系
MATCH (p:Person {name: '周星驰'})-[:ACTED_IN]->(m:Movie)-[:BELONGS_TO]->(t:Type)
RETURN m.title, t.name
```

聚合与排序:

```
-- 通用
MATCH 模式
RETURN 属性, 聚合函数(变量) AS 别名
ORDER BY 别名 DESC
LIMIT 数量

-- 举例:查询每部电影有多少演员,按人数降序
MATCH (p:Person)-[:ACTED_IN]-> (m:Movie)
RETURN m.title AS movie, count(DISTINCT p) AS actorCount
ORDER BY actorCount DESC
LIMIT 10
```

这里没有显式写 GROUP BY,因为 Cypher 会自动把 RETURN 中没有使用聚合函数的字段当作分组字段。所以上面的查询会按 m.title 分组,然后统计每组里有多少个不同的 p

p 是匹配到的 Person 节点变量,Neo4j 判断两个 p 是否相同,不是看 name 是否一样,而是看它们是不是同一个节点。这里使用 count(DISTINCT p) 是为了避免同一个演员和同一部电影之间如果存在重复 ACTED_IN 关系时被重复计数。

WITH 关键字:传递中间结果

WITH 是 Cypher 中非常重要的一个关键字,它的作用类似于 SQL 中的子查询,用来把前一段查询的结果传递到下一段查询中继续处理。可以把它理解为”管道符 |”——把上一步的结果交给下一步使用。

```
-- 通用语法:
MATCH 模式
WITH 变量1, 变量2, 聚合函数(...) AS 别名
WHERE 条件        -- WITH 后面可以再加 WHERE 过滤
MATCH 另一个模式  -- WITH 后面可以再 MATCH
RETURN ...
```

WITH 主要解决以下场景:

  1. 在 MATCH 之后做聚合,然后基于聚合结果继续查询
  2. 将上一步的结果重命名/过滤,再传给下一步
  3. 在 RETURN 之前做多步处理

举例:

```
-- 查询出演电影数超过3部的演员,并列出他们的电影
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WITH p, collect(m.title) AS movies, count(m) AS movieCount
WHERE movieCount > 3
RETURN p.name, movies, movieCount
```

执行过程:

  1. 第一个 MATCH 找到所有”人 → 出演 → 电影”的关系
  2. WITH 对每个人聚合,收集电影列表和数量
  3. WHERE 过滤出电影数 > 3 的人(注意:这个过滤是基于 WITH 的结果,不能在第一个 MATCH 后用 WHERE 直接做)
  4. RETURN 返回结果

没有 WITH 行不行?

不行。如果想”先聚合再过滤”,必须用 WITH 把聚合结果传递给后续的 WHERE。直接在 RETURN 后用 WHERE 是不允许的。

集合操作(IN 查询):

```
-- 通用
MATCH 模式
WHERE 变量.属性 IN [值1, 值2, ...]
RETURN ...

-- 举例:查询指定演员出演的电影
MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
WHERE p.name IN ['周星驰', '吴孟达', '元华']
RETURN p.name, collect(m.title) AS movies
```

数据操作 — 修改(类比 UPDATE)

通用语法:

```
MATCH (变量:标签 {条件})
SET 变量.属性名 = 新值
RETURN 变量
```

举例:

```
MATCH (m:Movie {title: '功夫'})
SET m.genre = '动作喜剧'
RETURN m
```

数据操作 — 删除(类比 DELETE)

通用语法:

```
-- 删除节点(节点没有关系时才能删除)
MATCH (变量:标签 {条件})
DELETE 变量

-- 删除节点及其所有关系(强制删除)
MATCH (变量:标签 {条件})
DETACH DELETE 变量

-- 删除关系
MATCH (a:标签A {条件})-[r:关系类型]->(b:标签B {条件})
DELETE r
```

举例:

```
-- 删除一个没有关系的节点
MATCH (p:Person {name: '测试人物'})
DELETE p

-- 删除节点及其所有关系
MATCH (p:Person {name: '测试人物'})
DETACH DELETE p

-- 只删除关系,保留节点
MATCH (p:Person {name: '周星驰'})-[r:DIRECTED]->(m:Movie {title: '功夫'})
DELETE r
```

3.3 理解项目中的 Cypher 查询

在我们的医疗分诊项目中,核心的知识图谱查询是”根据症状查找可能的疾病和对应科室”。在看具体查询语句之前,我们先来了解一下知识图谱中的数据结构。

项目知识图谱的数据结构

涉及的标签(3 个):

标签含义
Symptom症状(如”头痛”、”发热”、”咳嗽”)
Disease疾病(如”偏头痛”、”感冒”、”肺炎”)
Department科室(如”神经内科”、”呼吸内科”)

节点的属性:

节点类型属性说明
Symptomname症状名称
Diseasename疾病名称
Diseasedesc疾病描述
Departmentname科室名称

涉及的关系(2 个):

关系类型方向含义
has_symptomDisease → Symptom某疾病具有某症状
belongs_toDisease → Department某疾病属于某科室

数据示意图:

```
graph LR
    D1((偏头痛<br/>Disease)) -->|has_symptom| S1((头痛<br/>Symptom))
    D1 -->|has_symptom| S2((恶心<br/>Symptom))
    D1 -->|belongs_to| Dept1[神经内科<br/>Department]

    D2((感冒<br/>Disease)) -->|has_symptom| S3((发热<br/>Symptom))
    D2 -->|has_symptom| S4((咳嗽<br/>Symptom))
    D2 -->|belongs_to| Dept2[呼吸内科<br/>Department]
```

项目中的查询语句

下面是项目中实际使用的 Cypher 查询语句:

```
MATCH (d:Disease)-[:has_symptom]->(s:Symptom)
WHERE s.name IN $symptoms
WITH d, collect(DISTINCT s.name) AS matchedSymptoms
WITH d,
     matchedSymptoms,
     [x IN matchedSymptoms WHERE x IN $mainSymptoms] AS matchedMainSymptoms
WHERE size(matchedMainSymptoms) > 0
MATCH (d)-[:belongs_to]->(dept:Department)
RETURN d.name AS disease,
       d.desc AS diseaseDesc,
       dept.name AS department,
       size(matchedSymptoms) AS hit,
       matchedMainSymptoms
ORDER BY size(matchedMainSymptoms) DESC, hit DESC
LIMIT $limit
```

逐步解读:

  1. MATCH (s:Symptom)<-[:has_symptom]-(d:Disease) — 找到所有”疾病→症状”的关系
  2. WHERE s.name IN $symptoms — 只保留症状名称在用户症状列表中的
  3. WITH d, collect(DISTINCT s.name) AS matchedSymptoms — 用 WITH 传递中间结果:对每个疾病,收集它匹配到的症状列表
  4. [x IN matchedSymptoms WHERE x IN $mainSymptoms] AS matchedMainSymptoms — 从匹配的症状中筛选出属于主症状的
  5. WHERE size(matchedMainSymptoms) > 0 — 只保留至少匹配了一个主症状的疾病
  6. MATCH (d)-[:belongs_to]->(dept:Department) — 查出疾病对应的科室
  7. ORDER BY ... DESC — 按主症状匹配数、总匹配数降序排列

简单说:输入一组症状,找出匹配这些症状的疾病,以及疾病对应的科室,按匹配程度排序。

Java 语言访问 Neo4j

整体思路

Neo4j 和 MySQL 一样,也是 C/S(客户端/服务端)架构:

  • 服务端:Neo4j 数据库进程,监听 bolt://localhost:7687 端口
  • 客户端:我们的 Java 应用程序,通过驱动连接到服务端

所以,就像访问 MySQL 需要引入 mysql-connector-java 驱动一样,访问 Neo4j 也需要引入对应的 Java 驱动。

在 Spring Boot 生态中,我们使用 Spring Data Neo4j 来访问 Neo4j,就像使用 MyBatis-Plus 来访问 MySQL 一样。Spring Data Neo4j 底层封装了 Neo4j Java Driver,提供了更简洁的 API。

引入依赖

在 pom.xml 中添加 Spring Data Neo4j 的 starter 依赖:

```
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-neo4j</artifactId>
</dependency>
```

类比 MySQL:

MySQLNeo4j
spring-boot-starter-data-jpa 或 mybatis-plusspring-boot-starter-data-neo4j
JDBC 协议Bolt 协议
端口 3306端口 7687
SQL 语言Cypher 语言

配置连接

在 application.yml 中配置 Neo4j 连接信息(Spring Boot 自动识别):

```
spring:
  neo4j:
    uri: bolt://192.168.150.109:7687 # neo4j服务器地址
    authentication:
      username: neo4j  # 用户名
      password: password123 #密码
  data:
    neo4j:
      database: neo4j #访问的数据库名称
```

Spring Boot 会根据这些配置自动创建 Neo4jClient Bean,我们直接注入使用即可,不需要手动创建连接对象。

使用 Neo4jClient 执行查询

Spring Data Neo4j 提供了 Neo4jClient,它是执行 Cypher 查询的核心工具。

类比 MySQL 的访问方式:

MySQL (MyBatis-Plus)Neo4j (Spring Data Neo4j)
DataSource(自动配置)Driver(自动配置)
SqlSessionNeo4jClient
mapper.selectList(wrapper)neo4jClient.query(cypher).fetch().all()
@Param 传参.bind(value).to(“param”) 或 .bindAll(map)

基本查询示例:

```
@Service
public class MovieNeo4jService {

    private final Neo4jClient neo4jClient;

    public MovieNeo4jService(Neo4jClient neo4jClient) {
        this.neo4jClient = neo4jClient;
    }

    // 查询所有电影名称
    public List<String> listAllMovies() {
        String cypher = """
            MATCH (m:Movie)
            RETURN m.title AS movie
            ORDER BY m.released DESC
            """;

        List<String> movies = new ArrayList<>();
        for (Map<String, Object> record : neo4jClient.query(cypher).fetch().all()) {
            movies.add(record.get("movie").toString());
        }
        return movies;
    }
}
```

带参数的查询示例:

```
// 方式一:绑定单个参数
public List<String> listActorsByMovie(String movieTitle) {
    String cypher = """
        MATCH (p:Person)-[:ACTED_IN]->(m:Movie {title: $movieTitle})
        RETURN p.name AS actor
        ORDER BY actor
        """;

    List<String> actors = new ArrayList<>();
    for (Map<String, Object> record : neo4jClient.query(cypher)
            .bind(movieTitle).to("movieTitle") // 绑定参数:$movieTitle = movieTitle变量的值
            .fetch().all()) {
        actors.add(record.get("actor").toString());
    }
    return actors;
}

// 方式二:绑定多个参数(使用 Map)
public List<String> searchMoviesByActors(List<String> actorNames, int limit) {
    String cypher = """
        MATCH (p:Person)-[:ACTED_IN]->(m:Movie)
        WHERE p.name IN $actorNames
        RETURN m.title AS movie,
               collect(DISTINCT p.name) AS matchedActors
        ORDER BY size(matchedActors) DESC
        LIMIT $limit
        """;

    Map<String, Object> params = Map.of(
        "actorNames", actorNames,
        "limit", limit
    );

    List<String> movies = new ArrayList<>();
    for (Map<String, Object> record : neo4jClient.query(cypher)
            .bindAll(params)                   // 一次绑定所有参数
            .fetch().all()) {
        movies.add(record.get("movie") + ":" + record.get("matchedActors"));
    }
    return movies;
}
```

注意几个关键点:

  1. Neo4jClient 由 Spring Boot 自动创建并管理,直接注入即可(不需要手动管理连接和会话)
  2. Cypher 中的参数使用 $参数名 的形式(类比 MyBatis 的 #{参数名}
  3. 绑定参数有两种方式:.bind(value).to("name") 绑定单个,.bindAll(map) 绑定多个
  4. .fetch().all() 返回 Collection>,每个 Map 就是一行结果
  5. 也可以用 .fetchAs(Type.class).mappedBy(...) 做类型映射

项目中的实际用法

在我们的项目中,Neo4j 的访问封装在 Neo4jKgStore 类中。它的核心方法是根据症状查询疾病证据:

```
@Service
public class Neo4jKgStore implements KgStore {

    private final Neo4jClient neo4jClient;

    public Neo4jKgStore(Neo4jClient neo4jClient) {
        this.neo4jClient = neo4jClient;
    }

    // 核心查询方法:根据症状列表查询匹配的疾病和科室
    public List<DiseaseEvidenceResult> searchDiseaseEvidenceBySymptoms(
            List<String> symptoms, List<String> mainSymptoms, int limit) {

        Map<String, Object> params = Map.of(
            "symptoms", symptoms,
            "mainSymptoms", mainSymptoms,
            "limit", limit
        );

        List<DiseaseEvidenceResult> rows = new ArrayList<>();
        for (Map<String, Object> record : neo4jClient.query(DISEASE_EVIDENCE_QUERY)
                .bindAll(params)
                .fetch().all()) {
            rows.add(new DiseaseEvidenceResult(
                record.get("disease").toString(),
                record.get("diseaseDesc").toString(),
                record.get("department").toString(),
                ((Number) record.get("hit")).longValue(),
                (List<String>) record.get("matchedMainSymptoms")
            ));
        }
        return rows;
    }
}
```

整体流程就是:

  1. 从 Spring 容器中注入 Neo4jClient(Spring Boot 自动配置好的)
  2. 调用 neo4jClient.query(cypher) 指定要执行的 Cypher 语句
  3. 通过 .bindAll(params) 绑定查询参数
  4. 调用 .fetch().all() 执行查询并获取所有结果
  5. 遍历结果集(Map),按列名取值,封装为业务对象返回

评论

  1. 4 周前
    2026-7-15 11:54:19

    毕设做的就是图谱生成这方面的,感觉图数据库还有一个优点就是展示起来比较好看。大量的数据图谱化展示时就像一个小型天体图😏

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇