スタフェスドベカレ2024#3 検索機能がクソ遅かったのでOpenSearchに移行した話
はじめに
おはこんばんちは!スターフェスティバル株式会社 エンジニアのDPon です!
アドベントカレンダー3日目張り切ってやっていきます!!!
クソ重い検索機能をどうにかしたい!
とあるECサイトの検索機能が「使えなくはないけどめちゃくちゃストレスフル」な状態でした。
実際にバックエンドのパフォーマンスを New Relic の APM で確認したところ、検索処理に中央値で1.9秒かかっていることがわかりました。
※New Relic は、アプリケーションのパフォーマンスを監視するためのツールです。
Webアプリケーションのパフォーマンスを可視化し、問題の特定に役立ちます。
実際のサイト上でも目に見えて遅く、使い勝手が良い状態ではありませんでした。
当該アプリケーションの背景
このアプリケーション、社内リソースの関係から専任のエンジニアがついておらず、機能追加がその場しのぎで積み重なってきた歴史があります。
そのため検索に関わるロジック全体が複雑化している状態でした。
ひとまず改善を試みる
New Relic のデータを参考に時間がかかっているクエリを特定し、該当箇所のクエリの修正やINDEXの追加を試しました。
多少の改善もありましたが効果は少なく、根本的な解決にはロジック全体を見直す必要がありそうでした。
大きく手を入れるなら OpenSearch に移行しよう
既存の構成のままロジックを作り変えるのも相応に時間はかかるため、同様に時間がかかるなら OpenSearch に移行してしまおうと考えました。
OpenSearch は、AWSが提供するオープンソースの検索および分析エンジンです。
大量のデータを効率よく検索・集計することができECサイトの検索機能にもよく利用されています。
幸い弊社の主要プロダクトであるごちクルの検索機能で OpenSearch を採用していた実績がありました。
今回の検索機能もこれを活用すれば改善できる可能性が高いと考え、移行を決断しました。
バージョンの問題
ごちクルも当該アプリケーションもPHPで開発されており、ごちクルの実装を流用できるだろうと考えていました。
OpenSearch公式のクライアントをごちクル同様に利用しようとしたところ、問題が発生しました。
ごちクルはPHP8.2であったため問題なくクライアントは利用できましたが、当該アプリケーションはそのままで利用することができませんでした。
冒頭で述べたとおり長年専任がついていなかったのもあり十分にバージョンアップのメンテが行われておらず、PHP7.1と古いバージョンで稼働していたためです。
少しバージョンを遡っても、PHP7.3が最低要件でした。
このタイミングでPHPのバージョンを上げることも検討しましたが
- マイナーバージョンとはいえテストが十分整備されておらずリスクもある
- パフォーマンス改善が急務であり今回のスコープからも外れる
というのもあり、今回は、PHP 7.1でも動作するOpenSearchクライアントを自作 して乗り切ることにしました。
検索条件のデータを事前に準備する
検索に必要となる条件は、多岐にわたります。
- 商品のお届けエリア
- お届け予定日
- 予算
- 料理のジャンル(和洋中、サンドイッチ、寿司、焼き肉etc)
- メイン食材(魚、肉、野菜etc)
- その他オプション(無料ドリンク付き、インボイス登録店舗etc)
- フリーワード
既存処理では関連するテーブルを検索のリクエストがある毎に必要なデータをJOINされており、またツギハギでの実装でもあったため処理の重複も多い状態でした。
今回OpenSearchに移行にあたり毎時実行のバッチ処理で、DB -> OpenSearchに事前に検索に必要となるデータを結合した状態で投入しました。
バッチ処理自体は既にごちクルで実装されていたものがあったため、そちらに乗っかる形でスムーズに実装することができました。
バッチ処理のスケジューリング
ルールの作成
バッチのスケジューリングについては、Amazon EventBridge を利用し、毎時実行するように設定しました。
{
"Name": "create-search-index-rule",
"ScheduleExpression": "cron(30 * * * ? *)",
"State": "ENABLED",
"Description": "毎時30分に、OpenSearchに検索用商品のインデックスを作成する"
}
aws events put-rule --cli-input-json file://{ルールのjsonファイルパス} --profile {AWSプロファイル名}
ターゲットの設定
バッチ処理がGoで実装されておりECS Fargateで稼働しています。
先程のルールと紐づけるため、ターゲットを設定します。
詳細は割愛しますが、GitHub Actionsでワークフローが組まれており下記のようなjsonがテンプレートを元に作成されます。
{
"Rule": "create-search-index-rule",
"Targets": [
{
"Id": "create-search-index",
"Arn": "arn:aws:ecs:ap-northeast-1:123456789012:cluster/CREATE-SEARCH-INDEX-CLUSTER",
"RoleArn": "arn:aws:iam::123456789012:role/CREATE-SEARCH-INDEX-ECSEventsRole",
"EcsParameters": {
"TaskDefinitionArn": "<TASK_DEFINITION>",
"TaskCount": 1,
"LaunchType": "FARGATE",
"PlatformVersion": "1.4.0",
"NetworkConfiguration": {
"awsvpcConfiguration": {
"Subnets": ["subnet-1234567890abcdefg"],
"SecurityGroups": ["sg-1234567890abcdefg", "sg-1234567890abcdefg"],
"AssignPublicIp": "DISABLED"
}
}
}
}
]
}
作成後、以下コマンドでターゲットの設定が行われます
aws events put-targets --cli-input-json file://{作成したjson}
これにて、毎時30分にOpenSearchに検索用商品のインデックスを作成するECSタスクが起動するよう設定されました。
フリーワード検索の機能改善
元々フリーワード検索はMySQLのLIKE検索で処理されていました。
そのため曖昧な検索(ごはん -> ご飯)などは対応できていませんでした。
OpenSearchでは、検索を行う際に Text analysisという仕組みを活用できます。
Text Analysisとは、検索対象の文字列を細かく分解(トークン化)し、検索の柔軟性や精度を向上させるための処理を指します。
トークン化
「焼きそば定食」という文字列を「焼き」「そば」「定食」に分割。
正規化
全角・半角やひらがな・カタカナの違いを統一。たとえば「コーヒー」「コーヒー」「こーひー」を同じものとして扱います。
以下一部コードから抜粋。
var UserDictionaryKuromojiWords = []string{
"唐揚げ,唐揚げ,カラアゲ,名詞",
"からあげ,からあげ,カラアゲ,名詞",
"から揚げ,から揚げ,カラアゲ,名詞",
同義語処理
「ごはん」と「ご飯」、「スパゲティ」と「パスタ」のような同じ意味を持つ単語を同義語として登録しておけば、検索時に同時にヒットします。
以下一部コードから抜粋。
"synonym_filter": map[string]interface{}{
"type": "synonym",
"synonyms": []string{
"ウナギ,ウナジュウ",
"ポー玉,ポーク玉子",
日本語の検索に特化した形態素解析ライブラリである kuromoji を利用し、これらの処理を行いました。
これにより、フリーワード検索の精度が向上し、ユーザーが求める情報により近いものを提供できるようになりました。
結果どうなった
リリース後改めて New Relic を確認したところ中央値 1.9sec となっていたのが 0.9sec まで改善されていることが確認できました🙌
実際サイトにアクセスした際も体感できるレベルでの速度改善となりました。
余談
当該アプリケーションは専任がいない状況も長くなかなか改善できていなかったのもあり、普段やりとりのなかった営業の方などからも直接感謝の声をいただけたのが印象的でした。
ユーザーだけに留まらず喜んでもらえることができて何よりなお仕事でした。
採用やってます
スターフェスティバルではともに働く仲間を募集しております!
まだまだ社内には課題だらけで、いろんなチャレンジができる環境にあります。チャンスもいっぱいです!
お気軽にカジュアル面談のご相談ください!!!ウェルウェルカムカム!!!!!
Discussion