← ブログに戻る

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つに収束しました。

腐ったノード113件の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ヶ月の答えはそれくらいです。


参考リンク: