关于 neo4j-modeling-skill
neo4j-modeling-skill 为 Neo4j 图数据建模提供指导,适用于从零设计模型或审查、迁移现有模式。内容涵盖实体与关系选择、约束索引以及超节点处理。
主要功能
- 01从用例推导图模型
- 02区分节点、关系与属性
- 03迁移关系型模式到图
- 04检测并缓解超节点
- 05规划约束与索引
适用场景
- 01为新领域设计 Neo4j 图模式
- 02审查现有模式中的反模式
- 03将关系型或文档模式迁移到图
- 04处理高扇出节点及其性能风险
MCP 相关
README 提及 MCP 工具映射,可用 get-schema 检查模式,并通过 read-cypher 和 write-cypher 读取或创建约束。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ neo4j-contrib/ neo4j-skills --skill neo4j-modeling-skill安装后在 Claude Code 中输入/neo4j-modeling-skill确认已加载。
附带文件README.md
详细介绍
适用场景
- 从头设计图模型(领域 → 节点、关系、属性)
- 检查现有模型是否存在反模式
- 决定使用节点、属性、关系还是标签
- 将关系型或文档型架构迁移到图模型
- 为多元或复杂关系设计中间节点
- 检测并缓解超级节点/高扇出问题
- 为模型选择并创建约束和索引
不适用场景
- 编写或优化 Cypher →
neo4j-cypher-skill - Spring Data Neo4j(@Node、@Relationship) →
neo4j-spring-data-skill - GraphQL 类型定义 →
neo4j-graphql-skill - 导入数据(LOAD CSV、APOC import) →
neo4j-import-skill
设计前先检查
对于现有数据库,先运行以下命令——切勿在不了解当前状态的情况下提出更改:
CALL db.schema.visualization() YIELD nodes, relationships RETURN nodes, relationships;
SHOW CONSTRAINTS YIELD name, type, labelsOrTypes, properties RETURN name, type, labelsOrTypes, properties;
SHOW INDEXES YIELD name, type, labelsOrTypes, state WHERE state = 'ONLINE' RETURN name, type, labelsOrTypes;
如果有 APOC:
CALL apoc.meta.schema() YIELD value RETURN value;
MCP 工具对应关系:
| 操作 | 工具 |
|---|---|
| 检查架构 | get-schema |
SHOW CONSTRAINTS、SHOW INDEXES | read-cypher |
CREATE CONSTRAINT ... IF NOT EXISTS | write-cypher(先展示并确认) |
默认规则——适用于所有模型
- 先明确用例——设计前列出模型必须回答的 5 个以上查询
- 节点 = 具有标识的实体(名词);关系 = 具有方向的连接(动词)
- 标签使用 PascalCase;关系类型使用 SCREAMING_SNAKE_CASE;属性使用 camelCase
- 每种用于 MERGE 的节点类型都必须在其键属性上设置唯一性约束
- 在类型已知时添加属性类型约束(
REQUIRE n.prop IS :: STRING)——及早发现错误写入。仅限 Enterprise;Community:使用唯一性约束,并在应用中验证类型 - 不使用通用标签(
:Entity、:Node、:Thing);不使用通用关系类型(:RELATED_TO、:HAS) - 安全标签(用于行级访问控制)应以通用前缀开头(例如
Sec),以便应用代码可靠地将其从领域架构中筛除 - 关系方向应编码语义含义,而非随意设定
- 对现有数据库提出任何更改前,先检查架构
- 所有约束/索引 DDL 都使用
IF NOT EXISTS——可安全地重复运行 - **在 Neo4j 2026.06+(Enterprise/Aura,正式发布)上:**使用一个代码块中的
ALTER CURRENT GRAPH TYPE SET { … }声明完整模型,并使用ALTER CURRENT GRAPH TYPE ADD { … }扩展模型,而非逐条使用CREATE CONSTRAINT语句——详情请参阅neo4j-cypher-skill(GRAPH TYPE 参考)。Community 不支持此功能(ALTER CURRENT GRAPH TYPE SET→ “not supported in community edition”):请使用单独的约束。在 2026.02–2026.05 中,该语法为预览版。
关键模式
节点、关系与属性——决策表
| 问题 | 答案 | 建模为 |
|---|---|---|
| 它是一个具有标识、可作为查询入口的事物吗? | 是 | 节点 |
| 它是两个事物之间具有方向的连接吗? | 是 | 关系 |
| 该连接是否具有自身属性或多个目标? | 是 | 中间节点 |
| 它是一个标量,总是与其父项一起返回、从不单独用于筛选吗? | 是 | 父项上的属性 |
| 它是用于按类型筛选或路径遍历的类别吗? | 是 | 标签(而非属性) |
| 同一属性值是否在许多节点中重复出现(低基数)? | 是 | 标签,而非属性节点 |
| 它是连接 2 个以上实体的事实吗? | 是 | 中间节点 |
属性与标签——决策表
| 在以下情况下使用标签 | 在以下情况下使用属性 |
|---|---|
值较少且固定,用作遍历筛选条件(WHERE n:Active) | 值很多、动态变化,或每个节点的值都唯一 |
按类型遍历(MATCH (n:VIPCustomer)) | 按值筛选(WHERE n.tier = 'vip') |
| 类别决定索引选择 | 精细粒度的值用于范围扫描 |
示例::Active、:Verified、:Premium | 示例:status、score、email |
规则:添加标签成本低;扫描所有 :Label 节点很快。切勿将高基数值建模为标签。
对于 status 这类低基数状态:如果它是稳定的类别,用于遍历/筛选,则使用标签(:Active);如果它经常变化或只随节点一起返回,则使用属性。切勿创建 (:Status) 节点。
中间节点模式
当关系需要自己的属性、连接超过 2 个实体,或需要独立查询时使用。
之前(带属性的关系——受限):
(Person)-[:ACTED_IN {role: "Neo"}]->(Movie)
// Cannot query roles independent of movies
之后(中间节点——可查询、可扩展):
(Person)-[:PLAYED]->(Role {name: "Neo"})-[:IN]->(Movie)
// MATCH (r:Role) WHERE r.name STARTS WITH 'Neo' RETURN r
雇佣关系重叠示例:
// Find colleagues who overlapped at same company
MATCH (p1:Person)-[:WORKED_AT]->(e1:Employment)-[:AT]->(c:Company)<-[:AT]-(e2:Employment)<-[:WORKED_AT]-(p2:Person)
WHERE elementId(p1) < elementId(p2)
AND e1.startDate <= coalesce(e2.endDate, date())
AND e2.startDate <= coalesce(e1.endDate, date())
RETURN DISTINCT p1.name, p2.name, c.name
在以下情况下,将关系提升为中间节点:
- 关系有超过 2 个属性
- 关系本身是另一个查询的主题
- 多个实体共享相同的连接上下文
- 需要在一个事实中连接超过 2 个实体
关系型 → 图迁移表
| 关系型构造 | 图中的等价项 | 备注 |
|---|---|---|
| 表行 | 节点 | 每个表使用一个标签(按需添加更多标签) |
| 列(标量) | 节点属性 | |
| 主键 | 唯一性约束属性 | 优先使用 tmdbId/personId,而非单独的 id |
| 外键 | 关系 | 方向:从依赖项 → 被引用项 |
| 多对多联结表 | 中间节点 | 尤其适用于联结表有自身列的情况 |
| 联结表(无自身列) | 直接关系 | 更简单;之后可升级为中间节点 |
| NULL 外键(可选关系) | 不存在的关系 | 不创建节点;缺失本身就是信号 |
| 多态外键(Rails 风格) | 多个标签或关系类型 | 拆分为特定类型的关系 |
| 自引用外键 | 同标签关系 | :Employee {managerId} → (e)-[:REPORTS_TO]->(m) |
| 审计/历史列 | 中间版本节点 | 版本模式见参考资料 |
超级节点检测与缓解
检测:
// Find top-10 highest-degree nodes
MATCH (n)
RETURN labels(n) AS labels, elementId(n) AS id, count{ (n)--() } AS degree
ORDER BY degree DESC LIMIT 10
度数远高于其标签中位数的节点 = 超级节点候选。任何关系超过 100K 的节点,都会降低经过它的遍历查询性能。
原因:
- 领域超级节点:机场、名人、热门话题标签——不可避免
- 建模导致的超级节点:将性别、国家、状态建模为拥有数百万条边的节点——可以避免
缓解策略(按优先级排序):
| 策略 | 使用场景 | 实现方式 |
|---|---|---|
| 查询方向 | 存在方向不对称性 | 从低度数一侧查询;利用方向性 |
| 关系类型拆分 | 超级节点承担多个角色 | 使用 :FOLLOWS + :FAN,而非单一的 :RELATED_TO |
| 标签隔离 | 超级节点混合了多种实体类型 | 使用 :Celebrity 与 :User 区分 → 仅查询相关子类型 |
| 分桶模式 | 时间序列或高容量事件节点 | 见下文 |
| 避免建模 | 低基数分类值 | 使用标签而非节点(:Active,而非 (:Status {name:"Active"})) |
| 连接提示 | 查询调优的最后手段 | 在 Cypher 中使用 USING JOIN ON n |
分桶模式(时间序列/高容量):
// Instead of: (:User)-[:VIEWED]->(:Page) (millions of rels per user)
// Bucket by hour:
(u:User)-[:VIEWED_IN]->(b:ViewBucket {userId: u.id, hour: '2025-04-28T14'})-[:VIEWED]->(p:Page)
// Query last hour's views without traversing full history:
MATCH (u:User {id: $uid})-[:VIEWED_IN]->(b:ViewBucket {hour: $hour})-[:VIEWED]->(p)
RETURN p.url
命名约定
| 元素 | 约定 | 正确示例 | 错误示例 |
|---|---|---|---|
| 节点标签 | PascalCase,单数名词 | :Person、:BlogPost | :person、:blog_posts、:Entity |
| 关系类型 | SCREAMING_SNAKE_CASE,动词短语 | :ACTED_IN、:WORKS_FOR | :actedin、:relatedTo、:HAS |
| 属性键 | camelCase | firstName、createdAt | FirstName、first_name |
| 约束名称 | 具有描述性的 snake_case | person_id_unique | constraint1 |
| 索引名称 | 具有描述性的 snake_case | person_name_idx | index2 |
模式约束 — 各元素应创建的内容
所有 DDL 都使用 IF NOT EXISTS。在导入数据之前应用。
// 1. Uniqueness constraint — every node type used in MERGE
CREATE CONSTRAINT person_id_unique IF NOT EXISTS
FOR (p:Person) REQUIRE p.id IS UNIQUE;
// 2. Existence constraint (Enterprise) — mandatory properties
CREATE CONSTRAINT person_name_exists IF NOT EXISTS
FOR (p:Person) REQUIRE p.name IS NOT NULL;
// 3. Property type constraint (Enterprise) — enforce data type
CREATE CONSTRAINT person_born_integer IF NOT EXISTS
FOR (p:Person) REQUIRE p.born IS :: INTEGER;
// 4. Key constraint (Enterprise) — unique + exists in one
CREATE CONSTRAINT movie_tmdbid_key IF NOT EXISTS
FOR (m:Movie) REQUIRE m.tmdbId IS NODE KEY;
// 4b. Community alternative to NODE KEY — composite uniqueness (existence not enforced)
CREATE CONSTRAINT movie_title_released_unique IF NOT EXISTS
FOR (m:Movie) REQUIRE (m.title, m.released) IS UNIQUE;
// 5. Range index — equality and range filters on properties
CREATE INDEX person_name_idx IF NOT EXISTS
FOR (p:Person) ON (p.name);
// 6. Fulltext index — ranked free-text search via `db.index.fulltext.queryNodes` / `SEARCH` (Lucene syntax, wildcards like `An*`); not used for CONTAINS / STARTS WITH predicates
CREATE FULLTEXT INDEX person_fulltext IF NOT EXISTS
FOR (n:Person) ON EACH [n.name, n.bio];
// 6b. Text index — serves CONTAINS / STARTS WITH / ENDS WITH (range index preferred for STARTS WITH when present)
CREATE TEXT INDEX person_bio_text IF NOT EXISTS
FOR (n:Person) ON (n.bio);
// 7. Vector index — embedding similarity search
CREATE VECTOR INDEX chunk_embedding_idx IF NOT EXISTS
FOR (c:Chunk) ON (c.embedding)
OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: 'cosine' } };
// 8. Relationship index — filter on rel properties
CREATE INDEX acted_in_year_idx IF NOT EXISTS
FOR ()-[r:ACTED_IN]-() ON (r.year);
创建索引后,轮询直到状态为 ONLINE:
SHOW INDEXES YIELD name, state WHERE state <> 'ONLINE' RETURN name, state;
状态为 ONLINE 之前,请勿使用索引。
向量/嵌入属性建模
对于长文本,优先使用专用的 :Chunk 节点;短文本的嵌入可以存放在实体上:
(:Document)-[:HAS_CHUNK]->(c:Chunk {text: "...", embedding: [...]})
规则:
- Chunk 节点:
text(源文本)、embedding(浮点数组)、chunkIndex(整数) - 父文档:仅存放元数据(title、url、createdAt)
- 仅在
c.embedding上创建向量索引 - 生产环境默认使用 200–500 个 token 的分块大小,并设置 20% 重叠 [field]
- 分块文档:不要将嵌入放在
:Document上——这会使节点过大,并污染遍历
反模式表
| 反模式 | 问题 | 修正方法 |
|---|---|---|
通用标签 :Entity、:Node | 无过滤优势;会扫描所有节点 | 使用领域标签 :Person、:Product |
通用关系类型 :RELATED_TO、:HAS | 无法按关系类型筛选 | 使用语义明确的类型 :PURCHASED、:AUTHORED |
| 将低基数值建模为节点 | 超级节点(:Status {name:"active"} → 数百万条边) | 改用标签 :Active |
将属性用作标签(同时存在 n.type = 'VIP' 和 :VIP 标签) | 不一致、重复 | 二选一;若用于遍历,优先使用标签 |
| 在业务节点上存储嵌入 | 节点膨胀,遍历缓慢 | 使用专用的 :Chunk 节点 |
| 使用 MERGE,但未设置唯一性约束 | 悄悄创建重复节点 | 在任何 MERGE 操作之前添加约束 |
| 未赋予关系方向明确含义 | 方向随意;模型令人困惑 | 方向应表示动作的语义流向 |
| 将连接表建模为裸属性 | 丢失历史记录,难以扩展 | 使用带有自身属性的中间节点 |
将 id 作为属性名称 | id(n) 是已弃用的 Cypher 函数(改用 elementId(n));在实际使用中,id 本身可以作为属性名称,但带领域限定的名称(personId、movieId)更清晰,也可避免未来产生歧义 | 在有助于提升可读性的情况下,优先使用 personId、movieId、tmdbId |
| 将所有日期存为字符串 | 无法进行范围查询;无法使用时间运算符 | 使用 Neo4j 的 date() 或 datetime() 类型 |
输出格式 — 模式评估
审查现有模型时:
## Schema Assessment
### Compliant
- [constraint / pattern that is correct]
### Issues Found
#### [Title] — Severity: ERROR / WARNING / INFO
- **Current**: what the model does
- **Problem**: why it is an issue
- **Fix**: specific Cypher DDL or model change
## Recommended Schema
### Node Labels
- :Label {key: TYPE, prop: TYPE, ...} → constraints: [list]
### Relationships
- (:LabelA)-[:TYPE {prop: TYPE}]->(:LabelB)
### Constraints to Create
[CREATE CONSTRAINT ... statements]
### Indexes to Create
[CREATE INDEX ... statements]
严重性含义:
| 严重性 | 含义 | 操作 |
|---|---|---|
ERROR | 模型正确性故障(可能产生重复数据,存在数据丢失风险) | 停止;继续之前先修复 |
WARNING | 性能或可扩展性风险 | 报告;继续之前询问用户 |
INFO | 风格或约定偏差 | 告知用户;继续 |
来源标签
[official]— Neo4j 文档中直接说明[derived]— 根据文档所述行为推导得出[field]— 社区经验法则;将其视为默认值,但应进行验证
检查清单
- 建模前定义用例(≥5 个查询)
- 提出变更前检查现有数据库中的架构
- 每个作为 MERGE 目标的节点标签都有唯一性约束
- 不使用通用标签(
:Entity、:Node、:Thing) - 不使用通用关系类型(
:RELATED_TO、:HAS、:CONNECTED_TO) - 关系方向体现语义
- 使用中间节点表示 N 元关系或带属性的关系
- 将高基数值存储为属性,而非节点
- 将低基数分类值用作标签,而非属性节点
- 将嵌入存储在专用的
:Chunk节点上,而非业务节点上 - 识别并缓解超级节点候选项
- 所有 DDL 均使用
IF NOT EXISTS - 使用索引前轮询,直至其状态为 ONLINE
- 评估输出遵循上述结构化格式
- 每项禁止事项都配有具体的修复方法
参考资料
按需加载:
评论
支持 Markdown