皆さん,こんにちは!最近は検索エンジニアとしての仕事がメインの柏木(@asteriam)です.
直近は,検索基盤が整ってきたので,検索エンジンの精度改善の取り組みを行っています.その一環としてゼロ件ヒットの削減に努めていて,今回は「アルファベットの大文字小文字」に対応した話になります.
はじめに
改めて,今回は「検索クエリにおけるアルファベットの大文字小文字に依らない検索結果」を出すための取り組みとして,Character filterを用いて検索クエリを正規化することでこの問題に対処した内容になります.
今回は以下の内容を紹介していこうと思います.
- 取り組みの背景と課題
- Character filterを用いた正規化
- 対応後のゼロ件ヒットの推移
※ 検索エンジンとしてAmazon OpenSearch Serviceを活用しています.これはElasticsearchから派生したOSSの検索エンジンであるOpenSearchのマネージドサービスになります.
目次
取り組みの背景と課題
背景
私たちのチームでは検索結果の改善を行うためにゼロ件ヒットのログを収集しています.僕はそれを毎日眺めているのですが,その中でドキュメントとして存在してそうなワードが含まれているのに何故かゼロ件ヒットとして検索されている回数が多いものがあったため,調査したのが始まりです.
実際のゼロ件ヒットワードの一例ですが,「hpvワクチン」というワードがありました.小文字では検索結果が0件でしたが,これを大文字の「HPVワクチン」で検索するとドキュメントが数十件と返ってくることを確認しました.
このように幾つかのワードで,アルファベットの大文字小文字の違いによるゼロ件ヒットを確認し,中にはゼロ件ヒットワードとして,2~3週間で数百件以上検索されているものもあったので,この問題の解決に当たろうと思いました.
課題
課題としては明確で,アルファベットの大文字と小文字の違いだけで検索結果が変わってしまいユーザーの検索体験がマイナスになっている ということです.そもそもドキュメントが存在しない場合と違って,僕たちのドメインでは普通に使われうるワードでゼロ件ヒットしてしまうことは,検索に対する期待も下がってしまいますし,アプリからの離脱も発生します.
また,この問題は登録しているユーザー辞書による影響もあり,以前からユーザー辞書の整理は必要だと考えられていたため,そこをまとめて整理する良い機会でもありました.
Character filterを用いた正規化
前提理解
この問題を解決するためには,ElasticsearchにおけるAnalyzerの処理の流れを理解する必要があります.
- 0個以上のCharacter filters
- 1個かつ必須のTokenizer
- 0個以上のToken filters
こちらの図がわかりやすいので,引用(Elasticsearch のアナライザをカスタマイズする)させて貰います.
上図では,それぞれどのような動きをしているかというと,
- HTML要素を除去するCharacter filter
- 空白で分割するTokenizer
- 大文字を小文字に変換するToken filter
となっています.
ここで,ユーザー辞書が適用されるのは2番目のTokenizerを行う部分になります.我々が元々設定していたAnalyzerの定義では,Character filterとしてicu_normalizerのみを適用してました.Token filterはTokenizerの後に適用されるため,ユーザー辞書に基づいたトークナイズが優先され,アルファベットの大文字ワードをToken filterで小文字化しても別ワードとして処理される状況でした.このためシノニム辞書を用意していても別ワードとして処理されます(大文字と小文字は別物になってしまいます).
対応方法
この解決方法としては2種類あると思います.
- Character filterを使ってアルファベットの大文字を小文字にする(Character filterによる正規化)
- API側で検索エンジンに入れる前にアルファベットの大文字を小文字にする
そして,辞書に登録するワードは全て小文字で登録する.
今回は,検索されたワードは検索エンジン側で処理が全て完結するようにしたかったため,1番目の方法で対応することにしました.
処理の追加自体はとても簡単で,下のサンプルコードのように,char_filterに新しくフィルターを追加し,そのmappingsに愚直に大文字と小文字の変換を入れるだけになります.
# sample { "settings": { "analysis": { "analyzer": { "my_analyzer": { "tokenizer": "standard", "char_filter": [ "alphabet_mappings_filter" ] } }, "char_filter": { "alphabet_mappings_filter": { "type": "mapping", "mappings": [ "A => a", "B => b", "C => c", "D => d", "E => e", "F => f", "G => g", "H => h", "I => i", "J => j", "K => k", "L => l", "M => m", "N => n", "O => o", "P => p", "Q => q", "R => r", "S => s", "T => t", "U => u", "V => v", "W => w", "X => x", "Y => y", "Z => z" ] } } } } }
あとは,ユーザー辞書とシノニム辞書に登録されているアルファベットを全て小文字に変更することで,大文字と小文字の区別なく検索結果として同じ結果が返ってくるようになります.
1つ注意事項として,今回のようにAnalyzerの定義が変更されて,それに伴って辞書の更新が入る場合は都度インデックスの再作成が必要になるので注意が必要です.
事象としては,辞書が変更されたことにより,元々インデックス化されていたトークンとの整合性が取れずに何もヒットしない状態でした.改めてインデックスを作り替えることで,トークンが正しい状態でインデックス化された状態になりヒットするようになります.
対応後のゼロ件ヒット率の推移
ゼロ件ヒット率の推移を見てみると,下図から今回の対応前後で数値が減少していることがわかります.実際の割合で言うと,減少率は1%以下ではあるものの,検索数ベースだと数百件は減っていることになります.
ここで,ゼロ件ヒット率=全検索数のうち検索結果が0件であった検索数の割合と定義しています.
今回の施策では,他の「検索利用率・1訪問あたり平均検索回数」といったメトリクスには影響は見られませんでした.
そもそもゼロ件ヒットの検索数は全検索数のうち数%しかないため,改善できる幅はそれほど大きくなく,減少率で言うと大きな改善にはなっていませんが,ゼロ件ヒットの上位ワードは今回の対応で改善できたので,よりニーズのあるワードへの対策は実施できたのと,ドキュメントが存在しているのにヒットしないのはユーザー体験にも大きく影響するので,実施したことに価値はあったと考えています.
おわりに
今回はCharacter filterを用いてアルファベットの大文字小文字対応(Character filterによる正規化)を行いました.その結果としてゼロ件ヒットの削減に貢献できました!
今回の改善でも実感しましたが,トークナイズに影響を与える辞書整備は重要で,その整備がまだまだ十分行えていないので,ガッと進めて行きたいと思っています.
また,捨て仮名/中点対応,ひらがな/漢字対応なども今後進めて行く予定です!
千里の道も一歩からということでゼロ件ヒット削減に向けて,今回の取り組みや辞書更新,今後の取り組みを通して,小さな積み重ねでユーザー体験の向上目指して改善して行きます!
最後に,コネヒトではプロダクトを成長させたいMLエンジニアを募集しています!!(切実に募集しています!)
もっと話を聞いてみたい方や,少しでも興味を持たれた方は,ぜひ一度カジュアルにお話させてもらえると嬉しいです.(僕宛@asteriamにTwitterDM経由でご連絡いただいてもOKです!)
また,コネヒトにおける機械学習関連業務の紹介資料も公開していますので,こちらも是非見て下さい!!