2. Cypher基础:CREATE、MATCH、RETURN等基本操作

好了,开始上手敲代码了。Cypher是Neo4j的查询语言,专门用来和图数据打交道。你学过SQL的话会发现它有些影子,但它更直观——因为咱们操作的是节点和关系,不是二维表。

我个人觉得,Cypher的语法设计得挺漂亮。它用一对圆括号表示节点,用方括号和箭头表示关系。读起来像在画图,而不是在写代码。不信你试试:

(n:Protein) -[:BINDS_TO]-> (m:Compound)

这行代码是什么意思?很简单:一个蛋白节点和一个化合物节点,它们之间存在一个“结合”关系。嗯,就是字面意思。

2.1 创建节点:CREATE

在药物靶点知识图谱里,节点代表实体。最常见的就是靶点、药物和疾病。我第一次建图的时候,就只建了这三种。

我们来创建一个简单的药物靶点节点:

CREATE (t:Target {name: 'EGFR', uniprot_id: 'P00533', type: 'kinase'})

这条命令干了三件事:

  • 创建了一个标签为 Target 的节点
  • 给它加了三个属性:name、uniprot_id 和 type
  • 把这个节点赋给了变量 t

标签就是节点的“类型”。在图数据库里,标签用来做分类。我习惯把标签名首字母大写,这样一眼就能看出它是标签。

属性就是节点的数据。用花括号包起来,键值对形式。属性类型可以是字符串、数字、布尔值、列表,甚至是嵌套的Map——不过我不建议你在图里存太复杂的东西,扁平化就好。

小技巧:创建节点时,最好把唯一标识(比如DrugBank ID、UniProt ID)作为属性存下来。这样后续匹配、去重、更新都用得上。

2.2 创建关系:CRETAE + 箭头

图的核心是关系。在Neo4j里,关系必须连接两个节点,不能独立存在。关系也有方向,有类型,也能带属性。

先创建两个节点,再连上关系——这是最基础的流程:

CREATE (d:Drug {name: 'Gefitinib', drugbank_id: 'DB00317'})
CREATE (t:Target {name: 'EGFR', uniprot_id: 'P00533'})
CREATE (d)-[:INHIBITS {ic50: 0.033, unit: 'µM'}]->(t)

你可能注意到了,中间那行箭头是 -[:INHIBITS]->。它代表:从药物指向靶点,关系类型是 INHIBITS(抑制)。方向很重要,别搞反了。

我在项目中遇到过两次箭头方向反了导致查询结果为空的情况,排查了半小时才发现。后来我养成一个习惯:写关系之前,先在心里默念一遍主语、谓语、宾语。

你也可以用一条 CREATE 语句把节点和关系一次性建好:

CREATE (d:Drug {name: 'Gefitinib'})-[:INHIBITS {ic50: 0.033}]->(t:Target {name: 'EGFR'})

这样更紧凑。但如果你要创建的节点后续还要被其他关系引用,建议分开写——方便重赋值。

2.3 查询节点:MATCH

建完数据,接下来就要查了。MATCH 是Cypher里最常用的关键词。它的作用是从图里“匹配”出符合模式的节点或关系。

最简单的查询——查所有靶点:

MATCH (t:Target)
RETURN t

查某个具体的靶点:

MATCH (t:Target {name: 'EGFR'})
RETURN t

查某个靶点相关的所有药物:

MATCH (d:Drug)-[:INHIBITS]->(t:Target {name: 'EGFR'})
RETURN d

这里 (d:Drug)-[:INHIBITS]->(t:Target {name: 'EGFR'}) 就是一个模式。Cypher会遍历图数据,找到所有符合这个模式的子图,然后把匹配到的变量返回给你。

踩坑提醒:没有索引的情况下,MATCH会对标签下所有节点做全表扫描。数据量一上来,性能会崩。我曾经在100万节点上跑了一个不带索引的MATCH,等了3分钟没出结果……记得上线前建好索引。

2.4 返回数据:RETURN

RETURN 决定查到的数据怎么展示。你可以返回整个节点,也可以只返回它的某个属性:

MATCH (t:Target {name: 'EGFR'})
RETURN t.name, t.uniprot_id

可以给返回的列取别名:

RETURN t.name AS target_name, t.uniprot_id AS protein_id

可以返回数量统计:

MATCH (d:Drug)-[:INHIBITS]->(t:Target)
RETURN t.name, count(d) AS drug_count
ORDER BY drug_count DESC

这条查询会返回每个靶点被多少种药物抑制,然后按数量降序排列。看起来就像在写SQL对吧?

我平时写数据导出脚本时,经常用 RETURN ... LIMIT 100 先预览一下结果,确认没问题再放开限制。

2.5 组合操作:用模式查关系

你想想看,要是只用节点和关系就能描述清楚一个药物-靶点-疾病通路,那该多方便。Cypher干的就是这事。

我们建一个简单的通路示例:

CREATE (d:Drug {name: 'Imatinib'})
CREATE (t:Target {name: 'BCR-ABL'})
CREATE (dis:Disease {name: 'Chronic Myeloid Leukemia'})
CREATE (d)-[:TARGETS]->(t)
CREATE (t)-[:ASSOCIATED_WITH]->(dis)

然后查“哪些药物通过哪个靶点作用于哪种疾病”:

MATCH (d:Drug)-[:TARGETS]->(t:Target)-[:ASSOCIATED_WITH]->(dis:Disease)
RETURN d.name, t.name, dis.name

这就是图查询的威力——多跳关系一行写完。你要是用SQL写这个,得多张表JOIN,还得处理中间表的映射。Cypher里只有模式匹配。

2.6 避坑指南

  • 重复创建:CREATE 不做去重检查。同样的节点重复执行两条 CREATE,会生成两个一模一样的节点。我记得第一次建药物库时,跑了两次脚本,结果里多了2000个重复节点……后来我改用 MERGE(下一章讲)。
  • 方向搞反:关系箭头方向不匹配时,MATCH可能什么都查不到。特别注意 -[:A]->(b) 和 -(:A)<-(b) 不是一回事。
  • 忘记标签:MATCH (n) 不带标签,它会匹配所有节点。如果数据库里有几百万节点……你会后悔的。
  • 属性名打错:Neo4j不会校验属性名。你写 RETURN t.uniprot_id 但属性名实际是 uniprotId,结果就是 null。我建议在开发初期用 RETURN keys(t) 看一下节点属性列表。

2.7 本章知识体系

下面这张图把Cypher基本操作串联起来了。看着它,你就知道Cypher的骨架是什么样的:

Cypher 核心操作体系 CREATE(创建) MATCH(查询) RETURN(返回) 创建节点(标签 + 属性) 创建关系(方向 + 类型 + 属性) 一次性建多个结构 按标签匹配节点 按属性条件筛选 模式匹配(多跳关系) 返回节点/属性 别名(AS)、去重(DISTINCT) 聚合(count、sum、order by) 药物靶点场景:CREATE 靶点/药物 → MATCH 通路 → RETURN 药物列表 箭头方向代表数据流动:CREATE 构建图 → MATCH 查找模式 → RETURN 返回结果

图里你可以看到:CREATE、MATCH、RETURN 是三个独立但又紧密衔接的操作。实际开发中,它们经常组合使用。比如先MATCH到已有节点,再CREATE新的关系;或者先CREATE节点,再RETURN确认。

嗯,到这里,Cypher基础操作这部分就算讲透了。我已经尽可能把容易踩的坑、常用的技巧都提到了。你照着写几条命令试试,跑通了就说明基本掌握了。