エムスリーエンジニアリンググループ AI・機械学習チームでソフトウェアエンジニアをしている中村(po3rin) です。検索とGoが好きです。
今回は弊社で運用しているメンバーズメディアと言う医療系記事サービスの著者名検索を改善したお話をします。目新しいことはやってませんが、ちょっぴり特殊な対応なので共有します。
人名とニックネームが混じった検索とは
メンバーズメディアというサービスでは専門家が医師向けの記事を執筆しています。著者の中には人気の方もおり、ユーザーが著者の名前で検索されることもあります。
著者には人名(苗字+名前)の方もいればニックネームで登録している人もいます。下記は実際のデータではありませんが、具体的にどのようなデータがあるかを示した例です。
例: 人名系 中村 太郎 中村 清子 例: ニックネーム系 イワシたろう みっつ ABC マーク@新米医師
同じ検索要求を持つサービスの例として、Twitterなどアカウントの名前をユーザーが任意に設定できる場合に発生するサービスが挙げられます。意外にも同じ要件を持つサービスは多いのではないでしょうか?
課題
我々の初期の実装では完全一致のみがサポートされていました。しかし、ユーザーの検索ログを分析すると、ひらがな/カタカナの揺れや、部分文字列、小文字大文字の揺れ、苗字+名前の間のスペースありなしなどがあり、狙った著者がヒットしていませんでした。
ヒットしないクエリ例
クエリ | ヒットしない著者名 | ヒットしない理由 |
---|---|---|
いわし | イワシたろう | ニックネームの一部のクエリ |
マーク | マーク@新米医師 | ニックネームの一部のクエリ |
ミッツ | みっつ | 表記とクエリの揺れ |
abc | ABC | 表記とクエリの揺れ |
中村太郎 | 中村 太郎 | 姓名を区切るかの揺れ |
苗字+名前の人名だけであれば、名前を苗字+名前に分割してindexできます。今回のようにニックネームが混ざっていると汎用的な対処が少し難しいです。
ちなみに人名を苗字+名前に分割するPythonモジュールは下記などが挙げられます。
解決方法
前節で説明した課題を解決するために下記の解決方法を採用しました。
課題 | 解決方法 |
---|---|
ニックネームの一部のクエリ | 部分一致的な検索をする |
表記とクエリの揺れ | ひらがな/カタカナや小文字/大文字を寄せる |
姓名を区切るかの揺れ | 苗字+名前の間のスペースを全体で削除する |
部分一致的な検索をする
ニックネームには任意の単語が使われるため、部分一致的な検索が必要になります。
Elasticsearchの部分一致検索のためにワイルドカード検索が使えます。しかし、短い単語(ex: 医療用語で言う"がん"や"い"など)が検索された場合、意図せず数多くの著者名に当たってしまう可能性があります。そのため今回はn-gremを利用し、n=3を採用しました。そもそも2文字や1文字の場合は完全一致検索が意図されていることが多いのでn-gramでは無視しても問題ないと検索ログから判断しました。
ひらがな/カタカナや小文字/大文字を寄せる
ニックネームをひらがなやカタカナが揺れた状態で検索されていたため、こちらで寄せてあげる必要がありました。Elasticsearchで使えるICU transform token filterを使えばひらがなをカタカナに変換できるので、今回はこれを採用しました。
ICU transform token filter | Elasticsearch Plugins and Integrations [8.8] | Elastic
Indexの設定の例は下記になります。
{ "settings": { "analysis": { "filter": { "to_katakana": { "type": "icu_transform", "id": "Hiragana-Katakana" } } }
苗字+名前の間のスペースを全体で削除する
人名のスペースある/なしで検索にヒットするかしないかが決まってしまっていたため、index前に著者名のスペース削除をおこなっています。
もちろんニックネームにスペースが存在していないことや全角スペースが使かわれていないことを事前に確認しています。これによりスペースの有無関係なく検索にヒットするようになりました。
スコアの調整
n-gramでヒットする検索結果は完全一致より信頼性が低いため、完全一致の場合のスコアはboostする必要があります。下記はqueryの一部抜粋です。検索対象は著者名のフィールドと本文です(本文に著者名が登場するため)。
// ...(省略 { "multi_match": { "fields": [ "body", "body.ngram", "writer_name^100", "writer_name.ngram^10" ], "query": "イワシたろう", "type": "phrase" } }
上記のように著者名の完全一致はスコアをboostしています。writer_name.ngram^10
はwriter_name.ngram
にヒットした場合、スコアが10倍になります。bodyに出てくるフレーズヒットやn-gramヒットのスコアはそのままです。今回は著者名完全一致 > 著者名部分一致 > 本文部分一致
でスコアを調整しています。
まとめ
これで著者名で検索した際にリストの先頭に検索結果がくる理想的な検索を実現できました。今回の改善で下記のような検索がヒットするようになりました。
中村太郎 -> 中村 太郎 いわし -> イワシたろう ミッツ -> みっつ abc -> ABC マーク -> マーク@新米医師
地味な検索改善ですが、この辺は検索の使われ方の理解や、ログを日々眺めることが必要なので、検索エンジニアの腕の見せ所だと思います。
さらなる改善を行う場合
index前にニックネームか人名かを判断して、indexの方法や検索の仕方を分ける方法が考えられます。例えばGiNZA (spaCy)などを使ってNamed Entity Recognitionを行い、人名のラベルがついたものを仕分ける方法などが考えられます。
今回はそこまでの対応はコスト的に不要と判断して実装していませんがより厳密な検索を求める方はトライしてみても良いかもしれません。
We're hiring!
エムスリーではユーザーの検索体験をゴリゴリ改善する仲間を募集しています。我こそはという方はぜひ以下からご応募ください!