Knowledge Graphは3ヶ月で腐る — 300ノードで測ったrot率
半年前に自分で作ったKnowledge Graphを、先週、自分で読めませんでした。
正確に言うと、ノードは全部残っていました。300個ちゃんとNeo4jに座っています。Cypherを叩けばちゃんと返してきます。ただ、そのノードの4割は「あのとき何を考えて作ったか」を私が思い出せませんでした。
ラベルとタイトルとタグだけ残った、乾いた抜け殻でした。
そこで、腐り具合を数字で測ろうと決めました。この記事は300ノードのKGに対してrot率(rot rate)を定義し、Cypherで抽出し、直した3ヶ月分の実測記録です。
結論から先に書くと、90日後に生きていたノードは187個、rot率は37.7%でした。
rot率を、まず殴って定義する
「なんとなく古い」を計測しようとするたびに詰まるので、最初に殴って定義します。
rot率 = 過去90日間に一度も更新されず、他のノードから参照もされていないノードの割合
これを「腐ったノード(rotted node)」と呼びます。90日という数字は私の読み返しサイクルの中央値から取りました。3ヶ月見に行かなかったメモは、体感で「もう思い出せない」ラインです。読者のサイクルが違うなら、90を60や180に差し替えてください。
定義があいまいなまま計測は始められません。
「参照もされていない」はINCOMINGエッジの本数で見ます。他のノートから[[このノート]]と呼ばれていれば、たとえ本人が触っていなくても生きているノードとみなします。
誰も呼ばず、本人も更新していない ── これが腐り判定です。
Neo4j 5.xで腐ったノードを抽出する1本のCypher
私が使っているのは、Neo4j 5.x + Property Graphモデルです。ノードにはupdated(直近更新日、date()型)を必ず持たせています。以下のCypher1本で、腐ったノードのリストが返ります。
MATCH (n:Note)
WHERE n.updated < date() - duration({days: 90})
AND NOT EXISTS { MATCH (other)-[:LINKS_TO|MENTIONS]->(n) WHERE other <> n }
RETURN n.title AS title,
n.updated AS last_updated,
size([(n)-->() | 1]) AS outgoing_edges
ORDER BY n.updated ASC
LIMIT 100;
ポイントは3つです。
1つ目、duration({days: 90})で相対日付を計算する。ハードコードしたdate("2026-04-30")より、Cypherの側で計算した方が翌週も翌月もそのまま使えます。
2つ目、EXISTS { }のsubqueryパターンでincomingエッジの有無だけを見る。件数を数えると重いのでexistenceで済ませます。
3つ目、自己参照を除外するためにother <> nを入れる。自分で自分に張ったリンクは「参照されている」に数えません。
私は300ノードのKGに対してこれを叩いて、113ノードが返ってきました。300 - 113 = 187、生きているノードは62.3%。
逆に言うとrot率37.7%です。
3ヶ月分の実測: 何が腐ったか
腐ったノードを100件、手で目視分類しました。パターンは4つに収束しました。

| パターン | 件数 | 例 |
|---|---|---|
| 記事の下書きを転記して忘れた | 41 | 「Voice AIの300ms予算 (下書き)」 |
| 議事録・チャットログの断片 | 28 | 「10月のSprint振り返り」 |
| 読書メモが1行で終わっている | 24 | 「『実践Rust』 第3章 — あとで書く」 |
| 用語の暫定エントリで、誰にも参照されていない | 20 | 「AEO ← あとで定義」 |
面白いのは、下書き系(パターン1と3)が半数以上を占めていることでした。私は「下書きも将来のために貯めておこう」と思ってKGに突っ込んでいましたが、下書きは3ヶ月以内に本文化しないともう本文化しない、というのが今回のデータの結論です。
議事録系(パターン2)は別問題です。
これは腐っているのではなく、そもそも「参照される想定がない」ノードでした。Slackのログを機械的にKGに流し込んだ結果、他のノードとリンクを張らないまま座り続けた28個です。KGに入れるべきものと、入れなくていいものの線引きが甘かった、というのが本音です。
Notionに戻ろうか、と本気で思った瞬間
正直に書くと、rot率37.7%を見た瞬間、Notionに戻ろうかと本気で思いました。「Notionなら、こんなに腐らない気がする」と一瞬考えたのですが、これは錯覚です。
Notionでも腐ります。腐っても計測できないだけで、目の前から消えているだけです。
3ヶ月前、私は同じNeo4j KGを組んでNotionを解約した話を書きました。あのときの主張は「Property Graphは書き手の脳内に近い柔らかさを許す」でした。今もそう思います。
ただ、「柔らかさ」は「腐らなさ」を意味しません。柔らかいメモアプリほど、雑な下書きを大量に飲み込みます。ForceでもないSchemaでもない自由さは、腐敗の入り口でもあります。
Neo4jに残った利点は、腐ったことを数字で殴れる点です。Notionでは37.7%という数字を出す方法がありませんでした。腐りは同じでも、可視化できるか否かは天と地の差でした。
3種類の直し方 — 全部やる必要はない
腐った113ノードを、私は3種類に振り分けました。
削除(43件): 議事録の断片と、1行で終わった読書メモは容赦なく消しました。3ヶ月経ってもリンクが張られていない下書きは、未来の自分も救えません。「もったいない」で残すと、次のrot監査でまた同じノードが返ってきます。
復活(31件): 本文化する価値がある下書きは、その場で本文化しました。ここで一つルールを決めました。復活させるなら、その場で少なくとも1本、他のノードとリンクを張る。孤立した本文はまた腐ります。リンクが張られた瞬間、そのノードは「参照される側」になり、次の90日を生き延びます。
アーカイブ(39件): 削除するには惜しいが、本文化するほどでもない中間ノードは、:Archiveラベルを付けて別クラスタに追い出しました。rot監査の対象から外れます。「あとで読み返すかもしれないが、KGの回遊には要らない」という位置づけです。
削除:復活
= 4:3 くらいが、私にとっての現実解でした。全削除は感情が折れます。全復活は時間が終わります。全アーカイブは倉庫を作るだけです。KG運用の設計思想: 本にまとまっています
Property Graphの設計から運用フェーズの回し方まで、私が3ヶ月試行錯誤して掴んだことは、『実践Knowledge Graph入門』の運用パートに整理されています。今回のrot率の話は本の中では第14章の実装編にあたります。設計を先に読んで、こういう腐敗パターンを踏まないKGを最初から組む方が、あとで113ノードを分類するより圧倒的に速いです。私が先にこれを読んでいれば、下書きノードを最初から:Draftラベルで隔離していたと思います。
rot監査を仕組みにする
一度手で監査すると、次からは毎月やりたくなります。私はGitHub Actionsに月次cronを1つ足しました。上のCypher1本を叩いて、腐ったノードのタイトル一覧をSlackに投げるだけです。
name: kg-rot-audit
on:
schedule:
- cron: '0 0 1 * *' # 毎月1日 00:00 UTC
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Query rotted nodes
run: |
curl -X POST $NEO4J_HTTP_ENDPOINT \
-H "Authorization: Basic $NEO4J_AUTH" \
-H "Content-Type: application/json" \
-d '{"statements":[{"statement":"MATCH (n:Note) WHERE n.updated < date() - duration({days: 90}) AND NOT EXISTS { MATCH (o)-[:LINKS_TO|MENTIONS]->(n) WHERE o <> n } RETURN n.title LIMIT 100"}]}' \
| jq -r '.results[0].data[].row[0]' \
> /tmp/rotted.txt
- name: Notify Slack
run: |
COUNT=$(wc -l < /tmp/rotted.txt)
curl -X POST $SLACK_WEBHOOK \
-d "{\"text\":\"KG rot audit: ${COUNT} rotted nodes this month.\"}"
毎月1日に「今月の腐ったノード数」がSlackに落ちる。数字だけでいいです。詳細は自分でCypherを叩けば見えます。監視する数字を1つ持つ、これが3ヶ月試行錯誤して着地した個人KG運用の最小コアです。
先月の私は3ヶ月試行錯誤しましたと胸を張って書きましたが、それはつまり「3ヶ月放置して腐らせました」の言い換えです。
監視は仕組みで持つのが結局一番楽でした。
『実践Knowledge Graph入門』の運用章から補足
7つの実装章のあとに、こういう腐敗を前提とした運用章を用意しています。作る話は方々に転がっていますが、腐る話・測る話・直す話は書き手の実測がないと書けません。私自身がこの記事のCypherを叩いた実測から書き起こしています。
まとめ
- rot率 = 90日間更新されず、他ノードから参照もされないノードの割合と定義しました
- 300ノードのKGに対して、実測rot率は37.7% (113/300)でした
- 腐り方は4パターンに収束: 下書き放置41 / 議事録断片28 / 1行読書メモ24 / 暫定用語エントリ20
- 直し方も4:3:3で振り分け: 削除43 / 復活31 / アーカイブ39。全削除は感情が折れます
- rot監査は月次cron 1本 + Cypher 1本 + Slack通知1本で仕組み化しました
- KGは貯金じゃなく庭です。放っておくと雑草が生えます。3ヶ月に1度は草を抜くのが結局一番楽です
「腐らないKG」は無理でした。「腐ったことを数字で殴れるKG」は組めました。私の3ヶ月の答えはそれくらいです。
参考リンク:
- Neo4jで個人ナレッジグラフを組んだら、Notionを解約できた — この記事の「作る話」編
- Knowledge Graphを3週間で組み上げた7ステップ — 実装編
- Neo4j 5.x Cypher Manual: duration functions —
duration({days: N})の公式仕様
この記事は役に立ちましたか?