Yyatmita

検索基盤を建てるのをやめた話——2.3GB のログを測ったら『何も作らない』が正解だった

Claude Code の会話ログを全文検索したい。そう思って自作のハイブリッド検索基盤に載せる設計を詰めていったら、用途分解・計測・既存ツールの確認を重ねるごとに要件が消えていき、最終的に『標準の Grep ツールで引くだけ=実装ゼロ』に着地した。YAGNI を計測で正当化したプロセスの記録。

自分のエージェント基盤を組む#claude-code#kbsearch#yagni#measurement#interview

Claude Code との会話は、すべて ~/.claude/projects/<project>/*.jsonl に溜まっていく。過去の試行錯誤がまるごと残っているわけで、「これを全文検索できたら便利では?」と思うのは自然な発想だ。

ちょうど手元には KBSearch という自作のハイブリッド検索基盤(Qdrant のベクトル検索 + SQLite FTS5 + RRF マージ)がある。蔵書と Obsidian ノートを検索するために作ったものだ。会話ログもこれに載せれば、セマンティック検索つきの「AI 作業の記憶」が手に入る——はずだった。

結論から言うと、何も作らなかった。それが最適だと分かったからだ。その削り込みの過程を、相談相手だった Claude Code に振り返ってもらった。


1. 発端は「終了フックでインデックス」

記者: 最初は何を作ろうとしていたんですか。

開発者: セッションが終わるたびに、会話ログを KBSearch にインデックスする仕組みです。終了フック (SessionEnd) で発火させて、ベクトル検索できるようにする。最初の絵はそれでした。

記者: 素直な設計に見えますが。

開発者: 素直すぎて、論点を1つも詰めていなかった。相談しながら順番に潰していったら、要件がどんどん削れていったんです。最終的に作るものが消えた。

2. 用途を分解したら、意味検索が要るのは1/3だった

記者: まず何から詰めたんですか。

開発者: 「何を引きたいのか」です。ぼんやり「全文検索したい」では設計できない。具体的なユースケースに割ったら3つに分かれました。

  • A:「このエラー、前にどう直したっけ?」
  • B:「あの設計、なんでそう決めたんだっけ?」
  • C:「あのコマンド、何だったっけ?」

記者: それぞれ性質が違う?

開発者: まったく違います。A と C は 固有の文字列を探している。エラーメッセージもコマンド名も、一字一句決まった文字列です。こういうのは完全一致が一番強い。むしろベクトル検索は意味を曖昧化するので不利なくらい。

記者: B は?

開発者: B だけが概念検索なんです。当時「auth」と書いていたものを、今は「ログイン認証」で探したい。この語彙のズレを越えられるのが意味検索(embedding)の真価です。逆に言うと、embedding が勝てるのは B だけ。3つのうち1つ。しかも B を実際に何回引くかは読みにくい。

記者: 高機能が要るのは一部だけ、と。

開発者: そう。この時点で「全部に embedding」という前提が崩れ始めた。

3. 「終了」を待つ設計は、そもそも脆い

記者: トリガーの話に戻ると、終了フックは?

開発者: これも詰めたら脆かった。session の jsonl は、resume すると後から追記されるんです。「終了した」と思ってインデックスしても、続きが書かれる。SessionEnd という"終了イベント"を最終確定とみなせない。クラッシュで発火しないこともある。

記者: どう解決を?

開発者: mtime(更新時刻)の差分を見る方式です。Obsidian のインデックスで既にこのパターンを使っていて、毎時 cron で「変わったファイルだけ」拾う。mtime を見るなら『終了』という概念そのものが要らない。追記されたら次の巡回で取り込まれるだけ。終了を待たないから、かえって堅牢なんです。

記者: イベント駆動より状態監視のほうが堅い、と。

開発者: ケースによっては、ですけどね。今回はそうでした。

4. 自己 RAG の罠——コンテキスト爆発

記者: 検索結果は誰が使う想定だったんですか。

開発者: 人間が読むのではなく、今まさに作業している Claude(私)が引く——自己 RAG です。記憶の外部化ですね。ただこれが一番事故りやすい。

記者: どう事故るんですか。

開発者: 「過去会話を引く」が「過去の長い応答を、コンテキストにまるごと貼り直す」になりがちなんです。会話ログの応答は平気で千トークンを超える。素朴にやるとコンテキストが爆発します。

記者: 対策は。

開発者: 検索は全文を返さず、抜粋とポインタだけを返す。Claude はまず抜粋を見て、本当に必要なセッションだけ本体を読みに行く。段階的に開示する設計(progressive disclosure)ですね。これで爆発は防げる。設計としては成立していました。ここまでは。

5. 「rg で足りるのでは」

記者: ここまで設計が固まってきたのに、まだ削るんですか。

開発者: ええ。一段引いて聞かれたんです。「それ、ripgrep で足りるのでは?」と。これが効いた。

記者: grep で?

開発者: さっきの分解を思い出してください。A と C は固有文字列の検索です。固有文字列なら grep の完全一致が一番正確。2.3GB あっても ripgrep なら一瞬で舐めます。速度は問題にならない。embedding が勝てるのは依然 B だけ。

記者: 検索基盤の存在意義が、B 一点に絞られてきた。

開発者: そこへ追い打ちで、もう一言。「Claude が使う Grep ツール、中身 ripgrep じゃないの?」と。

記者: ……あ。

開発者: そうなんです。私は標準で Grep ツールを持っている。中身は ripgrep です。「前にこの話したよね、探して」と言われたら、その場で ~/.claude/projects*.jsonl で grep すればいい。A と C は、何も作らなくても今すぐ引ける。ラッパースクリプトすら要らなかった。基盤を建てようとして、手元の道具を忘れていたんです。

6. 計測が、残りを全部ひっくり返した

記者: B のための embedding は、まだ残っていた?

開発者: 残っていました。「2.3GB をベクトル化したら重いし、ストレージも爆発するのでは」という懸念とセットで。ここで初めて、憶測をやめて実データを測った

記者: 何を測ったんですか。

開発者: ログの中身の内訳です。結果がこれでした。

抽出範囲サイズ
生 jsonl 全体2,380 MB
user/assistant の content(ツール出力込み)1,218 MB
text ブロックだけ(本物の会話)73 MB

記者: 73MB。全体の3%しかない。

開発者: 2.3GB の正体は、大半がツールの出力でした。ファイルのダンプ、Bash の実行結果、OCR テキスト。人間と私が実際にやり取りした言葉は 73MB しかない。231 プロジェクト・30 万イベントもあって、です。

記者: その数字で、何が変わったんですか。

開発者: 二重にひっくり返りました。まず「2.3GB だから重い・爆発する」という懸念が消えた。73MB なら 3〜5万チャンク、ベクトルにしても 200MB 程度。爆発しない。やろうと思えば安全に作れると分かった

記者: じゃあ作れる、と。

開発者: ところが同じ数字が逆向きにも効くんです。embedding しても A と C は grep に勝てないのは変わらない。価値があるのは相変わらず B だけ。「作れる」と「作る価値がある」は別問題で、計測は前者を肯定しつつ後者を否定した。

記者: 測ったから、両方はっきりした。

開発者: 測らなければ「なんとなく重そうだからやめる」か「なんとなく便利そうだから作る」のどちらかだった。どっちも雰囲気です。73MB という数字が、判断を雰囲気から事実に変えた

7. 既存の仕組み(memory)と食い合わないか

記者: もう1つ懸念があったとか。

開発者: Claude Code には memory という仕組みが既にあります。会話で得た確定的な事実を、キュレートして保存し、毎セッション読み込む。会話ログ検索とこれが食い合わないか。

記者: 役割が被る?

開発者: いや、直交していました。memory は「確定した結論」を能動的に push する仕組み——少数精鋭で、毎回コンテキストに乗る。会話ログは「経緯・試行錯誤の生ログ」を、必要なときだけ pull する。抽象度も鮮度も取得タイミングも違う。むしろ「ログを引いて重要だと気づいたら memory に昇格させる」という供給関係になる。

記者: 競合しないなら、作ってもいい理由にはなる。

開発者: でもそれは「作らない理由が1つ消えた」だけで、「作る理由」ではない。B の需要が薄いという本筋は動かなかった。

8. 結論——段階0は「何も作らない」

記者: で、最終的に。

開発者: 段階を引き直しました。

  • 段階0: 必要なとき、私が Grep ツールで生 jsonl を引く。実装ゼロ。A・C をカバー。
  • 段階1: 見やすさが辛ければ、jq の整形ラッパーを1本。
  • 段階2: B の概念検索がどうしても欲しくなったら、73MB を KBSearch に載せる。爆発しないと確認済みなので安全に足せる。

そして「まず段階0、つまり何も作らないで運用してみる」が結論です。不満が出た部分だけ、後から足せばいい。

記者: せっかく設計を詰めたのに、ゼロ。

開発者: 詰めたから、ゼロでいいと確信できたんです。用途を分解し、トリガーを検討し、爆発対策まで設計し、データを測った。そこまでやって初めて「これは作らないのが最適だ」と、雰囲気でなく言える。削り込んだ末のゼロは、何も考えないゼロとは別物です。


振り返って——一般化できる教訓

この相談で起きたことを抽象化すると、こうなる。

  1. 「便利そう」を用途に分解する。 高機能(ここでは意味検索)が本当に要るのは、たいてい一部のユースケースだけだと分かる。今回は3つのうち1つ。

  2. 作る前に実データを測る。 想定(2.3GB)と実態(本文73MB)は、しばしば1桁以上ずれる。その数字が判断を雰囲気から事実に変える。

  3. イベント駆動より状態監視が堅いことがある。 「終了」を待つ設計は、追記やクラッシュに弱い。mtime を見れば終了概念そのものが要らない。

  4. 新しい基盤を建てる前に、手元の道具を確認する。 標準の grep、既存のエコシステム(memory)で足りないか。多くの場合、最適解は「何も作らない」だった。

便利そうなものを作る能力より、作らないと決められる判断のほうが、たぶん希少だ。そしてその判断を支えたのは、巧妙な設計ではなく、wc -c で測った1つの数字だった。