Amazon OpenSearch Service の Derived Source でインデックスストレージを削減してみた
はじめに
OpenSearch Service の運用でインデックスのストレージ使用量に悩まされている方も少なくないと思います。
私自身、お客様環境のコスト見直しでインデックスサイズを眺めていて、_source フィールドが想像以上に大きな割合を占めていることに気づく場面がよくあります。
そんな悩みに刺さりそうな機能として、Amazon OpenSearch Service に Derived Source が追加されました。
Managed 側は 2025 年 9 月、Serverless 側は 2026 年 4 月にリリースされています。
「_source を保存しない」と聞くと、enabled: false と何が違うのかとか、原本が消えるなら更新や reindex はどうなるのかといった疑問が浮かびます。
このあたりを整理しつつ、Managed の汎用 Derived Source を実際のインデックスに適用してストレージ削減幅と挙動を確認してみました。
なお類似の仕組みとして、ベクトルフィールド (knn_vector) 専用の先行実装が OpenSearch 3.0.0 で導入されており、3.0.0 以降で作成されるベクトルインデックスではデフォルトで有効になっています (詳細は OpenSearch 公式ブログ を参照)。
また Elasticsearch 側にも同じ発想の synthetic _source (公式ドキュメント) が存在します。
が、いずれも今回は深掘りしません。
_source を扱う 4 つのアプローチ
_source の扱いには大きく 4 つのモードがあります。
デフォルト (完全保持)、enabled: false で完全無効化、excludes / includes で部分除外、そして Derived Source です。
後ろの 3 つはストレージ削減を狙うモードですが、それぞれ失う機能や制約のバランスが違います。
観点ごとに並べると、トレードオフがはっきりします。
| 観点 | デフォルト | enabled: false |
excludes |
Derived Source |
|---|---|---|---|---|
| ストレージ削減 | 0% | 最大 | 中 | 最大 50% 程度 |
_update |
○ | × | △ (除外フィールドは失う) | ○ |
_reindex |
○ | × | △ (除外フィールドは失う) | ○ |
検索結果の _source 返却 |
○ | × | ○ (除外以外) | ○ (制約あり) |
| 設定変更 | dynamic | 作成時のみ | 作成時のみ (一部更新可) | static |
| 対応フィールド | 全て | 全て | 全て | 限定 |
enabled: false は機能制約が重く、Derived Source 登場以降は積極的に選ぶ場面がかなり限定的だと感じます。
従来 enabled: false を選んでいたようなワークロードも、運用制約が軽い Derived Source に置き換える選択肢が出てきました。
excludes は「特定の重いフィールドだけ原本から外したい」というピンポイント用途では今でも有効ですが、汎用的なストレージ削減策としては Derived Source が第一候補に上がるようになった、という位置付けです。
Derived Source とは
Derived Source は、_source の保存をスキップし、必要になったタイミングで doc_values や stored field から動的に再構成する機能です。
_source を完全に消してしまうと _update や _reindex、ハイライトといった _source 依存の操作が壊れそうですが、Derived Source ではそれらの操作に対して裏で _source を組み立て直して返してくれるので、見かけ上は透過的に動作します。
たとえば _update は、内部で再構成を呼び出して元の _source 相当を組み立て、差分を当てて再インデックスする流れになります。
_source はインデックスのストレージ使用量の 30〜50% を占めることも珍しくないと AWS のドキュメントでも説明されており、ここを丸ごと削れれば削減インパクトはそれなりに大きい、という狙いです。
多数のフィールドを持つ時系列・ログ解析ワークロードや、高次元ベクトルを含むワークロードほど効きが良くなる傾向があります。
ただし再構成には doc_values を引き直す CPU コストが掛かるため、_search で _source を返すケースや _update を頻発するワークロードでは、通常構成と比べて取得レイテンシや CPU 使用率が上がる点は念頭に置いておきたいです。
試してみた
Derived Source はインデックス単位で有効化します。
ここでは実際に OpenSearch Service のドメインに対して Derived Source 有効 / 無効の 2 つのインデックスを作成し、同じデータを投入してストレージサイズを比較してみます。
前提バージョン
Managed ドメインでは OpenSearch 3.1 以降、Serverless コレクションでは 2026 年 4 月以降の全対応リージョンで利用できます。
なお Derived Source は OSS では 3.2.0 で初出ですが、AWS Managed では Service Software Update によって engine version 3.1 のドメインから利用できる形でバックポート提供されています。
詳細は AWS ドキュメントを参照してください。
検証環境
| 項目 | 値 |
|---|---|
| サービス | Amazon OpenSearch Service |
| エンジンバージョン | OpenSearch 3.5 |
| インスタンスタイプ | t3.small.search |
| ストレージ | EBS gp3 100 GiB |
Step 1: 比較用の 2 インデックスを作成する
同じマッピングで Derived Source だけを切り替えた 2 インデックスを用意します。
Derived Source は静的設定なので作成後のインデックスに後から当てることはできず、既存インデックスに適用したい場合は新しいインデックスを作成して reindex する必要があります。
ベースラインとなる通常インデックス (logs-normal) は以下のとおりです。
PUT /logs-normal
{
"settings": {
"index": {
"number_of_shards": 1,
"number_of_replicas": 1
}
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"message": { "type": "text" },
"status_code": { "type": "integer" },
"response_ms": { "type": "float" },
"client_ip": { "type": "ip" },
"success": { "type": "boolean" }
}
}
}
Derived Source を有効にしたインデックス (logs-derived) は以下のとおりです。
PUT /logs-derived
{
"settings": {
"index": {
"derived_source": { "enabled": true },
"number_of_shards": 1,
"number_of_replicas": 1
}
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"message": { "type": "text" },
"status_code": { "type": "integer" },
"response_ms": { "type": "float" },
"client_ip": { "type": "ip" },
"success": { "type": "boolean" }
}
}
}
差分は settings.index.derived_source.enabled: true の 1 行だけです。
マッピングは Derived Source の対応フィールド (date / keyword / text / integer / float / ip / boolean) のみで構成しています。
両方とも以下のレスポンスで正常に作成されました。
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "<インデックス名>"
}
Step 2: 同じデータを両インデックスに投入する
下記のようなログ風のサンプルデータを各インデックスに 1,000 件ずつ _bulk API で投入しました。
service の連番、response_ms のランダム値、client_ip のランダム IP で 1 件ずつ少しずつ違うドキュメントになるようにしています。
{"index":{"_index":"logs-derived"}}
{"timestamp":"2026-04-15T10:00:00Z","level":"INFO","service":"svc-1","message":"Log entry number 1 with payload data for testing derived source","status_code":200,"response_ms":995.0,"client_ip":"10.104.91.177","success":true}
投入後、両インデックスを refresh してセグメントを可視化します。
POST /logs-derived,logs-normal/_refresh
レスポンス:
{
"_shards": {
"total": 4,
"successful": 2,
"failed": 0
}
}
Step 3: ストレージサイズを比較する
_cat/indices でインデックスごとのサイズを確認します。
GET /_cat/indices/logs-derived,logs-normal?v&h=index,docs.count,store.size,pri.store.size
出力:
index docs.count store.size pri.store.size
logs-derived 1000 70.7kb 70.7kb
logs-normal 1000 99.1kb 99.1kb
ベースラインの logs-normal が 99.1 KB に対し、Derived Source 有効の logs-derived は 70.7 KB でした。
約 29% の削減になっています。AWS のドキュメントにある「最大 50% 程度」より控えめな数字に着地しています。
理由はおそらくドキュメントが小さいことです。
今回 1 件あたり JSON 換算で 200 バイト前後しかなく、転置インデックスや doc_values の固定オーバーヘッドが相対的に大きく見える状態です。
1 件あたりのフィールド数が多かったり、text フィールドに長文が入ったりするほど _source の占める比率が上がるので、削減率も伸びると考えられます。
今回の結果からの感覚値としては、「最大 50%」はあくまで上限値で、ドキュメントサイズやフィールド構成次第では下振れすると見ておくのが実態に近そうです。
Step 4: 検索・取得挙動を確認する
Derived Source 側に対しても、通常通り _search で _source が返ってくるかを確認します。
GET /logs-derived/_search
{
"query": { "match": { "level": "INFO" } },
"size": 1
}
返ってきた _source は以下のとおりでした。
{
"success": true,
"level": "INFO",
"message": "Log entry number 1 with payload data for testing derived source",
"timestamp": "2026-04-15T10:00:00.000Z",
"status_code": 200,
"client_ip": "10.104.91.177",
"response_ms": 995.0,
"service": "svc-1"
}
値そのものはすべて投入時と一致していますが、見比べてみると 2 点ほど差分があります。
1 つはフィールド順です。
投入時は timestamp, level, service, message, status_code, response_ms, client_ip, success の順で流し込みましたが、返ってきた _source は success, level, message, timestamp, status_code, client_ip, response_ms, service の順に並び替わっています。
doc_values や stored field からフィールドを 1 つずつ引いて組み直す都合上、投入時の JSON キー順は保持されない挙動のようです。
もう 1 つは日付フォーマットです。
投入時の "2026-04-15T10:00:00Z" が、返却時には "2026-04-15T10:00:00.000Z" とミリ秒付きに正規化されています。
date 型は内部的にミリ秒精度で保持されるため、再構成時はその精度でシリアライズされるのが理由と思われます。
数値 (integer / float)、boolean、ip、keyword、text は、投入時と同じ表現で返ってきました。
_source のキー順や日付文字列の完全一致に依存しているクライアントコードがあると、このあたりで想定外の挙動に当たる可能性があるので、Derived Source を導入する前に取得側の前提をひととおり洗っておくのが無難です。
制約事項
Derived Source を導入する前に押さえておきたいのは、非対応フィールドタイプと静的設定の 2 点です。
nested、copy_to を含むフィールド、ignore_above / normalizer を持つ keyword / wildcard などの非対応フィールドタイプを含むマッピングで有効化すると、インデックス作成または取り込み時にエラーになります。
運用上は「どのフィールドが入ってくるか事前に分かっているインデックス」でのみ有効化するのが無難で、ログのようにスキーマが自動拡張されるケースはスキーマ固定方針とセットで検討したいところです。
また、Derived Source はインデックス作成時のみ有効化できる静的設定です。
既存インデックスに当てたい場合は、新しいインデックスを作って reindex する必要があります。
そのほか、日付のミリ秒への正規化、Geopoint の固定フォーマット化、配列順のソート、keyword の重複排除など、_source が完全な原本に戻らないケースがいくつかあります。
細かい挙動差は OpenSearch 公式ドキュメントを参照してください。
ユースケース別の推奨モード
4 アプローチを踏まえ、代表的なユースケースごとの推奨モードを整理すると以下のとおりです。
「向いている / 向いていない」の細かい条件は、これまでに挙げた制約事項から自然に決まります。
| ユースケース | 推奨モード |
|---|---|
| 汎用検索アプリ / 部分更新主体 | デフォルト |
| ログ解析・時系列分析 | Derived Source |
| ベクトル検索 / RAG | Derived Source (可能なら Disk-optimized vector search と併用) |
| 特定フィールドだけ原本から外したい | _source excludes |
| 原本完全性が必要 (コンプライアンス、配列順保持など) | デフォルト |
まとめ
Derived Source は _source を扱う 4 アプローチ (デフォルト / 無効化 / 部分除外 / 再構成) のうちの 1 つで、_source の保存をスキップし、doc_values などから必要に応じて再構成するという発想でストレージ使用量を抑えます。
うまくはまれば削減効果はかなり大きく、ログ解析やベクトル検索のように _source が肥大化しがちなワークロードでは特に効きそうです。
一方で、有効化はインデックス作成時のみの静的設定で、対応フィールドにも制限があり、配列順や日付フォーマットの再現性にもクセがあります。
とりあえず全インデックスに有効化してしまえる類のスイッチではなく、ワークロード単位で「ここは入れて良い、ここはやめておく」と判断していく機能だなと感じました。
新規インデックスで _source まわりの方針を決めるときは、まずはデフォルトのまま運用してみて、ストレージが問題になった時点で Derived Source を検討する、という順序がよさそうです。
enabled: false を最初から選ぶ場面は Derived Source 登場以降だいぶ限定的になりました。
ストレージコストが気になる環境があれば、本番に投入する前に検証用インデックスを 1 つ立てて、削減幅と挙動の差分を確認するところから始めると判断しやすいと思います。
他にも index.codec の Zstd 化や OR2 / OM2 インスタンスとの組み合わせなど、ストレージを更に抑える打ち手はあります。
今回は Derived Source 単独に絞りましたが、コスト最適化の全体像を描く際には併せて検討してみてください。







