インデックスを日付で分ける理由
一言でいうと
ログのインデックスをlogs-2026.08.21のように日付で分ける理由は、削除するためです。ドキュメントを1件ずつ削除するのは遅いですが、インデックスごと削除するのはファイルの削除なので、即座に終わります。
なぜ必要なのか
検索エンジンでドキュメントを削除するのは、実際に消すことではありません。「削除済み」の印を付けておき、あとでセグメントをマージするときに実際に取り除かれます。そのため、古いログ1億件をdelete_by_queryで削除すると数時間かかり、その間クラスターが遅くなります。
インデックスを日付で分けておけば、「8月1日分の削除」はDELETE logs-2026.08.01の1回で済みます。ディレクトリを削除するのと同じコストです。
どう動くのか
インデックステンプレートが、新しく作られるインデックスに設定を自動で適用します。
PUT _index_template/labhub-logs
{
"index_patterns": ["labhub-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"refresh_interval": "10s"
},
"mappings": {
"dynamic": false,
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"trace_id": { "type": "keyword" },
"pod": { "type": "keyword" }
}
}
}
}
"dynamic": falseがこの設定の核心です。理由は下で説明します。
ISM(ライフサイクル管理)が削除の作業を自動化します。
hot (7일) → warm (23일, 복제본 0, 강제 병합) → delete
マッピング爆発
dynamicを有効にしておくと、新しいフィールドが入ってくるたびにマッピングが自動で作られます。ところがアプリケーションがログにユーザーIDをフィールド名として入れたり(user_12345: "...")、エラーオブジェクトをまるごとJSONで入れたりすると、フィールドが数千個になります。
フィールドが増えるとクラスター状態(cluster state)が大きくなり、それはすべてのノードに複製されるため、クラスター全体が遅くなります。ひどい場合はマスターノードが落ちます。これをマッピング爆発(mapping explosion)といいます。
防ぐ方法は3つあります。
"dynamic": false: 定義していないフィールドは保存はするが、インデックス化しません(検索はできませんが、取得はできます)"dynamic": "strict": 定義していないフィールドが来たら拒否しますindex.mapping.total_fields.limit: 上限を設けます(デフォルト1000)
strictは安全に見えますが危険です。コレクターが想定外のフィールドを1つ付けるだけで、ドキュメントがまるごと拒否され、ログが失われます。実際にFluent Bitが内部フィールド_pを付けて400エラーが無限リトライで回り続け、ログがまるごと止まったことがあります。そのため、ログのインデックスにはfalseのほうがよいです。
よくある勘違い
シャードを多く置けば速いという考え: シャード1つはLuceneインデックス1つで、それぞれがファイルハンドルとメモリを使います。1日数GB程度のログにシャード5つは無駄です。経験則はシャード1つあたり10–50GBで、それより小さければシャードを減らします。
textとkeywordを混同すること: textは分析してトークンに分割するため全文検索はできますが、集計とソートができません。keywordはそのまま保存するため集計・ソートはできますが、部分検索ができません。レベル・Pod名・トレースIDはkeyword、メッセージ本文はtextです。
保持期間とコストを設計に入れる
ログインデックス設計の半分は検索性能で、残りの半分はいつ何を捨てるかです。後者を決めておかないと、ある日ディスクがいっぱいになり、そのときは慌てて削除するあまり、必要なものまで消してしまいます。
段階に分けます。最近のものは速いディスクに、古いものは遅いストレージに、さらに古いものはオブジェクトストレージに置き、最後に削除します。インデックスライフサイクル管理(ILM)がこの遷移を自動で行います。
| 段階 | 期間(例) | 何をするか |
|---|---|---|
| hot | 0–3日 | 書き込みと検索の両方が行われます |
| warm | 3–14日 | 読み取り専用です。セグメントをマージし、レプリカを減らします |
| cold | 14–90日 | 遅いノードやオブジェクトストレージへ移します |
| delete | 90日以降 | 削除します |
シャードを細かく分けすぎることが最もよくある失敗です。シャード1つごとにメモリとファイルハンドルが付くため、小さなシャードが数千個あるとクラスターが遅くなります。シャード1つあたり10–50GBを目標にし、1日分がそれより小さければ、インデックスを日別ではなく週別にするか、データストリームのロールオーバー条件(max_primary_shard_size)に任せます。
すべてのフィールドを検索可能にしません。検索はせず表示するだけのフィールドはindex: falseにしておくと、インデックス化のコストと保存容量が減ります。逆に、集計だけに使う数値はdoc_valuesがあれば十分です。
形の異なるログを1つのインデックスに入れません。フィールド名が重なっていて型が異なるとインデックス化が拒否され、そのドキュメントは黙って失われます。サービスごとにインデックスを分け、共通フィールドは命名規約を決めておきます。
コストは多くの場合、検索ではなくインデックス化から生じます。毎秒のドキュメント数がそのままCPUになります。デバッグレベルのログを本番でそのまま送ると、そのコストを毎日支払うことになります。サンプリングをかけるか、正常なリクエストはトレースに任せ、ログは異常な場合にだけ残す方向へ移します。
実務で本当に大切なこと
ログにトレースIDを入れることがオブザーバビリティの半分です。それがあれば、「この遅いリクエストが残したログ」を一度に探せます。なければ、時刻とPod名で手探りすることになります。
フィールド名は最初から標準に従うのがよいです。ECS(Elastic Common Schema)は@timestamp、log.level、service.name、trace.idのような名前を定めています。あとから変えるには、古いインデックスをすべて再インデックスする必要があります。