3、节点与关系属性:设置属性、多属性、索引
这一节咱们聊聊属性的那些事儿。说白了,属性就是给节点和关系贴上的标签——药物叫什么名字?靶点是什么类型?这些信息都需要用属性存起来。我刚开始用Neo4j那会儿,总觉得属性就是个“附带信息”,好家伙,后来发现属性设计得好不好,直接关系到查询快不快、数据好不好管。
3.1 节点属性的基本操作
创建节点时带上属性,语法很直观。你看这个例子:
CREATE (d:Drug {
name: "伊马替尼",
smiles: "CC1=C(C=C(C=C1)NC(=O)C2=CC=C(C=C2)CN3CCN(CC3)C)NC4=NC5=CC=CC=C5N4",
mol_weight: 493.6,
logP: 3.2,
phase: "上市"
})
RETURN d
这里我创建了一个药物节点,塞了5个属性进去。name是字符串,mol_weight是浮点数,phase又是字符串。Neo4j不强制你定义属性的类型,它会自动推断——这既是方便也是坑,后面我会说。
我个人习惯用小写字母加下划线,比如
uniprot_id、gene_symbol。千万别混用驼峰和下划线,查起来容易忘。
3.2 多属性的批量写入
在实际项目中,我常常需要一次写入几十个属性。比如从ChEMBL导入数据时,一个化合物可能有几十个计算性质。一条条写太慢了,用Map传参就方便很多:
WITH {
chembl_id: "CHEMBL941",
name: "厄洛替尼",
target_count: 12,
bioactivity: "IC50=0.02 nM",
synonyms: ["Tarceva", "OSI-774"],
therapeutic_area: "非小细胞肺癌"
} AS props
CREATE (d:Drug)
SET d = props
RETURN d
SET d = props这个写法,会把Map里所有的键值对一口气赋给节点。注意,这会替换节点上已有的全部属性。如果你只想增加或更新某几个属性,用SET d += props。
| 操作符 | 行为 | 适用场景 |
|---|---|---|
SET n = {map} |
替换节点所有属性 | 从外部数据源全量同步 |
SET n += {map} |
合并属性(新增或覆盖) | 增量更新,保留已有字段 |
SET n.prop = value |
单独设置一个属性 | 修改个别字段 |
为什么会推荐用+=?我在项目中吃过亏。有一次更新药物数据时用了=,结果把一个药物的synonyms列表给覆盖了,丢了老数据。嗯,从那以后我只要做增量更新,一定用+=。
3.3 复合属性与多层结构
有些药物属性是复合的,比如“多个靶点的活性数据”、“不同物种的IC50值”。你完全可以用列表和嵌套Map来存:
CREATE (d:Drug {
name: "阿法替尼",
activities: [
{target: "EGFR", ic50: 0.5, species: "human"},
{target: "HER2", ic50: 1.2, species: "human"},
{target: "EGFR", ic50: 3.8, species: "mouse"}
],
clinical_trials: {
phase1: "NCT00123456",
phase2: "NCT00765432",
phase3: "NCT01123456"
}
})
你想想看,这样存的好处是什么?一次查询就能拿到药物的全部活性数据,不用再走关联关系。当然,如果活性数据量特别大(几百条以上),我就建议把活性数据提成独立的节点,用:HAS_ACTIVITY关系连接,避免节点过于臃肿。
复合属性的查询要用
UNWIND 或者 [] 列表操作符,性能不如关系查询。所以只有当数据规模小(几十条以内)、且访问模式固定的情况下我才用复合属性。
3.4 关系属性的设置
关系也可以带属性。比如药物和靶点的:TARGETS关系上,我会存置信度、数据来源、文献引用这些信息:
CREATE (d:Drug {name: "吉非替尼"})
CREATE (t:Target {name: "EGFR"})
CREATE (d)-[r:TARGETS {
confidence: 0.95,
source: "DrugBank",
pmid: "21995302",
assay_type: "kinase inhibition"
}]->(t)
RETURN r
关系属性的查询也很直接。比如找所有高分置信度的药物-靶点关系:
MATCH (d:Drug)-[r:TARGETS]->(t:Target)
WHERE r.confidence > 0.9
RETURN d.name, t.name, r.confidence
我个人习惯把那些**变化频率高、多对多特有**的信息放在关系属性上。比如置信度、结合常数、实验条件这些,放在关系上是最合适的。
3.5 属性的类型与约束
Neo4j支持的类型有:String、Integer、Float、Boolean、List、Map、Date、DateTime等。但是注意,它不会自动校验类型一致性。比如同一个属性名“phase”,有的节点存"上市",有的存3,这不报错但查起来很坑。
我曾经在一个项目中,药物的
phase属性混用了字符串和整数,结果 WHERE n.phase > 2 这种查询直接忽略了字符串类型的节点,导致统计结果差了30%。所以我建议做数据导入时统一好属性类型,尽量别混用。
3.6 索引的创建与使用
属性设好了,查询慢怎么办?上索引。索引说白了就是给Neo4j一个“快速查找”的目录。比如药物名称经常作为查询起点,那就给name加索引:
CREATE INDEX drug_name_index FOR (d:Drug) ON (d.name)
同样,靶点的uniprot_id也常被用来快速定位:
CREATE INDEX target_uniprot_index FOR (t:Target) ON (t.uniprot_id)
建好后,如果用MATCH (d:Drug {name: "伊马替尼"})查询,Neo4j会自动使用索引。你可以在查询前加PROFILE关键字验证是否走了索引:
PROFILE MATCH (d:Drug {name: "伊马替尼"}) RETURN d
输出中如果看到NodeIndexSeek,说明索引生效了。如果看到NodeByLabelScan,那就是全表扫描。
| 索引类型 | 语法 | 适用场景 |
|---|---|---|
| 单属性索引 | CREATE INDEX name FOR (n:Label) ON (n.prop) |
等值查询、范围查询 |
| 复合索引 | CREATE INDEX name FOR (n:Label) ON (n.prop1, n.prop2) |
多字段组合查询 |
| 全文索引 | CREATE FULLTEXT INDEX name FOR (n:Label) ON EACH [n.prop] |
模糊匹配、文本搜索 |
复合索引特别适合药物靶点图谱中“根据靶点类型+物种”来找节点的场景:
CREATE INDEX target_type_species FOR (t:Target) ON (t.type, t.species)
MATCH (t:Target {type: "kinase", species: "human"})
RETURN t LIMIT 10
全文索引适合药物名称的模糊搜索。比如用户输入“伊马”,你想把“伊马替尼”、“伊马曲班”都匹配出来:
CREATE FULLTEXT INDEX drug_name_fulltext FOR (d:Drug) ON EACH [d.name, d.synonyms]
CALL db.index.fulltext.queryNodes("drug_name_fulltext", "伊马")
YIELD node, score
RETURN node.name, score
全文索引查询要用
db.index.fulltext.queryNodes 这个存储过程,不是 MATCH。
3.7 唯一性约束与模式索引
有些属性必须是唯一的,比如药物的chembl_id、靶点的uniprot_id。Neo4j用CREATE CONSTRAINT来实现:
CREATE CONSTRAINT unique_drug_chembl FOR (d:Drug) REQUIRE d.chembl_id IS UNIQUE
CREATE CONSTRAINT unique_target_uniprot FOR (t:Target) REQUIRE t.uniprot_id IS UNIQUE
加了约束之后,如果试图插入重复的chembl_id,Neo4j会直接报错。这比在应用层做检查可靠得多。而且——这个约束本身也会创建一个唯一索引,查询性能也是杠杠的。
每次建新图谱项目,我第一件事就是把那些业务上必须唯一的字段加上约束。比如药物的ChEMBL ID、靶点的UniProt ID。这能避免很多数据重复的糟心事。
3.8 本章SVG结构图
下面这张图把本章的知识点串起来,方便你直观理解属性、多属性、索引之间的关系:
3.9 实践中的权衡与建议
最后说几点我在项目中的实际体会。属性设计这件事,没有绝对的对错,关键看你的查询场景。
- 先想查询,再定属性:如果经常用
WHERE d.logP > 3这种范围查询,记得在logP上建索引。如果只是偶尔用,那没必要建。 - 别把所有东西都塞进一个节点:药物有几十个靶点活性数据时,我建议拆成独立的
:Activity节点,用关系连。不然节点属性列表越来越多,维护起来就头疼了。 - 复合索引的顺序很重要:
ON (type, species)和ON (species, type)在查询时完全不一样。经常用的查询条件放在第一位。
每个索引都会增加写入数据的开销。曾有一个项目,我把所有属性都加了索引,结果数据导入速度从每分钟10万条掉到了2万条。嗯,从那以后我只给那些真正需要快速查找的属性建索引。