経緯
現在関わっているサービスでユーザーから「商品のキーワード検索で想定以上の件数がヒットしてしまうので調べて欲しい」という問い合わせがありました。
問い合わせがあった箇所の検索にはElasticsearchを使っていたので、ユーザーからヒアリングした検索条件と実際のデータを見て調査することに。
(なお今回挙げている値はあくまで例なので実際の値ではないです🙏 )
前提
- Elasticsearchのバージョンは5系(最新バージョンでも今回のテーマについては挙動が変わるものではないのでバージョンはそんなに気にしなくて大丈夫です)
- 商品モデルには品番があり文字列を登録出来る
- その品番は部分一致で検索ワードの一致判定を行っている
- 品番のElasticsearchのフィールドはngram_analyzerをデフォルトのまま指定している
検索条件
- ユーザーの検索ワードは「123」
- 登録されている商品の品番は以下の通り
2312
1234
1235
この条件でユーザーが検索を行ってヒットして欲しかった品番はおそらく 1234
と 1235
の2つでしょう。
実際にヒットするものと検索結果が決まったメカニズムについて詳細を見ていきます。
検索結果
- 実際にヒットしたのは
1234
と1235
と2312
の3つ
なぜ 2312
はヒットしたのでしょう?🤔 (全然わからなかった...
結論から言うと ngram_analyzer
のデフォルト設定では、検索キーワードが 最小1文字最大2文字に分割してキーワードのマッチを図るから
です。
ngram_analyzerの文字分割
ElasticsearchのAnalyzer APIをローカルで叩いて文字の分割がどう行われるかを見てみます。
$ curl -X POST 'localhost:9200/product_development/_analyze?pretty' -H "Content-type: application/json" -d '{"analyzer": "ngram_analyzer", "text": "123"}' { "tokens" : [ { "token" : "1", "start_offset" : 0, "end_offset" : 1, "type" : "word", "position" : 0 }, { "token" : "12", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 1 }, { "token" : "2", "start_offset" : 1, "end_offset" : 2, "type" : "word", "position" : 2 }, { "token" : "23", "start_offset" : 1, "end_offset" : 3, "type" : "word", "position" : 3 }, { "token" : "3", "start_offset" : 2, "end_offset" : 3, "type" : "word", "position" : 4 } ] }
これだけだとわかりづらいと思うので詳細を解説していきます。
先ほど、最小1文字最大2文字で分割されると言いました。
ここでは「123」という検索ワードが「1」と「12」と「2」と「23」と「3」に分割されており、実際の検索ではそれらの部分一致でヒットするようになっています。
実際にヒットした 1234
と 1235
と 2312
の3つの品番を見ると以下の条件を満たしています。
- 「1」を含む
- 「12」を含む
- 「2」を含む
- 「23」を含む
- 「3」を含む
今回問い合わせを頂いたユーザーさんの実際のデータはもっと品番の桁数が多い & ngramでの解析なのでヒットしやすく、想定する検索結果以上にヒットしてしまうわけです😇
解決策
ベストな解決策かはわかりませんが、ngramの最低文字数と最大文字数を調整することでチューニングしていく他ないかなと思いました 😓
追記:
そもそも検索のヒット率のスコアリングがなかったから想定していないものが上位にきてしまったので...
スコアリングを追加することで想定通りの順位で検索結果が返るようにする方針でいくことにしました 📝
(その場合も取得される件数は変わらないのですが、取得したいものが上位に来ればユーザーさんの意図通りには動いてることになりそうなので一旦... とはいえ恒久対応は別途やる必要があるかも)
所感
ngramの検索ヒットの仕方をよく理解していなかったためにやらかしましたが、検索ワードをAnalyzerがどう解析するのかの理解が深まったので良い経験でした📝 💦
次はやらかさないようにしたい...