mcpField
N
neo4j-modeling-skill

neo4j-modeling-skill

github.com/neo4j-contribデータ・データベース1181.2k 回インストール1 週間前に更新
リポジトリ

neo4j-modeling-skill について

neo4j-modeling-skillは、Neo4jの新規グラフモデル設計や既存スキーマのレビュー、移行を支援します。ノードとリレーションシップの選択、制約・インデックス、スーパーノード対策を扱います。

主な機能

  1. 01ユースケースからモデルを設計
  2. 02ノード・リレーションシップ・プロパティの選択
  3. 03リレーショナルスキーマをグラフへ移行
  4. 04スーパーノードの検出と軽減
  5. 05制約とインデックスの計画

活用シーン

  1. 01新しい領域のNeo4jスキーマ設計
  2. 02既存モデルのアンチパターン確認
  3. 03リレーショナルまたはドキュメントスキーマの移行
  4. 04高ファンアウトノードと性能リスクへの対処

MCP 対応

READMEでは、get-schemaによるスキーマ確認、read-cypherによる制約の確認、write-cypherによる制約作成をMCPツールに対応付けています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/neo4j-contrib/neo4j-skills --skill neo4j-modeling-skill

インストール後、Claude Code で/neo4j-modeling-skillを入力して読み込まれたか確認します。

同梱ファイルREADME.md

詳細

使用する場面

  • グラフモデルをゼロから設計する場合(ドメイン → ノード、リレーションシップ、プロパティ)
  • 既存モデルのアンチパターンを確認する場合
  • ノード、プロパティ、リレーションシップ、ラベルのどれを使うか判断する場合
  • リレーショナルスキーマまたはドキュメントスキーマをグラフに移行する場合
  • n項関係や複雑なリレーションシップの中間ノードを設計する場合
  • スーパーノードや高ファンアウトの問題を検出し、軽減する場合
  • モデル用の制約とインデックスを選択して作成する場合

使用しない場面

  • 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 INDEXESread-cypher
CREATE CONSTRAINT ... IF NOT EXISTSwrite-cypher(最初に表示して確認を得る)

デフォルト設定 — すべてのモデルに適用

  1. まずユースケースを考える — 設計前に、モデルで回答すべきクエリを5つ以上列挙します
  2. ノード = アイデンティティを持つエンティティ(名詞)、リレーションシップ = 方向性を持つつながり(動詞)
  3. ラベルはパスカルケース、リレーションシップタイプはスクリーム・スネーク・ケース、プロパティはキャメルケースにします
  4. MERGE で使用するすべてのノードタイプについて、キー・プロパティに一意性制約を設定します
  5. 型が分かっている場合は、プロパティ型制約(REQUIRE n.prop IS :: STRING)を追加します。これにより、不正な書き込みを早期に検出できます。Enterprise のみ。Community では一意性制約を使用し、アプリで型を検証します
  6. 汎用ラベル(:Entity、:Node、:Thing)や、汎用リレーションシップタイプ(:RELATED_TO、:HAS)は使用しません
  7. 行レベルのアクセス制御に使用するセキュリティラベルは、アプリケーションコードがドメインスキーマから確実に除外できるよう、共通のプレフィックス(例:Sec)で始めます
  8. リレーションシップの方向は任意ではなく、意味を表します
  9. 既存のデータベースに変更を提案する前に、スキーマを確認します
  10. すべての制約/インデックスの DDL には IF NOT EXISTS を使用し、再実行しても安全にします
  11. Neo4j 2026.06 以降(Enterprise/Aura、GA)の場合: 個別の CREATE CONSTRAINT 文ではなく、ALTER CURRENT GRAPH TYPE SET { … } でモデル全体を1つのブロックとして宣言し、ALTER CURRENT GRAPH TYPE ADD { … } で拡張します。詳細は neo4j-cypher-skill(GRAPH TYPE リファレンス)を参照してください。Community では使用できません(ALTER CURRENT GRAPH TYPE SET → 「community edition ではサポートされていません」)。個別の制約を使用してください。2026.02~2026.05 では構文はプレビュー版です。

主なパターン

ノード、リレーションシップ、プロパティのどれを選ぶか — 判断表
質問回答モデル化
アイデンティティを持ち、エントリーポイントとしてクエリされるものですか?はいノード
方向性を持ち、2つのものをつなぐ関係ですか?はいリレーションシップ
そのつながりに独自のプロパティがある、または複数の接続先がありますか?はい中間ノード
常に親とともに返され、それ単独では絞り込みに使わないスカラー値ですか?はい親のプロパティ
型による絞り込みやパストラバーサルに使うカテゴリですか?はいラベル(プロパティではない)
同じ属性値が多くのノードで繰り返されますか(カーディナリティが低い)?はいラベル(プロパティノードではない)
3つ以上のエンティティをつなぐ事実ですか?はい中間ノード
プロパティとラベルの使い分け — 判断表
ラベルを使う場合プロパティを使う場合
値の種類が少なく固定されており、トラバーサルのフィルターとして使用する場合(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 つを超えるプロパティがある
  • リレーションシップ自体が別のクエリの対象となる
  • 複数のエンティティが同じ接続コンテキストを共有する
  • 1 つの事実で 2 を超えるエンティティを接続する必要がある

リレーショナルからグラフへの移行表
リレーショナルの構成要素グラフでの同等要素備考
テーブルの行ノードテーブルごとに 1 つのラベル(必要に応じて追加)
列(スカラー)ノードプロパティ
主キー一意性制約のプロパティ単なる id より tmdbId/personId を推奨
外部キーリレーションシップ方向:依存元 → 参照先
多対多の中間テーブル中間ノード特に、中間テーブルに独自の列がある場合
中間テーブル(独自の列なし)直接のリレーションシップよりシンプルです。後から中間ノードにアップグレードできます
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

ラベル内の次数が中央値を大きく上回るノードは、スーパーノードの候補です。10 万を超えるリレーションシップを持つノードがあると、そのノードを通過するトラバーサルクエリの性能が低下します。

原因:

  • ドメイン上のスーパーノード:空港、有名人、人気のハッシュタグなど — 避けられません
  • モデリングによるスーパーノード:性別、国、ステータスを数百万のエッジを持つノードとしてモデル化したもの — 回避できます

対処方法(優先順位順):

方法使用する状況実装
クエリの方向方向に非対称性がある次数の低い側からクエリし、方向性を活用します
リレーションシップタイプの分割スーパーノードが複数の役割を担う1 つの :RELATED_TO の代わりに :FOLLOWS と :FAN を使います
ラベルによる分類スーパーノードに異なるエンティティタイプが混在している:Celebrity と :User を分け、関連するサブタイプだけをクエリします
バケットパターン時系列または大量のイベントノード以下を参照してください
モデル化を避けるカーディナリティの低いカテゴリ値ノード((:Status {name:"Active"}))ではなくラベルを使います(: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
プロパティキーcamelCasefirstName, createdAtFirstName, first_name
制約名説明的な snake_caseperson_id_uniqueconstraint1
インデックス名説明的な snake_caseperson_name_idxindex2

スキーマの適用 — 各要素に作成するもの

すべての 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 トークン、オーバーラップは 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)を使用しない
  • リレーションシップの方向に意味を持たせる
  • 多項リレーションシップまたはプロパティを持つリレーションシップには、中間ノードを使用する
  • カーディナリティの高い値はノードではなくプロパティとして保存する
  • カーディナリティの低いカテゴリ値はプロパティノードではなくラベルとして使用する
  • 埋め込みはビジネスノードではなく、専用の :Chunk ノードに格納する
  • スーパーノードの候補を特定し、対策を講じる
  • すべての DDL で IF NOT EXISTS を使用する
  • インデックスがオンラインになるまでポーリングしてから使用する
  • 評価結果は上記の構造化された形式に従う
  • 禁止事項すべてに具体的な修正方法を添える

参考資料

必要に応じて読み込んでください:

コメント