chiebukuro-mcp シリーズの続きです。
今回は自分の過去のブログ記事と SNS 投稿をぜんぶ SQLite に詰めて、chiebukuro-mcp から Claude に見せられるようにしました、という話。
なんで入れたのか
Claude に「あの頃なに考えてたっけ」を聞きたいときが、わりとあります。
個人的な記憶は頭の中にありますが、解像度が粗いです。手元に過去の自分の発言ログがあれば、Claude に日付範囲とキーワードを渡すだけで拾い上げてくれる。そう思って詰め始めました。
長期記憶の機構は別で動いていますが、あれは Claude Code のセッションを後追いで保存するもので、2023年以前にわたしが考えていたことは当然入っていません。過去のわたしを呼び出すなら、過去のわたしが書いた文章から引き出すしかないわけです。
何を詰めたか
はてなブログや、Xなど合計 60,194 件、2006年5月から2026年4月までの20年分、という感じになりました。 それぞれのエクスポートデータから取り込んでいます。
テーブル設計
ドカンとシンプルです。
CREATE TABLE blog_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, -- 'twitter:bash0C7' など date TEXT NOT NULL, -- ISO8601文字列 title TEXT, -- ブログのみ、SNSはNULL body TEXT NOT NULL ); CREATE INDEX idx_blog_entries_source_date ON blog_entries(source, date);
date を ISO8601 文字列にしているのは、Claude が書く SQL が WHERE date >= '2020-06' AND date < '2020-07' のようなシンプルな文字列比較で書けるからです。このほうが複合インデックスが効きやすくて、substr() や date() を挟まれるよりも速い、という理由です。
title は SNS で常に NULL にしてあるので「ブログ記事だけほしい」というときは WHERE title IS NOT NULL で分けられます。
embedding は chunk 刻みで
semantic search 用の embedding は、chiebukuro-mcp gem 側の Embedder クラスを使って一括生成しました。
60,000件一気に回すとメモリが不安だったので、chunk_size=100 で細かく分けて、LEFT JOIN で未処理のエントリだけを拾う形にしています。
loop do rows = db.execute(<<~SQL, chunk_size) SELECT e.id, e.title, e.body FROM blog_entries e LEFT JOIN blog_entries_vec v ON v.entry_id = e.id WHERE v.entry_id IS NULL ORDER BY e.id LIMIT ? SQL break if rows.empty? rows.each do |id, title, body| text = [title, body].compact.join("\n") blob = embedder.embed(text).pack("f*") db.execute("INSERT INTO blog_entries_vec(entry_id, embedding) VALUES (?, ?)", [id, blob]) end GC.start end
LEFT JOIN 方式なので、途中で止めてもリランすれば続きから再開されます。
Claude にヒントを渡す
chiebukuro-mcp は chiebukuro.json の description を Claude にそのまま見せる作りになっています。なので description に単なる説明ではなく、データの特性とクエリの書き方のヒントを詰め込んでいます。
いまの blog DB の description にはこういうことを書いています。
- 合計件数と期間
- ソース別の件数・期間・平均文字数・テーマ
- 高密度期 / 低密度期
- 日付フィルタの書き方(文字列比較でOK)
- 複合インデックスがあるので
substr()/date()は避けて、という注意 - 時期ごとのデータの偏り(発信の多い時期、そうじゃない時期)
最後のは地味に大事です。「その時期のわたしの投稿を見せて」と頼んだときに「データがありません」とだけ返ってくると、データが消えたのか当時黙っていたのかがわかりません。description にあらかじめそういう期間と書いておくと、Claude がその旨を添えて返してくれます。
今後のためにスキルを切った
この更新作業、これからも定期的にやることになります。手動ではありますが追加取り込みが発生します。
そのたびに「どの順番で何を実行するんだっけ」を思い出すのは面倒なので、Claude Code 用のスキルとして固定化しました。blog-db-update という名前で、現状把握 → ソース別インポート → embedding 更新 → 検証、という手順を書いただけのものです。
どう使っているか
Claude に自然言語で聞くだけです。
2020年の夏のわたしのツイートを20件ほど見せて。技術系のものがあれば優先で。
すると chiebukuro_query_blog か chiebukuro_semantic_search_blog のどちらかが呼ばれて、結果が返ってきます。
SQL で日付レンジを絞りたいだけなら query 側、テーマで探したいなら semantic search 側、という使い分けになっています。Claude が適当に選んでくれるので、こちらは「何を知りたいか」だけ伝えればよい、というところです。
日誌系のスキルと組み合わせると面白いです。chiebukuro-daily-journal スキルが location_history と things3 と health DB を横断して日誌を作ってくれるのですが、そこに blog DB を加えると「その日に何をして、何を考えて、何を書いたか」まで一枚の日誌になります。
これから
次は「書いた内容の濃さ」をどう扱うかです。
ツイートの1件とブログ記事の1件を同じ重みで semantic search するのは、さすがに粗いです。retrospective の記事は平均1,247文字あり、twitter の1件は平均40文字なので、検索結果に大量のツイートが並ぶと、わたしが腰を据えて書いた記事が埋もれます。
source を重みに入れる、長さでフィルタする、長文は chunk 化する、あたりをぼんやり考えていますが、まずはドッグフーディングしてみてから決めようと思います。
あとは、この chiebukuro-mcp のシリーズ自体を、もう少しまとめて書き起こす時期かもしれません。いろいろ DB を並べてきて、個別に紹介してきましたが、全体像を改めて並べたほうが自分でも整理できそうです。そのうち。