Claude Media
Claude CodeでElasticsearchの遅い検索を絞る — スローログとprofile APIの読み方

Claude CodeでElasticsearchの遅い検索を絞る — スローログとprofile APIの読み方

スローログで遅い検索を拾い、profile APIで遅い句を特定し、書き換え案を出させる流れを手順にしました。スローログの設定、claude -pへの渡し方、profileの読み方と制限を扱います。

Elasticsearchの検索が遅いときは、まずスローログで「どの検索が遅いか」を拾い、次にprofile APIで「その検索のどの句が遅いか」を調べます。この二段を踏めば、Claude Codeに読ませる材料が揃います。

役割は分けておくのが安全です。ログの集計は手元のスクリプトに任せ、Claude Codeには集計結果とprofileの出力を読ませて、書き換え案を出させます。案の良し悪しは、もう一度profileを取って確かめます。

スローログは何を拾い、何を拾わないか

スローログは、設定した時間を超えた検索やインデックス操作を記録する仕組みです。しきい値の既定値はすべて -1 で、設定するまでは何も出ません。

検索のスローログは、シャードごとに出力されます。クエリフェーズとフェッチフェーズは別々に有効にする必要があります。フェッチは、ヒットした文書を取り出す段階です。

しきい値は warn / info / debug / trace の4段階で指定します。次の例はクエリフェーズとフェッチフェーズの両方を有効にします。値は公式の設定例に沿った形で、そのまま本番に入れる数字ではありません。

PUT /my-index-000001/_settings
{
  "index.search.slowlog.threshold.query.warn": "10s",
  "index.search.slowlog.threshold.query.info": "5s",
  "index.search.slowlog.threshold.query.debug": "2s",
  "index.search.slowlog.threshold.query.trace": "500ms",
  "index.search.slowlog.threshold.fetch.warn": "1s",
  "index.search.slowlog.threshold.fetch.info": "800ms",
  "index.search.slowlog.threshold.fetch.debug": "500ms",
  "index.search.slowlog.threshold.fetch.trace": "200ms"
}

リクエストを起こしたユーザーも残したいときは、index.search.slowlog.include.user を true にします。

検索側にはクエリログという選択肢もある

Elasticsearchには、検索のスローログとは別にクエリログ(query logging)があります。スローログがシャード単位の実行時間しか見ないのに対し、クエリログはElasticsearchが測ったリクエスト全体の所要時間を記録します。Query DSL、ES|QL、EQL、SQLを1つの設定で扱える点も違いです。

この設定のページは、検索まわりのスローログ利用はクエリログへ移すことを勧めています。一方、インデックス操作のスローログに代わるものはなく、そちらはスローログのままです。ログの出力形式や設定の細部は、使っているバージョンのドキュメントで確認してください。

スローログの時間はシャード内の実行だけなので、ログ上は遅くないのにアプリ側で遅い場合は、ネットワークやコーディネートノードの待ちを疑うことになります。その切り分けは、分散トレースの遅いスパンを探す手順のほうが向いています。

スローログをClaude Codeに読ませる

ログの形式は出力設定やバージョンで変わるため、集計の正規表現を最初から決め打ちしないほうが安全です。次のように、先頭数行を見せて形式の判断から任せます。

head -n 5 slowlog.log | claude -p \
  "このElasticsearchスローログ1行の構造を説明して。\
検索本体(クエリDSL)と所要時間が入っているフィールドを特定して"

標準入力に流したデータはそのままプロンプトの材料になります。公式の例も、ビルドログを cat build-error.txt | claude -p '…' と渡しています。

形式が分かったら、集計はスクリプトにします。Claude Codeに「この形式のログから、検索本体を形ごとにまとめ、件数と最大所要時間を出すスクリプトを書いて」と頼み、書かれたスクリプトを自分で実行します。件数や最大値の計算をモデルに直接やらせると、数え違いが混ざるからです。

集計結果を渡して、調べる順番を決めさせるのが次の段です。出力を機械で扱いたいときは、--json-schema で形を固定できます。

cat top_queries.json | claude --bare -p \
  "件数と最大所要時間の両方を見て、profileを取る優先順位を決めて" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"order":{"type":"array","items":{"type":"string"}}},"required":["order"]}' \
  | jq '.structured_output'

注意点は3つあります。

  • 標準入力は10MBが上限です。超えるとエラーで止まるので、集計後のファイルを渡します
  • --bare はOAuthの認証情報を読みません。環境変数 ANTHROPIC_API_KEY を用意してください
  • 構造化出力が得られなかった場合、structured_output は応答に含まれません。スクリプト側で有無を確認します

--bare はフックやCLAUDE.mdも読み込まないので、CIや定期実行で結果を揃えたいときに向きます。

profile APIで遅い句を特定する

上位に来た検索を、検証用のクラスタか同じデータのコピーで再実行します。やり方は、リクエストの最上位に "profile": true を足すだけです。

GET /my-index-000001/_search
{
  "profile": true,
  "query": {
    "match": { "message": "GET /search" }
  }
}

公式はprofile APIを「デバッグ用で、検索実行に大きな負荷を加える」と警告しています。本番で常時有効にせず、スローログで絞った検索にだけ使います。

応答は冗長なので、必要な部分だけに絞ってからClaude Codeへ渡します。構造は profile.shards[].searches[].query の下にクエリの木が入っています。

curl -s "$ES_URL/my-index-000001/_search?human=true" \
  -H 'Content-Type: application/json' -d @slow_query.json \
  | jq '.profile.shards[] | {id, query: .searches[].query}' \
  > profile_query.json

?human=true を付けると、time_in_nanos に加えて丸めた読みやすい時間も入ります。

見る場所はtime_in_nanosとbreakdown

クエリの木の各ノードには、種類(type)、説明(description)、所要時間(time_in_nanos)、内訳(breakdown)、子(children)が入ります。親の時間は子の時間をすべて含みます。大きな親から子へ辿り、時間を食っている枝を探します。

description は、同じ TermQuery が並んだときに見分けるための手がかりです。公式の例では message:get と message:search がそれに当たります。

breakdown の主な項目は次のとおりです。

項目意味大きいときに疑うこと
create_weight意味クエリが使う状態や統計の準備にかかる時間大きいときに疑うことクエリ構造の複雑さ
build_scorer意味Scorerの生成時間大きいときに疑うこと句ごとの初期化コスト
next_doc / advance意味次の一致文書を探す時間大きいときに疑うこと対象文書数の多さ、must句の組み合わせ
match意味フレーズなど二段階照合の2段目の検証大きいときに疑うことフレーズ検索の重さ
score意味文書のスコア計算大きいときに疑うことスコアが要らないなら別の書き方を検討

右列は読み方の見当で、原因の断定ではありません。*_count は各メソッドの呼び出し回数で、句どうしを比べれば選択性の見当がつきます。

時間は壁時計のナノ秒で、正規化されていません。そのため絶対値より、句どうしの大小比較が使いどころです。

ワイルドカードと正規表現は木が崩れる

wildcard、regex、fuzzyのようなMultiTermQuery系は、シャードのセグメントごとに別の形へ書き換わります。公式は、こうした検索の応答は冗長で構造が整わず、親子の系譜が崩れると説明しています。時間の数値自体は正しいため、最上位のMultiTermQueryの時間だけを見て、子は読み飛ばすのが近道です。

Claude Codeに渡すときも、この注意を一言添えると読み違いが減ります。

profileから書き換え案を出させる

絞った profile_query.json と元のクエリを渡し、依頼の型を決めます。

cat slow_query.json profile_query.json | claude -p \
  "1つ目が元の検索、2つ目がprofile結果。time_in_nanosが最大の枝を\
親から順に辿り、遅い句を特定して。その句の書き換え案を最大3つ、\
結果が変わる可能性の有無つきで挙げて。profileの数値に無いことは推測と明記すること"

「結果が変わる可能性」を必ず書かせるのが要点です。たとえばスコアが不要な条件をフィルターに移す案は、速くなる可能性がある一方、並び順が変わるかもしれません。書き換えの前後で検索結果の同一性を確認する前提で、案を受け取ります。

案が出たら、次の順で確かめます。

  1. 元のクエリと書き換え後のクエリを、同じインデックス・同じ条件でそれぞれprofileする
  2. 同じ枝の time_in_nanos を並べて比べる
  3. 検索結果の件数と上位の文書が一致するか確認する

profileの時間は、プロファイルなしの実行時間と比べてはいけません。公式は、計測のための仕込みで負荷が増え、プロファイルなしの検索時間とは比較しないよう明記しています。比べるのはprofile同士か、プロファイルなしのクエリの took 同士です。

権限は読み取りだけ

Claude Codeに本番クラスタへ直接つながせる必要はありません。ここまでの流れでは、ログと整形済みのJSONを渡すだけで足ります。Manual modeの claude -p では、--allowedTools に挙げていない操作のうち承認が要るものは拒否されます。ログの解析スクリプトを書かせる段階だけ、Read と Edit を許可する形が目安です。

profileの数字に出ない遅さ

profileの限界を知らないと、「profile上は速いのに実際は遅い」で混乱します。公式が挙げる限界は次のとおりです。

  • ネットワークの遅延は測らない
  • キューで待った時間は含まない
  • コーディネートノードでシャードの応答を結合する時間も含まない
  • グローバルオーディナル(検索を速める内部の構造)の構築など、周辺の作業も含まない
  • 集計のreduceフェーズのprofileは取れない
  • サジェストの統計も取れない

スローログの時間とprofileの合計がずれる、アプリ側の計測はもっと長い、といったときは、この範囲外の時間が使われていると見ます。

つまずきやすい点

スローログが何も出ない。しきい値を設定していないか、設定したのが別のインデックスです。既定は -1 で、ログも出ません。

フェッチが遅いのにクエリの設定しかしていない。クエリフェーズとフェッチフェーズは別設定です。ヒット文書の取り出しが重い検索は、フェッチ側のしきい値でしか拾えません。

profileの数字をそのまま本番の目標値にする。profileの時間は計測の負荷込みです。目標はプロファイルなしの実測で決めます。

1回の計測で結論を出す。時間は実行のたびに揺れます。同じ条件で数回取り、傾向を見ます。

SQLデータベースで同じ型の調査をする場合は、MySQLのスロークエリを読みインデックス案を出させる手順とPostgreSQLでpg_stat_statementsの遅いクエリを集計する手順に、集計と検証の流れがあります。

まとめ

遅い検索は、スローログで拾い、スクリプトで形ごとに集計し、上位の検索だけをprofileにかけます。Claude Codeに任せるのは、profileの木を読んで遅い枝を特定し、書き換え案を出す部分です。案は、前後をprofile同士で比べ、検索結果が変わっていないことも確かめて初めて採用できます。

検索側のログは、全体の所要時間を測れるクエリログへの移行を公式が勧めているため、これから整備するなら両方の特性を見比べておくと迷いません。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. elastic.co/docs/reference/elasticsearch/index-settings/slow-log
  3. elastic.co/docs/reference/elasticsearch/rest-apis/search-profile
この記事を共有:XはてブLinkedIn