アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

サイト内の検索精度を向上させました!

はじめまして!!はじめまして!! 検索とかその道に明るい 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を使うと、下記の様なトークンがインデックスされます。

japanese-tokenizer-factory.png

このままではいくつか問題が発生します。

  • 助詞の が消える
  • いこーよ という固有名詞が "いく, , (空文字)" といった意図しないトークンになる
  • 日本語特有の表記揺れやタイポに対応できない

上記を解決する手法の一つに、ユーザー辞書を使う手段があります。 これは一般的でないユニークな単語を辞書に追加することで、形態素解析される結果を制御できます。 例えば いこーよ を登録しておけば、 通常は"いく, , (空文字)" とトークナイズされるところ、そのままの語で "いこーよ" としてトークナイズできたりするわけですね。

ですが、この手法はあまりおすすめできません。 いこーよの様に、たくさんのコンテンツやドキュメントを検索するサイトでは、単語単語を逐一、辞書登録していては辞書ファイルが膨大になり、後々に管理しきれなくなります。 またSolrのインデックスデータに対して更新頻度が高まり、高負荷になりがちです。 そのため、あえて活用するのであれば "略語" や "サービス特有な語" など、登録する単語の運用ルールを徹底して登録数は限定するべきでしょう。


解決策

■index-time boost撤去

Lucene/Solr 7.0 で無効化された事実もあり、またインデックスする都度でブースト値が混在するのを恐れ、 index-time boost を撤去してインデックスし直しました。

■型の追加

既存の JapaneseTokenizerFactory の他に、新しく NGramTokenizerFactory を導入して、 *_ngram型 というダイナミックフィールドを追加しました。 上記と同様に "いちご狩りにいこーよ" を弊社Solrを使って、下記の様なトークンをインデックスさせました。

n-gram-tokenizer-factory.png

インデックスの量は増えますが、下記のメリットがありますね!

  • 品詞に依らず、固有名詞もそのままの文字列、順序でインデックスされる
  • 日本語特有の表記揺れ(例えば、送り仮名の有無)にも対応できる
    • いちご狩り → "いち, ちご, ご狩, 狩り" の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_textname_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を入れることで、スコアを確認できます。トークン数やブースト倍率でどのように計算されるか確認もできます。


いこーよの検索は、こういった細かな調整を施してできています!!!


さいごに

様々な試行錯誤を繰り返しながら、一緒にいこーよを育ててくれるエンジニアを募集しております。 興味のある方はぜひこちらから!