Amazon OpenSearch Service が新しいログ分析エンジンをリリースしていますので今日はそれを触っていきます。
例えばログ集計を行う際、蓄積されたデータの特定カラムを縦に集計していくケースが多くあります。サービスごとのエラー件数を数える、ホスト別にトレンドを見る、時間帯で絞り込む——こうした集計(アグリゲーション)が多用される場合、列指向で分析に振ったツールが向いています。
従来のOpenSearchは、フルテキスト検索やピンポイントのログ抽出(障害調査時のドリルダウン)が得意でしたが、数TB規模のログを対象にした大量の集計・トレンド分析は苦手で処理が重くなるケースがありました。
OpenSearchがデフォルトで使用するLuceneの転置インデックス(inverted index)は、「単語 → その単語を含むドキュメントの一覧(ポスティングリスト)」という形でデータを持っています。このため例えば「このtraceIdの1件を引く」といった検索は、該当ドキュメントへ一気に飛べるという構造になっています。
ところが集計は逆方向の処理になります。「サービス別にエラーを数える」「ホスト別にレイテンシを平均する」には、条件に一致した大量のドキュメントを1件ずつ辿りながら、対象フィールドの値を読み取って足し込んでいく必要があります。Luceneはこの用途のために列指向的な補助構造(doc values)を持ってはいるものの、実行モデル自体はドキュメント単位の逐次処理が発生してしまいます。さらに従来構成では生JSON(_source)をドキュメントごとに丸ごと保持するため、ストレージも読み取りI/Oも膨らみがちでした。件数が億単位になると、この「1件ずつ」の積み重ねと余分なI/Oが、集計クエリの重さとコストにそのまま効いてきます。
これに対し新エンジンの列指向(Parquet)+ベクトル実行は、必要な列だけを圧縮されたまとまりで読みこみ、不要な範囲(row group)を丸ごと読み飛ばし、まとめて演算することが可能になりました。
ログ分析特化の新エンジン(Optimized エンジン)
新エンジンは、列指向の分析ストレージ(Apache Parquet)とフルテキスト検索(Lucene の転置インデックス)を同じデータの上に同居させ、クエリごとに最適なコンポーネントへ自動で振り分けます。コーディネーターノード上の Apache Calcite がクエリを解析・最適化し、集計は Rust 製のベクトル化エンジン DataFusion が、検索述語は Lucene が手分けして処理を行います。両者は1つのクエリの途中で処理経過の受け渡しができるため、「ログ本文を検索して絞り込み、その結果を集計する」といった2つのエンジンを組み合わせて処理が可能となります。
新しくParquet形式のログファイルフォーマットの保存に対応したことでデータ保持における圧縮効率も向上しストレージコストが削減できるようになっています。
さっそくやってみる
ではマネージメントコンソールで新しいエンジンを備えたOpenSearchクラスターを起動してみます。
新しく Use Caseに Observability が選択可能となっています。

以下の選択では新しい Optimized エンジンを選択します。

あとはデフォルトのままOpenSearchクラスターを起動します。テスト用であれば設定を変更して、1AZ環境やアクセスしやすいようにVPC外部に作成するよう設定を変更しておきます。この記事では後程テスト用データを投入するために使う非IAMのマスターユーザーをベースとしたアクセスコントロールを設定します。

以下の通り起動中となりますので起動されるまで待ちます。

起動したら以下のドメインエンドポイントをメモしておきます。

接続情報を環境変数にセットします。
export OS_ENDPOINT="<上記でコピーしたもの>"
export OS_USER="<起動時に設定したusername>"
read -s -p "OpenSearch password: " OS_PASS; echoパスワード入力が求められますので入力します。
以下のコマンドで疎通を確認します。
curl -sS -u "$OS_USER:$OS_PASS" "$OS_ENDPOINT" | jq .以下の様な情報が戻れば成功です。
{
"name": "285e8ae50ee2ca2c793a167ba6024df2",
"cluster_name": "917561075114:kameoncloud",
"cluster_uuid": "ah_1MpyAQrmSmkbFUs_tXQ",
"version": {
"distribution": "opensearch",
"number": "3.7.0",
"build_type": "tar",
"build_hash": "unknown",
"build_date": "2026-07-19T06:24:14.453468637Z",
"build_snapshot": false,
"lucene_version": "10.4.0",
"minimum_wire_compatibility_version": "2.19.0",
"minimum_index_compatibility_version": "2.0.0"
},
"tagline": "The OpenSearch Project: https://opensearch.org/"
}ではテスト用のログを入力します。
cat > logs.ndjson <<'EOF'
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:01Z","service":"checkout","host":"host-1","level":"ERROR","status":500,"latency_ms":812,"message":"NullPointerException in CheckoutService.pay"}
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:02Z","service":"checkout","host":"host-2","level":"INFO","status":200,"latency_ms":95,"message":"order placed"}
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:03Z","service":"search","host":"host-1","level":"WARN","status":429,"latency_ms":40,"message":"rate limited"}
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:04Z","service":"search","host":"host-3","level":"ERROR","status":503,"latency_ms":1200,"message":"upstream timeout to inventory"}
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:05Z","service":"payments","host":"host-2","level":"ERROR","status":500,"latency_ms":640,"message":"gateway declined transaction"}
{"index":{"_index":"app-logs"}}
{"@timestamp":"2026-08-15T09:00:06Z","service":"payments","host":"host-2","level":"INFO","status":200,"latency_ms":73,"message":"transaction ok"}
EOFcurl -sS -u "$OS_USER:$OS_PASS" \
-H 'Content-Type: application/x-ndjson' \
"$OS_ENDPOINT/_bulk" --data-binary @logs.ndjson | jq '.errors'少し混乱しますが falseが戻れば成功です。
curl -sS -u "$OS_USER:$OS_PASS" -H 'Content-Type: application/json' \
"$OS_ENDPOINT/_plugins/_sql" \
-d '{"query":"SELECT COUNT(*) AS c FROM `app-logs`"}' | jq .先ほど投入したデータを確認します。
{
"schema": [
{
"name": "c",
"type": "long"
}
],
"datarows": [
[
6
]
],
"total": 1,
"size": 1
}先ほど6件投入しましたので 6 と表示されています。
サービス別の 5xx エラー件数をSQLで集計してみます。
curl -sS -u "$OS_USER:$OS_PASS" -H 'Content-Type: application/json' \
"$OS_ENDPOINT/_plugins/_sql" \
-d '{"query":"SELECT service, COUNT(*) AS errors FROM `app-logs` WHERE status >= 500 GROUP BY service ORDER BY errors DESC"}' | jq .{
"schema": [
{
"name": "service",
"type": "string"
},
{
"name": "errors",
"type": "long"
}
],
"datarows": [
[
"checkout",
1
],
[
"search",
1
],
[
"payments",
1
]
],
"total": 3,
"size": 3
}次に同じ集計をPPLで行ってみます
curl -sS -u "$OS_USER:$OS_PASS" -H 'Content-Type: application/json' \
"$OS_ENDPOINT/_plugins/_ppl" \
-d '{"query":"source=app-logs | where status >= 500 | stats count() as errors by service | sort - errors"}' | jq .{
"schema": [
{
"name": "errors",
"type": "bigint"
},
{
"name": "service",
"type": "string"
}
],
"datarows": [
[
1,
"checkout"
],
[
1,
"search"
],
[
1,
"payments"
]
],
"total": 3,
"size": 3
}では序文で触れた2つの組み合わせを行ってみます。
curl -sS -u "$OS_USER:$OS_PASS" -H 'Content-Type: application/json' \
"$OS_ENDPOINT/_plugins/_ppl" \
-d '{"query":"source=app-logs | where match(message, \"timeout\") | stats count() by service"}' | jq .{
"schema": [
{
"name": "count()",
"type": "bigint"
},
{
"name": "service",
"type": "string"
}
],
"datarows": [
[
1,
"search"
]
],
"total": 1,
"size": 1
}match(message, "timeout") で本文を全文検索し、ヒットした行を stats count() by service で集計しています。探す(Lucene の転置インデックス)と数える(列指向+ベクトル実行)を1本のクエリで繋げています。
従来の General Purpose Engine でもこのSQLとPPLの組み合わせは動作しますがoptimized Engineとは以下の違いがあります。
通常エンジン | Optimized エンジン | |
|---|---|---|
DSL クエリ | 使える(ネイティブ) | 非対応 |
SQL / PPL | 使える(DSL に翻訳して Lucene 実行) | 使える(ネイティブでベクトル実行) |
集計の速さ | Lucene の実行モデル依存 | 列指向+ベクトルで高速 |
序文で挙げた「検索は得意だが大規模集計は苦手」という従来の弱点に対して、検索(Lucene の転置インデックス)と集計(列指向+ベクトル実行)を、同じデータの上で使い分けながら1クエリに同居させることができるようになったのが今回のアップデートです。

