3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LaravelプロジェクトにOpenSearchを導入してみた 〜止まらない検索機能と運用Tips編〜

3
Last updated at Posted at 2026-04-14

🔍 OpenSearch × RDB 連携実装ガイド 〜検索・運用編〜

💡 前編の振り返り
前編(LaravelプロジェクトにOpenSearchを導入してみた 〜AWS認証からゼロダウンタイム再構築まで〜)では、Docker環境の構築からOpenSearchへのデータ同期、そしてエイリアスを活用した「ゼロダウンタイムでのインデックス全量再構築」までを実装しました。
後編となる本記事では、実際にOpenSearchへ検索クエリを投げる実装と、障害時にシステムを守るRDBフォールバック機構、そして実務運用のためのTipsを解説します。


5. 検索の実行(RDBフォールバック機構)

本番運用において、OpenSearchのクラスター障害やネットワークエラーが発生した場合に、システム全体がダウン(検索機能が完全停止)してしまうのは避けるべきです。
そこで、OpenSearchが使えない場合や例外が発生した場合は、**自動で従来のRDB検索に切り替わる(フォールバックする)**機構を実装します。

🛡️ フォールバック処理の実装

// app/Services/SearchService.php

public function getItemList(array $conditions, int $currentPage): Collection
{
    // OpenSearchが利用可能な状態か判定
    if ($this->searchIndexService->canUseSearchEngine()) {
        try {
            // OpenSearchでの検索を実行
            return $this->searchIndexService->searchItemList($conditions, $currentPage, 20);
        } catch (Throwable $e) {
            // 検索失敗時はエラーを監視ツール(Sentry等)に通知しつつ、後続のRDB検索へ流す
            app(ExceptionHandler::class)->report($e);
        }
    }

    // OpenSearchが無効、または検索に失敗した場合はRDBで検索(従来の処理)
    return $this->getItemListByRdb($conditions, $currentPage);
}

⚙️ canUseSearchEngine の判定ロジック

利用可否の判定は、「環境変数(静的)」と「DBの設定値(動的)」の2段階で行います。

// app/Services/SearchEngine/SearchIndexService.php

public function canUseSearchEngine(): bool
{
    // env の設定 + DBのフラグ の両方がtrueの場合のみOpenSearchを使う
    return config('searchEngine.search_enabled')
        && $this->searchSettingService->isSearchEnabled();
}

💡 2段階のフラグ制御にしている理由

フラグ 用途
env: SEARCH_ENGINE_ENABLED デプロイ時にON/OFFを制御(環境単位のハードスイッチ)
DB: is_search_enabled 差分同期失敗時などに自動でOFFにする(システムによる動的制御)

※ 例えば、RDBとOpenSearchのデータ同期ジョブが失敗した場合、一時的にDBフラグをOFFにしてデータ不整合の露出を防ぎ、再構築が完了したらONに戻す、といった自律的な制御が可能になります。


6. 検索リポジトリの実装

RDBのデータと紐付けるため、OpenSearchからはドキュメントの id だけを取得するシンプルな実装例です。

// app/Repositories/SearchEngine/SearchIndexRepository.php

class SearchIndexRepository
{
    private const TABLE = 'items';

    /**
     * 名前で検索し、IDの配列を返す
     */
    public function searchByName(string $keyword): array
    {
        $result = $this->client->search([
            'index' => self::TABLE,
            'body' => [
                'size' => 20,
                '_source' => ['id'],  // 取得フィールドをIDだけに絞って通信量を削減
                'query' => [
                    'match' => [
                        'name' => $keyword,
                    ],
                ],
            ],
        ]);

        // レスポンスからIDだけを抽出して配列で返す
        return array_map(
            fn ($hit) => $hit['_source']['id'],
            $result['hits']['hits'] ?? []
        );
    }
}

📤 リクエスト例:

{
  "size": 20,
  "_source": ["id"],
  "query": {
    "match": {
      "name": "サンプルアイテム"
    }
  }
}

📥 レスポンス例:

{
  "hits": {
    "total": { "value": 3 },
    "hits": [
      { "_id": "101", "_source": { "id": 101 } },
      { "_id": "205", "_source": { "id": 205 } },
      { "_id": "342", "_source": { "id": 342 } }
    ]
  }
}

7. 検索エンジン設定のキャッシュ管理

5章で実装した is_search_enabled の判定は、すべての検索リクエストごとに実行されます。毎回DBへ問い合わせると負荷がかかるため、Redisによるキャッシュを導入します。

// app/Repositories/SearchEngine/SearchSettingRepository.php

class SearchSettingRepository
{
    private const TABLE = 'search_settings';
    private const CACHE_KEY = 'search_settings:is_search_enabled';
    private const CACHE_TTL_SECONDS = 3600;

    public function isSearchEnabled(): bool
    {
        // 1. Redisキャッシュを確認
        $cached = Redis::get(self::CACHE_KEY);
        if ($cached !== null) {
            return (bool) $cached;
        }

        // 2. キャッシュがなければDBから取得して保存
        $row = DB::table(self::TABLE)->where('id', 1)->first();
        $value = (bool) ($row?->is_search_enabled ?? false);

        Redis::set(self::CACHE_KEY, $value ? 1 : 0);
        Redis::expire(self::CACHE_KEY, self::CACHE_TTL_SECONDS);

        return $value;
    }
}

8. 実務で運用するためのTips(落とし穴と対策)

本番環境でこの構成を運用する際に、気をつけておくべきポイントを3つ紹介します。

① ページネーションの実装 (fromsize)

検索結果をページネーションする場合は、size(取得件数)に加えて from(スキップする件数) パラメータの指定が必要です。

$perPage = 20;
$from = ($currentPage - 1) * $perPage;

$result = $this->client->search([
    'index' => self::TABLE,
    'body' => [
        'from' => $from,   // ★開始位置を指定
        'size' => $perPage,
        '_source' => ['id'],
        // ...
    ],
]);

⚠️ 注意: OpenSearchの仕様上、from + size の合計が10,000を超える深層ページネーションはエラーになります。1万件を超える結果を扱う場合は search_after の導入を検討してください。

② キャッシュのパージ(破棄)漏れに注意

Redisで設定フラグをキャッシュしているため、システム側でフラグを更新した際にキャッシュを消し忘れると事故に繋がります(「DBはOFFにしたのに、1時間OpenSearchに繋ぎに行ってエラーが出続ける」等)。
フラグの更新処理には、キャッシュクリアを必ずセットで実装しましょう。

public function disableSearch(): void
{
    DB::table(self::TABLE)->where('id', 1)->update(['is_search_enabled' => false]);
    // ★即座にキャッシュを破棄して最新状態を反映させる
    Redis::del('search_settings:is_search_enabled');
}

③ フォールバックのサイレント障害を監視する

RDBフォールバック機構は非常に強力ですが、**「OpenSearchが落ちていることに開発陣が気づかない(ユーザー影響が出ないため)」**というサイレント障害のリスクがあります。
catch ブロック内の例外処理が、SentryやDatadog等の監視ツールに正しく通知される状態としておくことが重要です。


📋 まとめ

前編・後編にわたり、LaravelとOpenSearchの連携方法を解説しました。

レイヤー クラス・ファイル名 役割
Service SearchService OpenSearchとRDBのフォールバック制御
Service SearchIndexService 検索エンジン利用可否のロジック判定
Repository SearchIndexRepository OpenSearchへの検索クエリ実行・ID抽出
Repository SearchSettingRepository DBフラグの取得とRedisキャッシュ管理

検索機能のパフォーマンスや可用性に課題を抱えている方の参考になれば幸いです!

📊 【参考】OpenSearch導入によるパフォーマンス改善の成果

本記事で紹介するコードはエッセンスを抽出したシンプルなものですが、参考までに、実際のプロダクトで「従来のRDB検索」から「OpenSearch」へ移行したことで、どれくらいレスポンス速度が改善したかの計測結果をご紹介します。

対象機能 従来のRDB OpenSearch 改善効果
検索結果ページ
(特定エリアの絞り込み等)
約 7.7秒 約 0.8秒 約 10倍 高速化 🚀

💡 なぜここまで差が出るのか?
RDBでは多数のテーブルJOINや複雑なORDER BY、深いページネーションによってボトルネックが発生していました。
今回の移行では、単に検索エンジンを置き換えただけでなく、「検索用データ」と「表示に必要な基本データ」をインデックスにあらかじめ非正規化して格納する設計に変更しました。これにより、ページ表示時のRDBへの追加クエリを大幅に削減できたことが、ページ全体のレスポンス改善に繋がっています。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?