One experience 検索移行の話

こんにちは、レシピ事業部検索チームのオリギル(@orgil_)です。 先日、この開発者ブログで紹介されたOne experienceプロジェクトによって、クックパッドはプロダクト基盤をグローバル版のシステムに移行しました。私はこのプロジェクトにおいて検索の領域で移行の進行、開発を担当していました。このブログでは検索システムの移行について紹介します。

グローバルのシステムに寄せた理由

One experience によってクックパッドは検索基盤をグローバル側のシステムに移行しました。プロジェクト発足当初、日本とグローバルのどちらのシステムに寄せるかをいろんな観点から検討しました。日本ではSolr、Ruby、ECSなどで動くVoyagerというシステム、グローバルではElasticsearch、Python、k8s、Kafkaなどを用いたglobal-search-v2(通称GS2)という検索システムが動いています。検討の際、どちらのシステムも一長一短で、特段優劣をつけがたいものでした。

検索アルゴリズムに関しては両システムで考え方は一緒でした。辞書ベースでクエリ拡張をし、ドキュメントのタイトルや材料などにマッチスコアを付け、辞書の属性によって細かい調整をするようなスコアリングアルゴリズムは両方にあります。

またドキュメントの検索への反映時間はどちらも同等と呼べるものでした。GS2はKafkaを用いたイベント処理システムを用いて、ほぼ即時反映を実現しています。Voyagerでも定期的な同期をするようになっており、レシピの変更が最短5分で検索結果に反映されるようになっています。下記のブログで触れているように、プロダクトが求めるユーザー体験を実現するためには5分でドキュメントが反映されれば十分であるため、どちらのシステムでも同様な価値を届けられます。

VoyagerがGS2より一番優れていた点は、速度でした。議論時点ではGS2のp50, p95 レスポンスタイムはVoyagerより約4倍ほど遅いものでした。これは大きな懸念要素で、速度は検索システムにとってとても大事な指標です。GS2を採用する際に、日本の大きなトラフィック量が増えることでGS2が更に遅くならないかなどの懸念点はありました。

いろいろと議論しましたが、最終的にはGS2の方に統合することになりました。一番の決め手は多言語対応がVoyagerでは難しかったからです。GS2はすでに約30言語をサポートしていてElasticsearchのAnalyzerを各言語で細かく設定、運用しており、辞書も言語、地域ごとに別れています。この運用をVoyagerでやるのが難しそうでした。

また辞書の扱い方が大きく違いました。日本の検索は長年運用されて、辞書は成熟しており、変更が頻繁に行われないため、辞書の更新は日次バッチで適用されます。しかし、GS2では辞書の変更も即時反映で、すぐに検索結果が変わります。これは辞書の管理を各地域のCM(コミュニティマネージャー)にお願いしていて、彼ら彼女らが辞書を変更しながら日々検索改善を行っているからです。まだ辞書を一から作っている新興マーケットが多かったため、辞書の変更による検索結果の変化を実際に見ながら辞書の作成をしているためです。 GS2採用にあたって速度面での懸念はありましたが、VoyagerがGS2より速い理由はわかっていて、いくつかGS2でも実現できる仕組みがあったため、なんとかできそうだということで進めました。

検索移行の進め方

検索移行のゴールとしては、日本の検索と完全に同じ結果を返すようにすることを目指しました。GS2の検索ロジックの考え方は日本と同じだと言いましたが、実際の計算式は違っていて、検索結果にいろんな差異があります。通常ならABテストなどで、GS2ベースのスコアリングに置き換えても大丈夫か、検証しながら切り替えることも考えられます。しかしOne experience プロジェクトでは検索のみならず、メインのレシピサービスやデータベース、基盤をまるごと移行しているため、一部をユーザーに出して検証することができませんでした。また日本の人気順検索はプレミアムサービスの大事な機能であるため、慎重になる必要があり、システム移行作業とアルゴリズムの大きな改変の検証を同時にやるリソースがありませんでした。GS2で日本専用の検索クエリビルダーを作ることは容易で、大きな技術的な負債にもならないので、まずは同じ結果を再現することに従事しました。

RBO指標

検索結果が同じになっているかを確かめるために、日本とグローバルの結果を比較する必要がありました。今回の移行で、ふたつの検索結果の一致度を測る指標として、Rank Biased Overlap(RBO)という指標を用いました。RBOは2つの順位付きリストの類似性を測定する指標で、特に順位の上位に重点を置きながら、リスト全体の比較を行える特長があります。

RBOを計算する際の上位100件の結果を減衰パラメータ=0.978に設定して測りました。このパラメータは上位60件の一致度でRBO指標の9割の重みが決まるように設定しました。これはレシピ検索においてほとんどのアクセスが最初の3ページの閲覧に収まっているためです。実装のデフォルトである減衰パラメータ=0.9では上位約10レシピだけでRBOスコアが決められてしまい、上位数件の結果が合ってるかどうかに重きが置かれる指標になってしまうからです。

この比較をTop20000キーワードに対して、新着順と人気順の両方で毎日自動で計算するようにしました。日々のRBOの変化、2万キーワードのRBOの分布で進捗を測り、チーム内で RBOが低かったキーワードを分析しながら、移植機能の優先度をつけたり、仕様の見落としを発見しながら進められました。

最初のRBO計測

RBO値とそれに対するキーワードのヒストグラムで移行の進捗が可視化することができました。上の図は一番最初のRBO分布のグラフです。このときはもちろん何もできていないのでほとんどのキーワードがRBO=0.0という結果でした。

初期の開発過程の変化
最初は一番基礎的な検索サポートをしていきました。Elasticsearchで正しいAnalyzerを設定したり、Voyagerのベースの計算式をGS2に実装したり、検索時のクエリを正しく形態素解析するようにしたり、基礎的な部分を作っていきました。この時点でRBO=0.0結果がほとんどなくなり、大部分が右側に寄っている形になりました。
同日の新着順と人気順のRBO分布
またこれは同日の新着順と人気順の分布です。人気順のみの機能などもあることから、両方ともモニターしていく必要がありました。プロジェクト通して人気順のほうがRBO成績は良い傾向にありました。これは新着のレシピドキュメントが日本とグローバルで同期がズレていたり、人気順は人気順スコアの比重が大きいため似やすいことに起因しています。

基礎的な機能の移植が一段落し、プロジェクトの中盤ではRBO値が低いクエリ(0.4以下)を並べて、漏れている機能の発見や、未実装機能の優先度付けを行いながら進めました。難しかったのはRBOが0.6-0.8あたりのキーワードの解析でした。RBOが高いキーワードはもうすでにある程度似ている結果で、差分を探すのが難しいものとなっていました。この辺になってくると単純にアルゴリズムの違いだけではなく、ドキュメント作成時の形態素解析の微妙な差異なども影響してくるため、地道に細かく調査しながら進めました。

RBO分布の推移を可視化してみました。移行が進むごとに分布が右側に寄っていく様子が伺えます。RBOは最終的に新着順で RBO>=0.8 の割合が 97.2%、人気順で RBO >=0.8: 95.1% という結果になりました。いくつかの機能は違う形でもってきたり、Voyagerから持ってきたくない仕様などもありましたが、最終的にはとても高い一致率になりました。ユーザーリリース後もKPIが大きく変化することなく移行できました。

速度

検索移行において日本のシステムと同等な検索結果を出すことと同時に、同等な速度を用意することに注力しました。前述したようにGS2はVoyagerの4倍ほど遅いレスポンスタイムでした。

VoyagerがGS2と比べ速い理由はいくつかわかっていました。一つはクエリ拡張の方式です。辞書を使ったクエリ展開は主に同義語展開と、語の親子関係による拡張があります。日本では synonym token filter を使用して同義語展開を行い、親子関係の単語はインデックス時に事前計算して入れています。そのため検索時は元キーワードのみの検索で同義語展開と親子マッチが可能になっています。その変わりに辞書の変更を反映させるには、日次バッチを待つしかないです。対して、GS2では辞書反映をリアルタイムで実現したいため、クエリ拡張をElasticsearchクエリビルド時に行っているため、ESクエリが肥大化し、検索が重い処理になります。

もう一つの速い理由はSolrを用いた検索基盤が最適化されていたことです。Voyagerでは様々な工夫を凝らしてインデックスサイズを極限まで減らしています。またSolr-hakoを用いたシステムがとても高コスパで高パフォーマンスでした。GS2のElasticsearch構成と比較してクラスタを組んでいないし、Solrノードが1インデックスしか持たないため、クエリキャッシュが効率的に使えます。

GS2移行において、実際の日本のトラフィックに近い負荷試験シナリオを用意してパフォーマンスを計測していきました。前述したSynonym token filterや親子関係の事前計算をGS2にも移行して持ってきたので、GS2の他の言語と比較してすでにある程度速くなってました。その上で、API側、Elasticsearchの構成、インデックス設定などいろいろ模索しながらパフォーマンス改善していきました。GS2にはPrometheus, Grafana を用いたAPM機能が備わっていたので、検索の一連の流れ、クエリ解析、Elasticsearchへのリクエストなどのどこがボトルネックになっているのか明確で、注力する領域を明確にできていました。

またユーザーから見えるWebやアプリの検索ページのパフォーマンスも測定しながら、別のチームの方々が、検索基盤より前にあるシステムの最適化にも積極的に取り組んでいました。

最終的にはGS2はp95ではVoyagerより高速なパフォーマンスになりました。p50のベースのレスポンスタイムは上がりましたが、p95レベルでは日本より安定したレスポンスタイムを実現できました。

  • Voyager -> GS2
  • p50: ~22ms -> ~33ms
  • p95: ~80ms -> ~50ms

さいごに

One experienceはUI、体験が大きく変わり、ユーザーに大きな負担を強いる挑戦でした。ユーザーの方には、検索結果とその応答性は同等なものを提供するように検索チームとして注力して来ました。結果、ほとんどのキーワードでは同等な検索結果を完全にGS2のシステムで用意することができ、レスポンスタイムも損なうことなく検索移行を実現することができました。

検索移行が終わったいま、真のGlobalな検索開発が始まりました。一つのチームで約30言語の検索改善に取り組んでいきます。また日本とGlobalの検索アルゴリズムをなるべく揃えていこうとしています。実際のユーザートラフィックがあるなか、改めてGS2に備わっているABテスト基盤を活用しながら、Global、日本両方のアルゴリズムのいいとこ取りをし、同じアルゴリズムに揃えていきたいと思っています。もちろん言語ごとの差異は残ると思っていて、完全に一緒にするとは思っていませんが、お互いから学べるところは多いはずです。

レシピ検索の詳細な話や検索以外の機能など、ここで書ききれない移行の話はたくさんあるのですが、またいずれどこかの機会に話せたらと思います。また、One experienceプロジェクトについて弊社のTechlifeブログでこれまでに、これからもいろんな記事が投稿されていきます。Techlifeのツイッターで随時更新しています、ぜひチェックしてください。