はじめに
自己紹介
株式会社LITALICOでWEBエンジニアとして働いている @ti_aiuto です。
普段は主に個人向けのWEBサービスの開発のサポートを担当していて、特にモノリスなアプリケーションを持続可能な形に保つことや、データモデリング・データ分析に関わる諸々の整備に関心があります。
最近は技術の面からプロダクト・事業を進化させる仕事にも取り組んでいて、レコメンデーション・全文検索・生成AIの活用といった技術の検証〜導入も行っています。
なんの話か
OpenSearchを使って全文検索とレコメンデーションの基盤整備を行った話です。課題・事前の検討事項からリリース後の後日談も含めて一連の流れをご紹介できればと思います。
プロダクトの紹介
今回の事例はLITALICO発達ナビという「発達が気になるお子さまの保護者や支援者の方に向けたポータルサイト」での事例です。
まず今回の記事に関連する主な機能をご紹介します
「コラム」
社内外のライターが書いた記事を無料で読める機能です。
「Q&A」
会員が投稿した質問に、別の会員が回答できる機能です。
検索機能の課題
そうしたコンテンツを一括検索できる「検索」機能があります。
この機能ですが、次のような課題がありました。
1. パフォーマンス問題による不便な仕様
従来の検索機能はMySQLのLIKE検索を使った簡単な実装だったのですが、実装当初から期間が経過していくうちに、行数が数万〜数十万行規模に増加していました。(それだけたくさんのユーザに使っていただいたと思うと嬉しい悲鳴ではあります。)
その結果、一回のリクエストの所要時間が数秒〜数十秒単位まで増加してしまい、検索ボタンを連打するとサイト全体がダウンしかねない状態になっていました。
数年前に外形監視が一瞬落ちたことをきっかけに、検索対象コンテンツをごく最近のものだけに限定する仕様に変更したため、サイトがダウンするほどの深刻なパフォーマンスの問題は緩和されました。
しかしユーザからすれば、せっかく過去のユーザが投稿したコンテンツが、検索機能でヒットしないという不便な仕様になってしまったため、何らかの対応が望ましい状態でした。
2. 利便性と拡張性の問題
もともとの検索機能は、別で存在する「タイムライン」というフィードが流れる機能をベースに実現されていたのですが、これにより次のような制約が生まれていました。
- フィードに掲載されている部分しか検索対象にならない
- 例えば「コラム」機能では記事本文が検索対象に含まれていませんでした
- コンテンツの親子構造を考慮できない
- 例えば「Q&A」機能では、「質問」に対して複数の「回答」が紐づく形になりますが、これを検索結果のUIでうまく表現できず、質問と回答がそれぞれ単体で・別個に表示されていました
各所からの改善要望
そうした中で、ユーザやカスタマーサポートの担当者から「あるはずのコンテンツがヒットしない」「過去の質問を検索する機能が不十分なことで同じような質問が増えているのではないか」といった声が挙がっていました。
プロダクトマネージャーからも「検索機能の導線をもっと目立つ場所に置いたほうがいいのでは」と提案がありましたが、パフォーマンスの問題により検索機能のアクセス増加を歓迎できない状態だったため、そうした施策も打ちづらい状況にありました。
調査・設計
RDBMS対決
全文検索の改善という点では、社内の他プロダクトでも導入事例のあるElasticsearch/OpenSearchの話題が最初に持ち上がったのですが、近年はRDBMSでも全文検索のためのサポートが充実しているという話があったため、少し寄り道をして試しに使ってみることにしました。
新たな構成要素を導入すればそれだけランニングコスト・メンテナンスの手間は発生するので、構成要素を増やさずにできるアプローチとして何があるのかを知っておくことも重要だという判断です。
実サービスのデータ・ユースケースを想定して、30万件のレコード・14種類の検索クエリを使って実際に検索してみた結果が次の通りです。MySQLではLIKE検索とFULLTEXT INDEX(bigram)を、PostgreSQLではLIKE検索とpg_bigmを使った結果をそれぞれ掲載しています。
最も早かった検索クエリが青色、最も遅かった検索クエリが赤色です。
従来から採用していたMySQLのLIKE検索は最速の場合でも10秒以上かかっていて、やはりこの方式では厳しそうなことが分かります。
PostgreSQLのpg_bigmは最速だと数十ミリ秒で処理できていてとても高速ですが、遅い場合は3秒超かかっていて、思ったよりもブレが大きいようです。
MySQLのFULLTEXT INDEX(bigram)は速いときは速いのですが、遅いときはLIKE検索よりも遅くなってしまっています。この要因として、次の参考記事では余計にヒットした転置インデックスのうち、本当にヒットしているもの(≒検索クエリの順番通り連続してマッチしているもの)を絞り込む部分でパフォーマンスが落ちているのではないか、というような可能性が指摘されています。
結果としては、遅くとも数百msくらいの結果を期待していたのですが、思ったよりも時間がかかることが分かりました。これだけをもってRDBMSだけで頑張ろうという話にはならなさそうです。
OpenSearchのAnalyzer対決
ということでOpenSearchを検証することになりました。OpenSearchを簡単に説明すると、「検索機能を実現するために必要な機能が一式揃った高速・多機能なOSS」と言っていいと思います。
OpenSearchでは、Analyzerというもので検索時の字句の処理方法を設定できます。表記揺れや類義語の考慮、不要な語句の除去など色々やってくれて至れり尽くせりです。
色々な話がある中で、特に重要なのは、検索ワードを小さなカタマリに細切れにする「トークン化」の部分です。全文検索エンジンは、この細切れにした「トークン」同士を検索・比較することで検索を実現しています。
詳しいことは他社さんの記事に素晴らしい解説がたくさんあるので記事末尾の参考記事を参照頂きつつ、ここでは最低限のポイントだけご紹介します。
n-gram
事前に指定した文字数ごとに細切れに分割していく方式です。n=2のときはbigramと言います。
例えば「京都へ行こう」は「京都・都へ・へ行・行こ・こう」のように、「東京都へ行こう」は「東京・京都・都へ・へ行・行こ・こう」のように分割されます。
形態素解析
文法を考慮して品詞ごとに細切れに分割していく方式です。文法を考慮しているので、文法上無視したほうが良い品詞を除去したり、動詞や形容詞などの活用を原形に戻すといった処理をすることもできます。
例えば「京都へ行こう」は「京都・行く」に、「東京都へ行こう」は「東京都・行く」に分割されます。
n-gramと形態素解析の特徴
例えば記事に「東京都」という単語が含まれているとして、「京都」という検索ワードで検索するとします。
このとき、n-gramなら「東京都」は「東京・京都」にトークン化されているので「京都」というトークンにヒットしますし、形態素解析なら「東京都」としてトークン化されているので「京都」というトークンにはヒットしません。
これがヒットして嬉しいか嬉しくないかは場合・状況によりますが、これらの2つの手法を組み合わせることで、自分が望む検索機能の挙動にチューニングすることができるということです。
記事末尾の参考事例では、「両方の手法を併用しつつ、形態素解析でヒットしたアイテム(≒より文意を反映している可能性が高い)をより上位に表示する」という例が紹介されていて、今回の施策でも参考にしています。
比較結果
OpenSearchで実際に形態素解析とn-gramを使用して、先ほどと同じ30万件のレコードを検索した結果です。
今回は、日本語の形態素解析のためにkuromojiとsudachiという2種類のプラグインを導入して比較しています。いずれの場合も、遅くとも数百ミリ秒と十分なパフォーマンスが発揮できています。
実装
導入にあたっての細かい検討
親子構造を考慮した検索
例えば「質問に複数の回答が紐づいている」のような関係を考慮して検索機能を実装することに関しては、OpenSearchの Nested field type Nested query を使うことで、親子関係をデータ構造で表現できることが分かりました。
Ruby/Railsとの統合
アプリケーション本体はRailsを使って実装していますが、Ruby向けにはsearchkickというgemがあります。これについては他プロダクトで既に導入事例があったため、同様にこのgemを活用していくことにしました。
運用方法
OpenSearchをどこでどうやって動かすのか?どうやってモニタリング・メンテナンスしていくのか?という話ですが、これに関しては他プロダクトでAmazon OpenSearch Serviceを使っている先行事例があり、特に問題もなさそうだったので、今回も踏襲することにしました。
コスト試算
検証の段階である程度低いマシンスペックでも十分なパフォーマンスを発揮できることが分かっていたので、複数台の冗長構成を前提にしても十分許容範囲なことが分かりました。
リリース
言ってしまえばただの検索機能なので完成したものもだいたい読者の皆さんの想像通りだとは思うのですが、OpenSearchの機能との関連も踏まえて、改善後の機能のポイントをご紹介します。
複数種別の一括検索
OpenSearchには「インデックス」というRDBでいうテーブルのような概念がありますが、この「インデックス」を「コラム」「Q&A」などのコンテンツごとに最適な内容・範囲・形式で定義した上で、一回の検索クエリで複数種別のコンテンツを各コンテンツに最適な設定で一括検索できるようになりました。
検索処理における親子構造の考慮
Q&Aの質問や回答にヒットしたときに、質問と回答の組み合わせがUI上で分かるような形で表示するように変更しました。(上記のスクリーンショットを参照)
一致箇所のハイライト表示
Highlightという機能を使うことで、検索クエリにヒットした箇所をマーカー表示する機能を実装できました。
メタデータを考慮したスコア調整
同じ条件でヒットしたアイテム同士でも、条件により片方をより優先して表示したいこともあります。
例えば今回の検索機能では、「専門家監修のある記事をより優先して表示したい」という要望があったため、これをFunction Scoreという機能により実現しています。
OpenSearchを活用したレコメンド機能の改善
全く別の話ではあるのですが、OpenSearchの導入により同時に対処した話がもう一つあるので、一緒にご紹介します。
従来の類似コンテンツレコメンドの問題
昨年のアドベントカレンダー記事でご紹介したように、コラム機能のレコメンデーションではEmbeddingを使ったベクトル検索を利用しています。
従来の方式では、ベクトル検索にBigQueryを使っていたのですが、次のような問題がありました。
- 実行時間・コスト・実装方法の課題
- 数千〜数万件のコンテンツの類似コンテンツを総当たりで検索することで、実行時間とコストも少しずつ積み上がっていました
- 実装の工夫によりパフォーマンスを改善できる部分もありましたが、クエリが不格好な実装になってしまいました
- 各環境での開発・動作確認する方法が未整備
- 各環境にベクトルを同期した上で、各環境からベクトル検索を実行する必要がありましたが、そのための手順が十分に整備できていませんでした
- そのため他の機能に同様の施策を展開するための開発も進めづらい状態になっていました
OpenSearchによるBinaryEmbeddingの検索
OpenSearchでは全文検索だけではなく、ベクトル検索の機能も用意されています。特に最近はBinaryEmbeddingという手法も注目されていて、精度をある程度維持したまま、Embeddingの保存容量を96%も削減できてしまいます。
このBinaryEmbeddingの検索がOpenSearchでも実現可能ということで、ベクトル検索の課題については「BinaryEmbeddingをアプリケーションのDBに一緒に格納する」「それをOpenSearchに格納して検索する」というシンプルな方法で解決することにしました。
実装
こちらについても、searchkick gemを活用しながら移行を完了しました。
新方式に切り替えてOpenSearchという新たな構成要素は増えたものの、アプリケーション側の実装がシンプルになったことで、他メンバーへの引き継ぎ・他機能への同様の施策の展開が容易になり、属人化の緩和や開発スピードの向上にもつながったと思います。
その後
リリース後にあった出来事を簡単にご紹介します。
施策の柔軟性の向上
検索機能のパフォーマンスと利便性が向上したことで、サイト内に検索機能へ誘導する導線を設置したり、検索機能の利用を促すような施策を打ちやすくなりました。実際に検索機能を利用するユニークユーザ数は施策前よりも増加しています。
スペック調整
実際にリリースしてみると、検索ページのLatencyが思ったよりも不安定で、大半は数百ミリ秒ほどで処理できるのに、稀に数秒を超えることがありました。色々試したのですが、最終的にはスペック(おそらく主にRAM)不足だったことが分かりました。スペック不足でも全く動かないわけではなくそこそこ動くのは、それはそれでありがたいことではあります。
バージョンアップ
OpenSearchも活発に開発が継続されているソフトウェアなので、最新バージョンが次から次へと出てきます。
何回かバージョンアップ対応を行いましたが、Amazon OpenSearch Serviceのデフォルトで備わっているブルーグリーンデプロイにより、無停止メンテナンスで対応できました。sudachiなど追加で導入しているプラグインも一緒に自動でアップグレードしてくれるため便利です。
最後に
生成AI時代の未来を切り拓く一歩
今回のプロダクトのようなUGC・SNS機能を含むポータルサイトでは、「自分に必要なコンテンツに出会う」「自分と境遇の近いユーザに出会う」といった意味で、検索・マッチングの機能はとても重要だと思います。
さらに昨今の生成AIの時代では、RAGのような仕組みで生成AIと検索システムを連携させたり、記事中でもご紹介したようにレコメンドエンジンにEmbeddingの検索を組み込んだりなど、新技術と従来の検索技術を組み合わせることでできることも広がっています。
今回のOpenSearchの導入により、単なる全文検索機能の改善に留まらず、ユーザが・レコメンドエンジンが・生成AIが、プロダクト内のコンテンツをより高精度で検索できる仕組みを作り、ユーザへのさらなる価値の提供につながるような施策につなげていきたいと思っているところです。実際にOpenSearchによるRAGを使った新たな機能開発のR&Dも進行しており、今後の展開が楽しみです。
参考記事








