はじめまして!!はじめまして!! 検索とかその道に明るい moriyama です。
過去にこちらとかこちらで取り上げた通り、いこーよではApache Solrを使っています。
今回は、ひっそり行ったサイト内の検索精度を向上させたやり方を紹介します。
現状の問題点
■index-time boost
フィールドのブースト(重み付け)に index-time boost
を採用していましたが、 Lucene/Solr 7.0
から無効化されましたね。
いろいろなサイトでも詳しく説明されていますが、個人的にブーストするのはクエリ発行時で十分だと思っています。
インデックス時にブーストすると、(全件取り込みでない限り)インデックスする都度でスコアを変更できるので、様々なブースト値が適応されたインデックスになってしまいます。例えばSunspotでは下記の :boost
キー値を変更してリリースするだけで、ブースト値が混在したインデックスが用意に生成できてしまいますね!
Model.searchable do text(:name, :boost => 1.5) end
■日本語の対応
もともと "形態素解析" を行う JapaneseTokenizerFactory
を使った *_text型
のダイナミックフィールドを使用しているのですが、固有名詞での検索結果に適合感がありませんでした。
例えば、 "いちご狩りにいこーよ
" という文字列について考えてみましょう。
JapaneseTokenizerFactory
を活用した弊社のSolrを使うと、下記の様なトークンがインデックスされます。
このままではいくつか問題が発生します。
- 助詞の
に
やよ
が消える いこーよ
という固有名詞が "いく
,ー
,(空文字)
" といった意図しないトークンになる- 日本語特有の表記揺れやタイポに対応できない
上記を解決する手法の一つに、ユーザー辞書を使う手段があります。
これは一般的でないユニークな単語を辞書に追加することで、形態素解析される結果を制御できます。
例えば いこーよ
を登録しておけば、 通常は"いく
, ー
, (空文字)
" とトークナイズされるところ、そのままの語で "いこーよ
" としてトークナイズできたりするわけですね。
ですが、この手法はあまりおすすめできません。 いこーよの様に、たくさんのコンテンツやドキュメントを検索するサイトでは、単語単語を逐一、辞書登録していては辞書ファイルが膨大になり、後々に管理しきれなくなります。 またSolrのインデックスデータに対して更新頻度が高まり、高負荷になりがちです。 そのため、あえて活用するのであれば "略語" や "サービス特有な語" など、登録する単語の運用ルールを徹底して登録数は限定するべきでしょう。
解決策
■index-time boost撤去
Lucene/Solr 7.0
で無効化された事実もあり、またインデックスする都度でブースト値が混在するのを恐れ、 index-time boost
を撤去してインデックスし直しました。
■型の追加
既存の JapaneseTokenizerFactory
の他に、新しく NGramTokenizerFactory
を導入して、 *_ngram型
というダイナミックフィールドを追加しました。
上記と同様に "いちご狩りにいこーよ
" を弊社Solrを使って、下記の様なトークンをインデックスさせました。
インデックスの量は増えますが、下記のメリットがありますね!
- 品詞に依らず、固有名詞もそのままの文字列、順序でインデックスされる
- 日本語特有の表記揺れ(例えば、送り仮名の有無)にも対応できる
いちご狩り
→ "いち
,ちご
,ご狩
,狩り
" の4トークンでhitいちご狩
→ "いち
,ちご
,ご狩
" の3トークンでhit
- 上記同様にタイポへ適応できる
もちろんデメリットも生まれます。 * トークンが増えるので、ノイズになりやすい * インデックスサイズが増える
このメリットを活かしつつ、デメリットを薄めるのが次の策になります。
■QueryBoostの適応
型を追加するだけでは、インデックスされません。
新しい型なので、Sunspotを使って、Railsから NGramTokenizerFactory
の型にインデクシングさせるために、もろもろ設定が必要ですね。
まずは config/initializers/sunspot.rb
で拡張しましょう!
module Sunspot::Type class NgramType < AbstractType def indexed_name(name) "#{name}_ngram" end def to_indexed(value) value.to_s if value end def cast(text) text end end end
そしてmodel側でngram型を認識させます。
Model.searchable do text(:name) # solr に name_text フィールド が宣言される ngram(:name) # solr に name_ngram フィールド が宣言される end
さあ、これが今回のキモです。
NGramTokenizerFactory
のノイズを軽減させるため、検索時にname_text
と name_ngram
にそれぞれブーストをかけます。
下記の例は、 name_text
に15倍、 name_ngram
に5倍の倍率でブーストをかける記述です。
Model.search do fulltext(name) adjust_solr_params do |params| params[:qf] = 'name_text^15 name_ngram^5' end end
各フィールドのブースト値を調整することで、検索結果の適合感を直接的に調整しています。
上記例では *_text型
は *_ngram型
の3倍ブーストをかけていますが、フィールドに投入しているデータ内容によって倍率を調整するべきでしょう。
こと細かな調整は、実際に発行されるSolrのURLにdebug=true
を入れることで、スコアを確認できます。トークン数やブースト倍率でどのように計算されるか確認もできます。
いこーよの検索は、こういった細かな調整を施してできています!!!
さいごに
様々な試行錯誤を繰り返しながら、一緒にいこーよを育ててくれるエンジニアを募集しております。 興味のある方はぜひこちらから!