この記事は 一休.com Advent Calendar 2023 6日目の記事です。
一休レストランの開発チームでエンジニアをしている香西です。 今回は Solr クエリの速度改善についてお話します。
背景
2023年10月、一休レストランのスマートフォン用 レストラン詳細ページをリニューアルしました! UI/UX の見直しとともに、使用技術も一新しました。
- バックエンド言語:Python から Rustへ
- フロントエンドフレームワーク:Nuxt.js から Next.jsへ*1
課題
「日付を選ぶカレンダーの表示が遅い」
社内限定リリースの直後、多方面からこの声が聞こえてきました...
レストランへ行く日付を選ぶカレンダーは予約フローの第一ステップなので、表示速度が遅いことは致命的です。
特に、設定データ(料理のコース種類・席の種類など)が多いレストランでは、カレンダーの空席状況を取得するのに15秒以上かかることがあり、このままでは正式リリースできない状況でした。
一休レストランでは、各レストランの空席情報を全文検索システム Solr にインデックスし、予約できる日を Solr で検索して空席状況を取得しています。 Solr には、レストランの設定データごとにドキュメントを作成しインデックスしています。そのため設定データが多いレストランは検索対象のドキュメント数が膨大になり、検索に時間がかかっていました。
やったこと
検索マイクロサービスを経由するのをやめた
Solr へのアクセスは「フロントエンド → バックエンド → 検索マイクロサービス → Solr」という流れで行われます。
従来からあった検索マイクロサービスのオーバーヘッドが大きかったため、検索マイクロサービスを経由するのをやめて、「バックエンド → Solr」に直接アクセスするようにしました。
検索マイクロサービス(C#)で行われていた Solr クエリの組み立てや、Solr からのレスポンスをオブジェクトに変換し在庫計算を行う処理を、バックエンド(Rust)に移行しました。
ワイルドカードを使うようにした
Solr にインデックスされているデータのなかには、日付ごとに異なる情報が含まれています。これらの情報は、それぞれ特定の日付(例:231025)を含むフィールド名で表現されています。
"231025Close_tdt": "2023-10-21T00:00:00Z", "231025VisitTimeFrom_tdt": "2023-10-25T18:00:00Z", "231025VisitTimeTo_tdt": "2023-10-25T18:30:00Z", "231025HasInventory_b": true, "231025HasRotationOrBlockTime_b": false, "231025Inventory_ti": 1, "231025SalesUpperLimitOver_b": false,
例えば、先1か月分の各日付の情報を取得する場合、以下のような Solr クエリを生成していました。
// 変更前 fl=231025Close_tdt,231025VisitTimeFrom_tdt,231025VisitTimeTo_tdt,231025HasInventory_b,231025HasRotationOrBlockTime_b,231025Inventory_ti,231025SalesUpperLimitOver_b,231026Close_tdt,231026VisitTimeFrom_tdt,231026VisitTimeTo_tdt,231026HasInventory_b,231026HasRotationOrBlockTime_b,231026Inventory_ti,231026SalesUpperLimitOver_b,231027Close_tdt,231027VisitTimeFrom_tdt,231027VisitTimeTo_tdt,231027HasInventory_b,231027HasRotationOrBlockTime_b,231027Inventory_ti,231027SalesUpperLimitOver_b,231028Close_tdt,231028VisitTimeFrom_tdt,231028VisitTimeTo_tdt,231028HasInventory_b,231028HasRotationOrBlockTime_b,231028Inventory_ti,231028SalesUpperLimitOver_b,231029Close_tdt,231029VisitTimeFrom_tdt,231029VisitTimeTo_tdt,231029HasInventory_b,231029HasRotationOrBlockTime_b,231029Inventory_ti,231029SalesUpperLimitOver_b,231030Close_tdt,231030VisitTimeFrom_tdt,231030VisitTimeTo_tdt,231030HasInventory_b,231030HasRotationOrBlockTime_b,231030Inventory_ti,231030SalesUpperLimitOver_b...つづく
field list
で「231025のフィールド群,231026のフィールド群,231027のフィールド群 ...」のように、特定の日付が含まれるフィールド群を個別に指定していましたが、日付部分(231025)をワイルドカード(??????)に置き換えて「??????のフィールド群」という書き方に変更しました。
// 変更後 fl=??????Close_tdt,??????VisitTimeFrom_tdt,??????VisitTimeTo_tdt,??????HasInventory_b,??????HasRotationOrBlockTime_b,??????Inventory_ti,??????SalesUpperLimitOver_b
この変更により、設定データが多いレストランではレスポンスタイムが約 1/5 に短縮され、大きな改善効果が得られました!
100件ずつ並列で取得するようにした
最初に、検索結果の総件数のみを取得し、総件数を100で割って何回取得すればよいか判断し、100件ずつ並列で Solr にリクエストを送るようにしました。
Rust で Solr から結果を取得するサンプルコードです。search_calendar
にカレンダーの検索条件を渡すと、まず Solr から総件数を取得し、そのあと100件ずつ検索結果を取得します。
pub async fn search_calendar( &self, input: &model::CalendarInput, ) -> anyhow::Result<Vec<model::Date>> { let rows = 100; let query = CalendarQuery(input.clone()); // 先に総件数のみを取得する let total_count = self.get_solr_data(&query, 0, 0).await?.response.total_count; let query = &query; // 100 件ずつ取得する let futures = (0..total_count.div_ceil(rows)).map(|n| async move { self.get_solr_data(query, n * rows, rows).await }); let res = futures_util::future::try_join_all(futures).await?; // 以下略(Solr の結果をもとに返り値を作る) }
不要な Solr クエリを削る
改めて Solr クエリに削除できる部分がないか見直しました。
- 不要なフィールドを取得していないか
- ユーザーの指定条件によって削除できるフィールドはないか
- 無駄に
group
,sort
の機能を使用していないか
といった観点でチェックを行いました。
成果
この改善により、カレンダーの空席状況を取得するのに15秒程かかっていたのが2~3秒程度に短縮され、スムーズな UX をユーザーに提供することができました!
また、システム観点でも大きな効果がありました。 今回の速度改善対象は、スマートフォン用 レストラン詳細ページのカレンダーの検索処理でしたが、Solr 全体のパフォーマンスが向上しました。
- Solr の CPU コア使用率が半分以上減少
- Solr 全体のレスポンスタイムが450ミリ秒から200ミリ秒程度に短縮
一休レストランの Solr に関する改善点はまだ多くありますが、少しずつ着実に取り組んでいきたいと思います。
さいごに
一休では、ともに良いサービスをつくっていく仲間を募集中です!
カジュアル面談も実施しているので、お気軽にご応募ください。
*1:Next.js で起きた課題については 一休.com Advent Calendar 2023 15日目の記事で解説予定です。