Apache Solrによる検索サジェスト機能の実現

こんにちは。E・レシピ担当の長谷川です。先日「Apache Solr」を使用した検索サジェスト機能を開発しました。今回は検索サジェスト機能実現のための「Solr」の設定内容などをご紹介します。

Apache Solr

f0364156_14424325.png

まず「Apache Solr」とは、全文検索ライブラリを用いたオープンソースの検索エンジンです。「Solr」は検索式を元にインデックスを検索し、大量のドキュメントから瞬時に検索結果を返します。単純な検索以外にも様々なサーチコンポーネントを持ち、その中の1つに「SpellCheckComponent」があります。このサーチコンポーネントで動作する「Suggester」を用いて、サジェストを実現することができます。

サジェスト

f0364156_15163160.png

次にサジェストとは、検索フォームの入力から検索単語を予測して提案することによって、ユーザーの入力を補完する機能です。これにより、ユーザーのキーワード入力の手間の省略、誤入力防止、検索語の関連ワードの明示などを実現できます。サジェスト導入によって、検索の利便性を上げることで、ユーザーの満足度向上の効果を期待できると考えられます。

Solrによるサジェストフロー

f0364156_17214566.png

「Solr」によるサジェストは以下のフローで実現しています。インデクシングは予め行っておき、フロントは「Ajax」を用いてサーバー側とやりとりします。
  1. (Solr)ドキュメントからサジェストに表示したい単語を含むテキストを取得し、形態素解析などを行い、単語を分割し、インデックスに登録
  2. (Solr)各単語の読みを付加情報として追加
  3. (フロント)ユーザーが検索ワードを入力する度にAjaxを用いて検索ワードをAPIへ送信
  4. (Solr)APIからの検索ワードをローマ字変換し、検索を行い、サジェストする検索ワードを取得し、APIを通して返す
  5. (フロント)サジェスト検索ワードを表示

Solrの設定

では、このフローを踏まえ、Solrの設定ファイル「schema.xml」, 「solrconfig.xml」に設定を追加していきます。

schema.xml

まず、サジェストする単語のフィールドを追加します。このフィールドにおいてインデクシングされる値が実際にサジェストに現れる単語になります。

suggestフィールド

<field name="suggest" omitNorms="true" type="suggest" indexed="true" multiValued="true"/>

ここで、「multiValued」としているのは複数の値(レシピ名や材料名など)をsuggestフィールドに追加しているためですが、必ずしも必要ではありません。

次に、suggestフィールドに設定するフィールドタイプ(suggest)及びローマ字変換用のフィールドタイプを設定します。

suggestフィールドに設定するフィールドタイプ

<fieldType name="suggest" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_suggest_ja.txt"/>
<filter class="solr.SynonymFilterFactory" synonyms="lang/synonyms_suggest_ja.txt" ignoreCase="true" expand="true"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigrams="false" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
<filter class="solr.SynonymFilterFactory" synonyms="lang/synonyms_suggest_ja.txt" ignoreCase="true" expand="true"/>
</analyzer>
</fieldType>

ローマ字変換用のフィールドタイプ

<fieldType name="text_ja_romaji" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="true"/>
<filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigrams="false" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
<filter class="solr.LowerCaseFilterFactory" />
</analyzer>
<analyzer type="query">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="true"/>
<filter class="solr.ICUTransformFilterFactory" id="Hiragana-Katakana"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Latin"/>
<filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigrams="false" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
<filter class="solr.LowerCaseFilterFactory" />
</analyzer>
</fieldType>

* 設定内容は環境やデータに合わせて適宜変更いただければと思います。

「JapaneseReadingFormFilterFactory」と「useRomaji="true"」によって、日本語をローマ字変換しています。しかし、レシピ検索では形態素解析器kuromojiの形態素解析では解析できないカタカナ語が多いため、「ICUTransformFilterFactory」によって、不明な単語もローマ字変換できるように設定しています。
また、「ShingleFilterFactory」を用いることで、形態素解析で入力単語が分割されてしまった場合でもそれらを結合させ、サジェスト単語を検索することができます。例えば、「えb」という入力は「え」「b」に分解され、続いて「e」「b」にローマ字変換されます。そして「ShingleFilterFactory」で結合することで、「eb」が得られ、サジェスト単語を取得することができます。

複数単語への対応

基本的な「schema.xml」の設定はここまでですが、今回のレシピのサジェストでは、サジェストに出す単語を検索履歴を用いず、レシピ名や材料名から取得しています。したがって、複数単語のサジェストでは、1語目の単語と同時に出現した単語を出現頻度が多い順に表示させるようにしました。そこで、複数語対応のために、以下の設定も追加します。

絞り込み検索用フィールド

<field name="suggest_facet" omitNorms="true" type="suggest_facet" indexed="true" multiValued="true"/>

絞り込み検索用フィールドタイプ

<fieldType name="suggest_facet" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false">
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_suggest_ja.txt"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.SynonymFilterFactory" synonyms="lang/synonyms_suggest_ja.txt" ignoreCase="true" expand="true"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="normal" discardPunctuation="false" userDictionary="lang/userdict_suggest_ja.txt"/>
<filter class="solr.CJKWidthFilterFactory" />
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.ShingleFilterFactory" minShingleSize="2" maxShingleSize="99" outputUnigrams="false" outputUnigramsIfNoShingles="true" tokenSeparator=""/>
</analyzer>
</fieldType>

これらの設定を追加し、検索式を工夫する(後述)ことで、入力の1単語目と同時に出てくる単語を絞り込むことができます。この絞り込んだ単語群とサジェスト結果を組み合わせることで2単語目のサジェストを実現させます。もちろん2単語目、3単語目と入力があった場合は、1, 2単語で絞り込んだ3単語目のサジェストを表示します。

solrconfig.xml

続いて、上記で設定したフィールド・フィールドタイプを使用した検索を行うサーチコンポーネント及びリクエストコンポーネントの設定を追加します。

<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="storeDir">autocomplete_ja</str>
<str name="buildOnCommit">true</str>
<str name="comparatorClass">freq</str>
<str name="field">suggest</str>
<str name="suggestAnalyzerFieldType">text_ja_romaji</str>
<bool name="exactMatchFirst">true</bool>
</lst>
<str name="queryAnalyzerFieldType">text_ja_romaji</str>
</searchComponent>
<requestHandler name="/autocomplete_ja" class="org.apache.solr.handler.component.SearchHandler">
<lst name="defaults">
<str name="df">suggest_facet</str>
<str name="omitHeader">true</str>
<str name="rows">0</str>
<str name="facet">true</str>
<str name="facet.field">suggest</str>
<str name="facet.mincount">1</str>
<str name="spellcheck">true</str>
<str name="spellcheck.dictionary">autocomplete_ja</str>
<str name="spellcheck.count">100</str>
<str name="spellcheck.onlyMorePopular">true</str>
<str name="spellcheck.collate">true</str>
</lst>
<arr name="components">
<str>query</str>
<str>facet</str>
<str>autocomplete_ja</str>
</arr>
</requestHandler>

ここで、「SpellCheckComponent」の「Suggester」を用いた検索を設定しています。加えて、複数語のサジェストに対応するための絞り込み検索も同時に実行するようにしています。複数単語の検索の場合は検索式を「A OR (B AND NULL)」とし、A(Aが複数の場合はANDで繋げる)に絞り込みたい単語、Bにサジェストに表示させたい単語の一部を入れることで、複数語に対応できます。「Solr」から検索結果を取得できれば、API部分でそれを加工してフロント側に返すことになります。

まとめ

今回は「Solr」を使った検索サジェスト機能の実現を紹介しました。データ及び入力をローマ字変換することで、日本語のサジェストを実現しました。また、絞り込み検索を用いることで、複数単語のサジェストを可能にしました。こちらは実際にPC版、スマホ版、スゴ得のE・レシピで動作しています。

「Solr」の知識ゼロからのスタートでしたが、最終的にサジェストを実装するにあたり「Solr」の仕組みの理解が深まりました。「Solr」のサジェストに関する参考サイトはあまりなく、今回のケースでの複数語の対応となるとあまり上手く資料が見つからず苦労しましたが、こちらの記事が何かしらヒントになれば幸いです。余計な設定や不具合がまだまだ含まれていると思うので、設定を更に見直したり、アプリなどへの導入なども今後行っていければと考えています。

参考

[改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン(Amazon)

エンジニア募集

エキサイトではエンジニアとして一緒に働いてくださる方を新卒採用と中途採用で募集しています。
詳しくは、こちらの採用情報ページをご覧ください。

[PR]
by ex-engineer | 2016-10-24 11:00