📕

安直な実装は露骨にパフォーマンスを劣化させる by OpenSearchの事例

に公開

はじめに

おはようございます!こんにちは!こんばんは!はじめまして!
スターフェスティバルの DPon です!

この記事は スターフェスティバル Advent Calendar 2025 の15日目の記事です!!!!!

https://qiita.com/advent-calendar/2025/stafes

今回はOpenSearch周辺で発生した問題の実装と対応事例を紹介します。
なお、ある程度OpenSearchの基礎知識があることを前提に話を進めていきます。

OpenSearchとは

OpenSearch は、AWSが提供するオープンソースの検索および分析エンジンです。
大量のデータを効率よく検索・集計することができECサイトの検索機能にもよく利用されています。

弊社でのOpenSearch利用状況

弊社の主要プロダクトであるごちクルの商品検索機能でも採用しています。
それともうひとつ別のアプリケーション(以降Bと呼びます)と単一のOpenSearchクラスターを共有している状態です。

OpenSearchがダウンした場合、どちらのアプリケーションでも検索機能が利用できなくなるため、ベストな構成とは言えませんが管理都合上これを許容しています。

インスタンスは共有していますが、各アプリケーションごとに専用のINDEXを用意しているので、参照するデータは分離されています。
このINDEXは毎時バッチ処理にて生成されています。

事の発端:アプリケーションBでの検索機能改修

アプリケーションBにおいて、検索画面遷移時のソート処理を改修する話が出てきました。
INDEX中に含まれている既存のパラメータで、ソートはできそうだったので以下のような処理を組んでみました。

'_script' => [
    'type' => 'number',
    'order' => 'asc',
    'script' => [
        'source' => <<<'PAINLESS'
                def sortVal = 999999999;
                for (info in params._source.store.infos) {
                  if (info.codes.contains(params.code) && info.val1 == 0) {
                    sortVal = Math.min(sortVal, info.val2);
                  }
                }
                return sortVal;
                PAINLESS,
        'params' => [
            'code' => $code,
        ],
    ],
],

実際のパラメータ名などは伏せていますが、要はスクリプトでループを回して条件に合うものを探し、最小値をソートキーとして返すというものです。

問題1. 開発環境での検索処理パフォーマンス悪化

この時点でお気づきの方もいらっしゃるかと思いますが、これを開発環境へ適用してみたところ、検索レスポンスが遅くなってしまいました。
具体的にログで時間を計測するまでもなく体感レベルの劣化でした。

問題2. TOP画面の件数表示も遅くなる

さらに今回対応した検索画面とは別の画面の処理もレスポンスが遅くなっている箇所がありました。

上記のようなTOP画面に検索フォームがあり、条件を入力した際にヒット件数を表示している箇所がありました。
なぜかここの件数表示も同様にレスポンスが遅くなっていました。

件数を取るだけなので、ソートの影響が出るとは思わず面食らった場面でした。

問題1の対処

実装したソート処理の計算量

ヒットしたドキュメント数を N
store.infos の要素数を M とすると計算量は

O(N*M)

となります。
つまりはデータ量に比例してレスポンスが遅くなってしまいます。

最小の検索条件ですと、5000件以上のドキュメントがヒットしてしまい、store.infosの要素数も1桁では済まないものも多く、開発環境で顕在化するほどの遅さとなってしまいました。

データ量に比例するのは実装時から明白でしたが、実際の処理時間までは予測がつかなかったのでひとまず組んでみましたが、案の定という結果でした。

ソート条件の事前計算

幸いなことにソート処理に必要となるパラメータは動的に変わるものではなく、バッチ処理で事前に計算しておくことが可能でした。
先程のソート処理で組んでいたループ中での条件判定と最小値の計算をバッチ処理中に行い、ソート用のフィールドとして追加しておくことにしました。
以下はバッチ処理中のマッピングの定義です。

// 事前計算済のソートキー
// script sortを回避し、標準ソート機能を使用するために事前計算した値
"sort_keys": map[string]interface{}{
  "type": "nested",
  "properties": map[string]interface{}{
    "code": map[string]string{"type": "keyword"},
    "value":     map[string]string{"type": "integer"},
  },
},

検索時のソート指定は以下のように変更しました。

'sort_keys.value' => [
    'order' => 'asc',
    'nested' => [
        'path' => 'sort_keys',
        'filter' => [
            'term' => ['sort_keys.code' => $code]
        ]
    ]
]

改修後の計算量

  • $codeにマッチしたsort_keys.valueを取り出す
  • その値で 通常の数値ソート

とだいぶシンプルな形になりました。
これにより、計算量は O(N) となり、データ量が増えてもレスポンスが遅くなりにくい形に改善できました。

問題2 TOP画面の件数表示遅延

原因

さて、TOP画面の件数表示が遅くなる問題。
こちらは実際のコードを追ってみたところ先程手を加えた検索処理が同様に使用されていました。

OpenSearchの Search APIは、検索結果のヒット件数も同時に返却してくれます。

  "hits": {
    "total": {
      "value": 110,

上記の hit.total.value をTOPページで取得して表示していたようでした。
ただ実質検索処理を走らせているため、ソート処理も内部で行われ今回の実装の影響を受け表示が遅延している状態でした。

対処

件数の取得には Count API を使用するように変更しました。
Count API はヒット件数のみを返却するAPIでありソート処理は行われません。
これにより、TOP画面の件数表示も高速化されました。

さらなる影響

問題2については、じつはもうひとつ影響がありました。

同時期にごちクルでも検索まわりの機能追加などがあり検索のリクエストが増えていたタイミングでした。
その時期からごちクルのCPUアラートがときどき発生しており、検索画面が起因と見られていました。
こまごまレプリカ数を調整したり、試行錯誤していたのですが、なかなか改善されずにいたところだったのですが
アプリケーションBのTOP画面の件数取得を修正すると、落ち着きを見せCPUアラートが発生しなくなりました。

AWSコンソールのSearch Rate(秒間の検索リクエスト数)も確認すると目に見えて減少しており、TOP画面の件数取得の影響が大きかったことがわかりました。

まとめ

シンプルですね。安直な実装はしない。

  • 既存パラメータを使い、スクリプトのループで計算処理を実装
  • 件数取得に既存の検索処理を流用

いずれも既存のものを流用して手早く実装しようとした結果、パフォーマンスに影響が出る形となっていました。

  • 実際のデータ量の考慮。
  • 内部処理を理解し、要件に適したAPIの選択。

が必要だった場面だと思います。

またクラスターを共有している点もメリット・デメリット含めて選択していましたが、今回デメリットの部分が顕在化した形となりました。
このあたりは何を重視するかによって判断が分かれるところかと思いますが、今後も注意深く見ていきたいと思います。

スタフェステックブログ

Discussion