自己紹介

多田拓氏:みなさんこんにちは。本日は「Knowledge derived from search for huge track metadata with Elasticsearch」というタイトルで発表します。

まずは簡単に自己紹介します。私は多田拓といいます。2020年に新卒としてLINEに入社して、開発Tチームでサーバーサイドエンジニアをしています。

業務ではLINE MUSICに関するサービス開発として、音楽レーベル向けサービスを担当しています。バックエンドサーバーのアプリケーションコードを書いたり、Elasticsearchを始めとするインフラの設定をしたりしています。本日はよろしくお願いします。

LINE MUSICにおけるMeta Search APIサーバーとElasticsearchの課題

みなさん知っているかもしれませんが、LINE MUSICはサブスクリプション型の音楽サービスです。特に10代、20代の若いユーザーに支持されており、iOS、Androidをはじめ、Web版も展開しています。

楽曲数は約8,500万曲となっており、これらの楽曲の中からあなたに合った曲をレコメンドしてくれる機能や、LINEアプリのプロフィールに設定する機能を備えています。

みなさんがLINE MUSICで視聴できる音楽は、音楽レーベル会社より提供いただいています。私の開発部署では、音楽レーベル向けに楽曲の納品や、LINE MUSICにおける楽曲の再生情報などを確認できるサービスを開発・運用しています。

例えば、CMSで楽曲情報をWeb UIで閲覧できたり、APIでシステム的に楽曲情報を取得できるようになっています。

これらの音楽レーベル向けのサービスの背後には、楽曲情報を検索するAPIを持つMeta Search APIサーバーが存在します。ユーザーが楽曲検索を行う場合には、このMeta Search APIサーバーにリクエストを送ることでElasticsearchを活用した検索を行い、結果を提供します。

本日は、このMeta Search APIサーバーとElasticsearchに焦点を当て、これらの開発を中心に話していきます。

先ほどのMeta Search APIサーバーが利用しているElasticsearchは、約8,500万件の楽曲データが入っています。LINE MUSICの楽曲数は年々増加しており、ここ1年でも約1,600万もの楽曲が新たに登場しています。

この膨大な楽曲量は、これまで開発を進めていく中で障壁となりました。本発表では、この膨大な楽曲数を扱う検索機能を開発していく上で立ちはだかった問題と、その解決策を事例とともにお話しします。

そして、これらの問題の解決策は楽曲データに限らず、本のタイトルやレストランのメニューなどのデータでも同じように応用可能です。そのため、大量のデータを扱う検索機能開発の知見として、みなさまに共有できたらと思います。

本発表では2つの事例を取り上げます。まずはCMSの検索機能開発の事例を紹介します。

検索手法のアップデート

(スライドを指して)こちらのように、CMSには楽曲の検索フォームが備わっています。青枠で囲まれているフォームに検索ワードを入れると、検索結果として、検索ワードの部分一致でヒットした楽曲が、リスト形式で表示されるようになっています。

この検索機能には機能改善のアップデートがありました。この検索機能は、もともと楽曲名のみの検索しかサポートされていませんでした。しかしそれだと、あるアルバムの楽曲で絞りたい時や、あるアーティストの楽曲で絞りたいなどの要望を満たせませんでした。また、同じ楽曲名が複数存在した時に、さらに他の楽曲情報で絞り込むといった用途についても、実現できませんでした。

そこで、楽曲名だけではなく、正式名称以外の別称や、アルバム名、アーティスト名などの他の項目についても、1つの検索フォームで同時に検索をできるように改善することになりました。

しかし、従来の検索手法のまま項目数を増やしてしまうと、検索にかかる時間が1秒程度であったのが、5秒程度まで増えてしまいました。楽曲を検索するために5秒も待たされるのは、ユーザー体験として良くないです。そのため、検索を高速化するために、まずは従来の検索手法を見直しました。

なぜ従来手法は遅かったのでしょうか。従来は検索項目のデータを、Elasticsearchのデータ型であるキーワード型でデータ格納していました。

キーワード型は単純なデータ型で、データを格納する際に特に何もせず、生の文字列を入れています。そのデータに対して、ワイルドカードクエリというタームレベルクエリの一種を使って検索していました。

ワイルドカードクエリは、その名のとおりワイルドカードを使い、パターンを記述するクエリです。例のように“*bc*”というクエリに対しては、bc、abc、bcd、abcdといった文字列にマッチします。

しかし、公式レファレンスにも記載がありますが、このワイルドカードクエリは、ワイルドカードから始まる、いわゆる後方一致のクエリでは、遅い検索パフォーマンスになってしまうことがあります。今回は、大量のデータを線形探索していくこの検索手法が、根本的な原因でした。

そもそもなぜキーワード型を使っているかというと、この検索機能の初期リリースでは、素早くリリースするために、デリバリーを優先していました。キーワード型に対してテキスト型と呼ばれるデータ型が、Elasticsearchの高速検索を可能にするデータ型としてよく使用されます。

しかし、テキスト型を使うためには、あるまとまりで文字列を分解するトークンナイズの処理が必要で、楽曲データのように短く、固有名詞が含まれる場合には、一般的に使われる辞書ベースのトークンナイズが効果的ではありません。

この例のように、LINE RECORDSのアーティストであるyonkeyは固有名詞だし、同アーティストの楽曲である『Haunter』も、辞書にはない新語や造語に当たる単語になります。このように、あるまとまった意味で単語を区切るのは難しく、うまく扱うための開発には多くの工数が必要でした。

そのため、初期リリースの段階では検索項目が少なく、著しく検索性能が悪かったわけではなかったため、キーワード型を採用することによって、素早く価値を提供することを優先しました。

検索項目を拡大する検索改善に当たって高速化が必要になったため、ここに工数を割いてデータ型をテキスト型に変更し、クエリはフルテキストクエリの1種であるマッチクエリを採用することにしました。

これによって後述する転置インデックスも使用でき、大きなデータに対しても高速で検索可能になります。このマッチクエリを使用するためには、課題であったトークンナイズをどうするか考える必要がありました。

我々は辞書ベースのトークンナイズではなく、n−gramによるトークンナイズを使うことにしました。n−gramは文字列をn文字の連続した列に分割する手法です。1−gramであれば、“abcde”はa、b、c、d、eに。2−gramであれば、ab、bc、cd、deに。3−gramであれば、abc、bcd、cdeのように分割されます。

このn−gramでトークンナイズされたデータに対し、マッチクエリを使用した場合はどうなるでしょうか。“tokyo”という文字列は1−gramでt、o、k、y、oに分割されます。これに対しElasticsearchは転置インデックスを作成して、どの文字列がどのドキュメントに含まれているかを表現します。

これによって、検索のたびに転置インデックスから、すべての文字が含まれるドキュメントを探すのみになるため、余計なドキュメントを操作することなく、高速になります。

しかし、結果として“kyoto”のような、文字の順序が異なるドキュメントも検索結果に含まれることになります。この検索結果では、仕様である部分一致と結果が異なってしまうし、ユーザーにとって意図しない結果が含まれることになってしまいます。

このように、フルテキストクエリでは文字列の順序を保証してくれません。そこで、動的にクエリを変更するようにして、この問題を実用上解消しました。

具体的には、検索文字lengthの大きさによって、n−gramのnを変更します。ドキュメントにあらかじめ1−gram、2−gram、3−gramのデータを作成しておき、検索文字列が1文字であれば1−gram、2文字であれば2−gramの処理をしてから、それぞれの対応するフィールドへマッチさせます。4文字以上についてはドキュメントが大きくなり過ぎてしまうため、3−gramを使って処理をしています。

先ほどの“tokyo”の例で3−gramで処理をすると、“kyoto”はtok、okyを含まないので検索結果に含まれず、期待どおりの検索結果になります。3−gramまでくると、順番が入れ替わって出てくるといったケースはかなりレアになってくるため、文字列上の短いデータでは実用上問題になりにくいです。

今回の検索改善では、テキスト型にしてn−gramと動的クエリを組み合わせることによって、検索性能の高速化を実現できました。

(スライドを指して)改善前のワイルドカードと、改善後のmatchのクエリの速度を比較したデータがこちらです。用いたデータは、本番環境相当の約7,500万曲の楽曲データです。

横軸は検索項目数で、例えばワイルドカードの2項目の時は2つのフィールドに対するワイルドクエリとなっており、matchの4項目の時は4項目に対するmatchのクエリになっています。

改善前の項目数が1つの時は、ワイルドカードクエリでも約1秒程度と実用に耐え得るレスポンス時間でしたが、11項目では4.6秒とかなりレスポンス時間が大きくなっています。対して、マッチクエリは検索項目を増やしていっても11項目でレスポンス時間が0.2秒と、高速な検索性能を示しました。

今回の検索機能開発から得られた知見ですが、まずワイルドカードクエリを使った後方一致を膨大なデータで処理をすると、遅い検索性能になってしまいます。

そのためマッチクエリに変更して高速化することを検討していきたいのですが、楽曲データのように短く、かつ、固有名詞が多いデータは、辞書ベースのトークンナイズをすることが難しい場合があると思います。

そんな時はn−gramと動的クエリを使うことで、文字列の順序を実用上の範囲で保証し、高速化を実現できます。

この検索改善により、検索項目数が一気に増えたことによって検索がしやすくなり、さらに検索速度も向上したためにユーザー体験をよりよいものにできました。

音楽レーベル向けAPI開放事例

ここから音楽レーベル向けのAPI開放の事例と、最後のまとめまで続けて話していきたいと思います。

先ほどのCMSの検索機能の他にも、検索機能をAPIとして開放することになりました。

これまでは音楽レーベルのスタッフがCMSを操作する時にリクエストがくる、といった頻度でリクエストがきてました。

しかし、APIとして開放することになったため、音楽レーベルの持つシステムからリクエストがくることになり、バッチ処理のような、多数のまとまったリクエストに耐えられるようにする必要が出てきました。

システムからのリクエストに耐え得るのか負荷試験を実施しましたが、楽曲情報の検索では負荷に耐えられないことがわかりました。LINE MUSICではミュージックビデオも扱っているので同様に負荷試験を行いましたが、ビデオ情報は問題がなかったため、単純に8,500万曲というデータ量が性能に影響していることがわかりました。

さらに調査を進めるとElasticsearchがボトルネックになっており、ノードのCPUが100パーセントで張りついて、リクエストが詰まってしまうことがわかりました。

最初は検索結果として不必要なデータを取らないようにするなど、検索クエリの工夫を試しました。しかし、性能改善をするようにクエリを洗練しても、楽曲数の多さによって負荷要件を満たすことはできなかったため、インフラの強化に踏み出しました。

Elasticsearchのクラスターには、処理の制御をするマスターノードと、データを持っていて分散的に処理を行うデータノードの2種類があります。このデータノードを増やすことによって、検索処理の複数のノードに分散できるため、検索性能を向上できます。

当たり前ですが、データノードを増やすことはそれだけコストがかかってきます。また、社内のプライベートクラウドであるVerdaから提供されているElasticsearchを利用していますが、ノード数には一定の制限があります。

しかし、約8,500万曲の楽曲量と負荷要件を考慮すると、さらにノードを追加したほうがよいと判断したため、インフラの担当者と相談することによって、何台か増設してもらいました。

ノード数を増やすといっても、みなさんも同じように自由に何台でも増やすのは難しいと思います。

そのため、限られた数のノードを効率的に動作させるために、ShardとReplicaの設定を最適化することにしました。

Shardはデータを水平分割した単位です。A、B、CのShardを3つのデータノードにそれぞれ1つずつ配置をすれば、3台で分散処理できます。

次に、ReplicaはShardを複製する単位です。Primery Shardに対していくつのReplica Shardを複製するかを設定できます。

(スライドを指して)このように、3台のデータノードにReplica Shardを配置すれば可用性を高られます。また、データノードの数やShardの配置によっては、負荷性能の改善に貢献できます。

このように6台のデータノードにShard数が2、Replica数が1で配置をすると、使われないデータノードが2つできてしまいます。そこでReplica数を2にすることによって、すべてのデータノードに分散させて処理できるようになります。そのため「Shard数×(Replica数 + 1)」がデータノード数以上になることを目安に、Shard数とReplica数を決定しています。

このようにReplica数は可用性を高める以外に、分散処理を任せられるデータノードを増やすように設定するのがよいと思います。

しかし、Shard数はデータを水平分割した時の大きさや、クエリによっても性能が変化するため、実際に性能測定をして決定する必要があります。

(スライドを指して)こちらはShard数とReplica数を変えたインデックスを複数用意して負荷試験を行った時の、レスポンス時間の違いを示したグラフです。本番環境相当の約7,500万曲の楽曲データの入った12台のデータノードを使い、400バーチャルユーザーの負荷をかけた時のレスポンス時間を測定したデータです。

少しわかりづらいですが、Shard数が4、Replica数が2の時に、レスポンス時間の95パーセンタイル値が71.70ミリセカンド。Shard数が6、Replica数が1の時のレスポンス時間が65.25ミリセカンドとなっており、Shard数が6の時が測定した中で最もよい性能を示しました。

楽曲データが全体で73.8ギガバイトであるため、1Shard当たり12.3ギガバイト程度に分割し、1リクエストに対して6台で分散処理をするのが検索性能的によいことがわかりました。

現在の本番環境では、18台のデータノードに対して、Shard数を6、Replica数を2に設定して稼働しています。楽曲情報の検索に18台フル稼働で処理をさせたいため、検証環境のReplica数を1から2に増やしています。

また、Replica数が2で可用性としては十分だと判断しているため、更新系のパフォーマンスも考慮して、Replica数を3にするところは今の時点では考えていません。

今回の負荷要件は、これらの設定で満たせることを負荷試験によって確認できたため、リリースから安定稼働ができています。しかしShard数については、データが増えていくと最適なShard数が変化していくため、適宜変更していかなければなりません。

そのため、定期的にベンチマークを行える環境を整備することが重要であると考えており、今回の事例をとおして、適切なShard値がわかったことはもちろん、負荷試験を行える環境を整備できたことも大きな成果であったと感じています。

CMS検索機能改善とAPI開放事例のまとめ

最後にまとめです。CMSの検索機能改善では、キーワード型の後方一致を含んだワイルドカードクエリは性能面に問題があったため、テキスト型のmatchクエリに変更をしました。

しかし、楽曲データのように短文かつ固有名詞が多いデータは、辞書ベースのトークンナイズが難しいためn−gramをベースにし、文字列の順序を動的なクエリによって、実用上保証する手法を取り入れることで解決しました。

次の音楽レーベル向けのAPI開放の事例では、システムからの利用による負荷増加にElasticsearchが耐えられないことがわかったため、データノードの増設をしました。限られたデータノードの中で効率良く動作させるためには、最適なShard数とReplica数を設定することが重要です。

Replica数は可用性の観点の他に、性能面では「Shard数×(Replica数 + 1)」がデータノード数以上になるように設定するのがよいでしょう。

また、Shard数は実際に性能測定をしてみる必要があり、負荷試験ができる環境を整備して測定をすると、負荷要件を満たしているかについても確認できてよいと思います。

発表は以上になります。ありがとうございました。