こんにちは。エンジニアの永井(shnagai)です。
今回は、現在進めている検索システム内製化プロジェクトの中で、日本語サジェストを実装するために試行錯誤した話を書こうと思います。
内容は、ざっくり下記の構成になっています。
- 日本語サジェストの難しいところ
- よりよい日本語サジェストのために試行錯誤した点
この記事はコネヒトアドベントカレンダー2022の16日目の記事です。
日本語サジェストの難しいところ
日本語のサジェストをOpenSearch(Elasticsearch)で実装するにあたりいくつか難しい点がありました。
この話の前提として、コネヒトではインデックス作成に使えるデータとして下記を持っています。
- 検索ログデータ
- 検索対象のテキストデータ
以後の話はこのデータを使いサジェストを実装する際に、どのような点を意識したかについて話していきます。
①よみがなの考慮
サジェストの実装を考えるに当たり、「赤ちゃん」「妊娠」のような1語として意味をなす入力に対するサジェストは比較的簡単に出来ました。(elastic社のElasticsearchで日本語のサジェストの機能を実装するを参考に実装することでそれなりのベースラインとなるサジェスト機能を作ることが出来ました。良記事に感謝)
ですがそう一筋縄ではいかず、よみがなの部分で苦戦しました。参考実装では、ローマ字入力を考慮し、カナをすべてローマ字にするchar_filterを噛ます実装になっていました。
ママリはスマホアプリなのでローマ字の考慮は不要なのですが、「あかち」「にん」のようなかなの入力途中の語に対して、当然ユーザの検索を補助するためにサジェスト候補を表示する必要があります。
つまり、「赤ちゃん」というワードに対して、「赤ちゃん」「あかちゃん」という2パターンのトークンを持つ必要があります。
後で詳細は述べますが、「あかち」でヒットさせるためにedge_ngramを使いトークンを先頭からn文字という形に分割させることで「赤ちゃん」「あかち」の両方にヒットするような工夫をしています。
また、ユーザの入力途中のワードを分かち書きしてしまうと意図せぬ分割が起こるため、よみがな用のアナライザーを用意する必要があります。(後述)
整理して書くと当たり前だよなって気もするのですが、腑に落ちるまではある程度の時間がかかりました。
②単語の区切り
これはサジェストだけに限った話ではなく日本語の解析全般にいえることなのですが、英語等のスペース区切りで単語が分割される言語と異なり、日本語は単語の区切りが曖昧なためルールベースで単語分割することが難しく、形態素解析が必要になります。
形態素解析することで、単語分割とよみがな振りが出来、トークンとして検索システムで使える形になります。
検索ログ(スペースで単語が区切られた日本語)をデータとして使う場合に、どうトークナイズするのが出したい結果に対してベストなのか色々と試行錯誤しました。
よりよい日本語サジェストのために試行錯誤した点
日本語サジェストの実装にあたっては、先程の紹介した下記のブログが非常に参考になりここで紹介されているマッピング定義をベースに試行錯誤しながらより理想に近づける作業を行いました。
Implementing Japanese autocomplete suggestions in Elasticsearch
ここからは、いくつか理想に近づけるためにチューニングを行った内容を紹介していきます。
これらはまだ本番リリースまではしてないので検証の中で得られた知見の共有という点にご留意ください。
検索ログ活用のために、kuromojiで形態素解析はせずkeywordトークナイザーを使う
既に述べましたが、今回使えるデータとしては下記2つがありました。
- 検索ログデータ
- 検索対象のテキストデータ
検索ログの件数をソートの要素に使いたいという理由から今回検索ログデータを利用してサジェストを実装することにしました。
この検索ログデータですが、「つわり△いつから」「保育園△見学」「義実家」のように、単語もしくはスペース区切りのある程度整ったデータが格納されています。
※ちなみに文中の「つわり△いつから」△はスペースを表しています。
また、データパイプラインで全角/半角スペースの正規化とログデータなのでノイズも多いため、しきい値を設けて特定件数以上のワードをデータとして使うことでノイズ除去しています。
最初このデータを、参考実装に則りkuromojiトークナイザを使い分かち書きする手法で検証を進めていました。
そこで、問題にぶち当たります。
これが一番わかりやすい例で、「ほい」という入力が来たらママリだったら「保育園」をサジェストしたいですが、「ほっけ △いつから」がヒットしました。
※チーム内では「ほっけいつから問題」と言われしばらくホットワードになっていました。
この事象、形態素解析に明るい方なら勘づかれるかもしれませんが、search_analyzerに指定したkuromojiが「ほい」を ほ/い
にトークン化して検索をかけにいくため「保育園」ではなく「ほっけ△いつから」が候補としてヒットしていました。
中々頭で理解するのが難しいので、自分の中で手書きで分解して何が起きてるのかを理解しようと下記のキャプチャのようなことをいくつか繰り返し理解しました。手で書いて整理すると不思議と理解出来て不思議なものです。
この件でとっかかりを掴み、analyzerの設計方針が固まりました。suggest_analyzer(読みではなく入力ワードに対するサジェスト)では、searchとindexでアナライザーを分けているので、その内容も簡単に解説します。
search_analyzer
search_analyzerの役割はユーザの入力文字列を最低限の正規化だけすることです。入力途中の文字が入ってくることが前提なので、形態素解析はせずkeywordトークナイザーを使ってスペースも1語として扱います。
例えば、「つわり△い」のようなワードが来た時に、そのままの形で検索を行うのが役目。出来るだけそのままの形にするという部分が大事なことに気づいたのが大きな転換点でした。
index_analyzer
index_analyzerの役目は、入力データである検索ログを正規化し、edge_ngramを使って入力途中の文字列にもヒットするようなトークンを転置インデックスとして格納することです。
こちらのトークナイザーも最初はkuromojiに始まり、whitespace、textと試し、結果的には検索ログをそのままの形で格納するのが一番良いという結論になりkeywordトークナイザーを使うことにしました。
具体例がわかりやすいと思うので説明すると、
「つわり△いつから」という入力に対して、それぞれ下記のようにトークン化され転置インデックスとして登録されます。
keywordトークナイザーだと つ/つわ/つわり/つわり△/つわり△い/つわり△いつ/つわり△いつか/つわり△いつから
whitespaceトークナイザーだと つ/つわ/つわり/つわり/い/いつ/いつか/いつから
この場合に、サジェスト観点で検証すると明確な差が出ます。
- 「つわり」という入力に対してはどちらもヒットする
- 「つわり△い」という入力に対してはwhitespaceトークナイザーだとヒットしない
サジェストやオートコンプリートと言われる入力補助の機能にとっては、「つわり△い」に対して、「つわり△いつから」「つわり△いつまで」のような補助をしてユーザの検索体験をサポートしていきたいのでkeywordトークナイザーを採用しています。
よみがなで当てるためのアナライザーでは、search_analyzerは同じくkeywordトークナイザーを使い、index_analyzerには、カスタム辞書のくだりでも話したよみがなでのヒットさせる必要があるのでkuromojiトークナイザーを使う形にしました。この当たりの全体像はまた機会があれば書こうと思います。
カスタム辞書にカナを振る
今使っているKuromojiのカスタム辞書は、カナを使う機会がなかったのもありカナ振り作業を後回しにしていました。
ですが、 kuromoji_readingform
filterで単語のよみがなを使う必要が出てきたので、カスタム辞書へのカナ振りが必要になりました。
初期のカスタム辞書を作るフェーズでは、よみがなを使わないケースでの導入だったためその時のツケを払う形だったが中々に骨の折れる作業でした。
# before ママリ公式,ママリ公式,ママリ公式,カスタム名詞 # after ママリ公式,ママリ公式,ママリコウシキ,カスタム名詞
よみがなを振ったおかげで、「ママリ公式」という単語がこのような形で読みでもヒットするようになりました。つまり「ママリこ」という入力に対して、「ママリ公式」がヒットするようになります。
before: マ/ママ/ママリ/ママリ公/ママリ公式
after: マ/ママ/ママリ/ママリコ/ママリコウ/ママリコウシ/ママリコウシキ
※最終的に、カタカナでトークン化するtoken filterも入れているのでトークンはカタカナに
他にもいくつか試行錯誤した点はあるのですが、それらの紹介はまたの機会に取っておきます。記事にすると意外とあっさりした内容になりますが、1つ1つ動作確認しながらのチューニングになるので時間はかなりかかるしやろうと思えばいくらでもチューニング出来るので、そこがこの分野の面白さだなと実感しています。
プロジェクトとしては、ある程度の性能のサジェストを作ることが出来たのでこれからABテストに向かうところです。良い結果が出ることを願いつつここらで筆を置こうと思います。
この試行錯誤のログが日本語サジェストを実装しようとする方の一助になれば幸いです。
明日は @takapy0210による記事です!お楽しみに。
最後にコネヒトでは一緒に働く仲間を募集しています。
今回のテーマの検索システムだけでなく様々な技術要素でチャレンジ出来る環境があるので、少しでも興味を持っていただけたら気軽にご連絡いただければ幸いです。