おいしい健康 開発者ブログ

株式会社おいしい健康で働くエンジニア・デザイナーが社内の様子をお伝えします。

Apache Solrでサジェスト機能を実装する

こんにちは。おいしい健康に入社して2ヶ月目の44akiです。

今回、おいしい健康のアプリで検索窓の入力サジェスト機能を導入する機会があり、 Apache SolrのSpellCheckComponentを使って実現しました。 おいしい健康のサービス全体でいえば小さな機能ではありますが、前職では検索エンジンベンダーでエンジニアをやっていたので、こういうところには無駄に拘ってしまいます。

この記事の内容は、Apache Solrのバージョン4.10.4で動作確認をしています。

f:id:oishi-kenko:20171107173455p:plain

候補語のデータについて

入力サジェスト機能を実現する場合、その候補語となるデータの取得元は大きく分けて2つあり、それぞれにメリット、デメリットがあります。

  1. 検索対象のデータから形態素解析で取得する
    検索対象のデータをmecab等の形態素解析エンジンで分かち書きして、候補語のデータとして使用する方法です。おいしい健康の場合はレシピ情報がこのデータにあたります。検索対象データを形態素解析して取得した候補語のため、必ず検索にヒットする候補語が取得できるというメリットがありますが、形態素解析エンジンの辞書データをもとに分かち書きされるため、辞書データの精度が低いと、候補語が名詞の途中で途切れたり、意図しない候補語が表示される可能性があります。

  2. 検索ログから取得する
    ユーザが実際に検索したキーワードをログとして保存しておき、候補語のデータとして使用する方法です。検索ログから候補語を取得しているため、ユーザーが実際の入力するようなキーワードをサジェストすることが出来ます。また、過去1ヶ月間の検索ログなど、集計対象の期間を絞ることで、キーワードのトレンドを反映し、秋ならサンマ、冬に近づくとサバを候補語の上位に表示するなどが実現できるようになります。ただし、検索ログベースのため、検索ログを取得する環境を構築する必要がある、検索にヒットしない候補語を除外する必要がある等、実装のコストは増えてしまいます。

今回は2の検索ログベースでサジェスト機能を実装しました。

schema.xml

先ずはApache Solrのスキーマを定義していきます。
候補語の表示用として name フィールドを定義します。

<fields>
    <field name="_version_" type="long" indexed="true" stored="true"/>
    <field name="id" type="string" multiValued="false" stored="true" indexed="true"/>
    <field name="name" type="string" multiValued="false" stored="true" indexed="true"/>
</fields>

また、検索用に text_ja_romaji の型を定義しています。 ローマ字に変換して検索を行うことで、入力途中のキーワードでもサジェストが行えるようにしています。

<fieldType name="text_ja_romaji" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
  <analyzer>
    <tokenizer class="solr.JapaneseTokenizerFactory" mode="normal"/>
    <filter class="solr.CJKWidthFilterFactory"/>
    <filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="true"/>
    <filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

全体的な流れとしては、 JapaneseTokenizerFactory でKuromojiによる形態素解析を行い、 JapaneseReadingFormFilterFactory のオプションでuseRomajiを指定することで形態素解析で取得した読みをローマ字に変換します。最後に ShingleFilterFactory により形態素解析で分割された文字列を連結します。

ローマ字変換のイメージ>
おいしい健康→おいしい 健康→oishi kenko→oishikenko

solrconfig.xml

次にサジェスト用のsearchComponentとrequestHandlerを定義します。

今回はSolrの solr.SpellCheckComponent を使います。

<!-- Auto Complete component -->
<searchComponent class="solr.SpellCheckComponent" name="autocomplete_ja">
    <lst name="spellchecker">
      <str name="name">autocomplete_ja</str>
      <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
      <str name="lookupImpl">org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory</str>
      <str name="buildOnCommit">true</str>
      <str name="comparatorClass">score</str>
      <str name="field">name</str>
      <str name="suggestAnalyzerFieldType">text_ja_romaji</str>
      <bool name="exactMatchFirst">true</bool>
    </lst>
    <str name="queryAnalyzerFieldType">text_ja_romaji</str>
</searchComponent>

suggestAnalyzerFieldTypeとqueryAnalyzerFieldTypeに先程定義したtext_ja_romajiを指定することでローマ字での前方一致検索を行っています。

comparatorClassで候補語のソート順を指定します。score、freq、もしくは独自で作成したcomparatorClassが指定可能です。今回のように検索ログベースで、検索回数順にソートする場合は、通常はcomparatorClassを作成する必要がありますが、今回は前述のnameフィールドに「キーワード:検索回数」のようなフォーマットでインデックスし、アプリ側で取得したキーワードと検索回数をもとに検索回数順にソートすることで、簡易的に検索回数順ソートを実装しました。

最後に、上記のsearchComponentを「/autocomplete_ja」のパスで受けられるように、requestHandlerを定義します。

<!-- Auto Complete handler -->
<requestHandler name="/autocomplete_ja" class="org.apache.solr.handler.component.SearchHandler">
    <lst name="defaults">
      <str name="spellcheck">true</str>
      <str name="spellcheck.dictionary">autocomplete_ja</str>
      <str name="spellcheck.collate">true</str>
      <str name="spellcheck.count">100</str>
      <str name="spellcheck.onlyMorePopular">true</str>
    </lst>
    <arr name="components">
      <str>autocomplete_ja</str>
    </arr>
</requestHandler>

終わりに

今回はApache Solrの定義の部分のみの説明でしたが、実際には、Fluentd、BigQueryによる検索ログ取得、検索にヒットしない候補語の削除等も行いました。Apache SolrのSpellCheckComponentを使うことで比較的簡単に入力サジェストの実装を行うことができました。