以前Solr4.0の記事を書いてからはや半年以上、いつの間にやらSolrが4.4になっていました。追いつけたもんじゃない。
Solr関係の調べ事をしているとSolr Wikiをよく見ることになるんですが、その中でsolr.EdgeNGramFilterFactoryがずっと気になっていたわけです。これを使えばいい感じのAuto Completeができるんじゃないか?というかそのために作られたようなFilterにしか見えない!という感じで。
そんなわけで、このsolr.EdgeNGramFilterFactory
を使って日本語対応のAuto Completeを実装してみました。
簡単な動作確認しかしていませんが、うまく動いているような・・・?どうでしょう。
以下は全てSolr4.4での実装メモです。
solr.EdgeNGramFilterFactory
について
まずこのsolr.EdgeNGramFilterFactory
がどのようなものかというと、各トークンを片側から切り刻んでNGram化するものです。
Solr Wikiの設定例は以下のような感じです。
<filter class="solr.EdgeNGramFilterFactory" minGramSize="2" maxGramSize="15" side="front"/>
どんな動作をするのか?はたぶん実例が一番わかりやすいと思うので、文字列 `solr.EdgeNGramFilterFactory' に適用してみました。
solr.EdgeNGramFilterFactory
|-> so
|-> sol
|-> solr
|-> solr.
|-> solr.E
|-> solr.Ed
|-> solr.Edg
|-> solr.Edge
|-> solr.EdgeN
|-> solr.EdgeNG
|-> solr.EdgeNGr
|-> solr.EdgeNGra
|-> solr.EdgeNGram
|-> solr.EdgeNGramF
こんな感じで文字列を左(front)から2文字、3文字、・・・、15文字と区切ってくれるようです。
やっぱりAuto Complete向きな気がしますね。
設定例
そんなわけで、日本語対応の前方一致フィールド用のfieldType
を定義してみました。
<fieldType name="text_ja_start_with" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="25" side="front"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
<fieldType name="text_start_with" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="25" side="front"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="25" side="front"/>
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
いやー、長いですね!
ちょっと動作上の都合で1つのfieldType
にまとめきれず、2つ定義しています(これ1つでいけるよ!という設定例があれば教えてください。。)。
とりあえずレビューも兼ねて1つずつ見てみることにします。
analyzer type="index"
まずtype="index"
属性のついたanalyzer
要素ですが、実は2つ定義しているfieldType
のtext_ja_start_with
とtext_start_with
で全く同じ定義をしています(つまり冗長。直したいですね。。アイデアをください)。
定義の内容を抜き出すと、以下のようになっています。
<analyzer type="index">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="25" side="front"/>
</analyzer>
ちょっとfilter
が多いので箇条書きにします。
-
solr.JapaneseTokenizerFactory
で分かち書き-
すもももももももものうち
がすもも
も
もも
も
もも
の
うち
になるようなアレですね
-
-
solr.JapanesePartOfSpeechStopFilterFactory
で余計な品詞を落とす- 接続詞とかが前方一致でマッチしてきたら嫌かなあということで
-
solr.StopFilterFactory
で余計な単語を落とす- 一応入れていますが、デフォルトの辞書はほぼコメントアウトしています
- 例えば「これは酷い」の「これ」がインデックスされなくなったりするので、ひらがな1文字や放送禁止用語などのみを落とすとか?
-
solr.CJKWidthFilterFactory
で半角カナと全角カナの表記ゆれを統一- どちらでもマッチしてほしいですよね
-
solr.JapaneseKatakanaStemFilterFactory
でカタカナ末尾の長音を統一- あってもなくてもな感じですが、「コンピューター」と入力して「コンピュータ」がAuto Completeされてもいいですよね
-
solr.JapaneseReadingFormFilterFactory
で漢字を読みに変換- これのおかげで
fieldType
が2つになった気が(すぐ下に書いてます)
- これのおかげで
-
solr.ICUTransformFilterFactory
でカタカナひらがなを統一- ひらがなで検索してもカタカナで検索してもヒットしてほしい
-
solr.LowerCaseFilterFactory
でアルファベットを小文字化- ケースインセンシティブの方が嬉しいかなと
-
solr.EdgeNGramFilterFactory
でトークンをNGram化- 今回の主役!
このanalyzer
を通すと、例文追いオリーブオイル
は追い
オリーブ
オイル
に分かち書きされ、最終的にお
おい
お
おり
おりい
おりいぶ
お
おい
おいる
という形でインデックスされます。
analyzer type="query"
次にtype="query"
属性のついたanalyzer
要素ですが、これはtext_ja_start_with
とtext_start_with
で違う定義をしています。
定義を分けた理由はsolr.JapaneseReadingFormFilterFactory
です。
このFilterはどうやら形態素解析後の各トークンの読みがなフィールドにアクセスしているだけで、自分で解析などをするわけではない?みたいなので(ちょびっとだけソースも覗いた)、solr.JapaneseTokenizerFactory
などで形態素解析した後にしか使えなさそうでした。
今回想像していたAuto Completeは
-
おいお
で追いオリーブオイル
がヒットする -
追い
で追いオリーブオイル
がヒットする -
オリーブ
で追いオリーブオイル
がヒットする
といったものだったので、おいお
のような未知語を左側からNGram化したり追い
を読みがなに変換したりする必要があったわけです。
ここで言うおいお
をカバーするfieldType
がtext_start_with
で、追い
をカバーするのがtext_ja_start_with
だったりします。ちなみにオリーブ
はどちらでもいけそうな気がします。
text_start_with
のanalyzer type="query"
これはスペース区切りで入力されたクエリを左側からNGram化していくものですね。
とりあえず定義を再掲します。
<analyzer type="query">
<tokenizer class="solr.WhitespaceTokenizerFactory"/>
<filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="25" side="front"/>
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
この動作を箇条書きすると以下のようになります。
-
solr.WhitespaceTokenizerFactory
で空白区切りで単語を分割- 未知語に対応できる最強の区切り空白で単語を区切ります
-
solr.EdgeNGramFilterFactory
で単語を左側からNGram化- 前方一致のためですね
-
solr.CJKWidthFilterFactory
で半角全角を統一-
analyzer type="index"
と同様
-
-
solr.ICUTransformFilterFactory
でカナの長音を統一-
analyzer type="index"
と同様
-
-
solr.LowerCaseFilterFactory
でケースインセンシティブ化-
analyzer type="index"
と同様
-
このanalyzer
においお
を渡すとお
おい
おいお
のようになり、analyzer type="index"
でインデックス化された追いオリーブオイル
はお
おい
お
お
おい
で一致します。ちょっと例文のせいか色んなところにマッチしてますね。これはいいのか。。
そして追い
だと追
追い
になってヒットしません。
ちなみにオリーブ
はお
おり
おりい
おりいぶ
になって、お
お
おり
おりい
おりいぶ
お
でヒットします。
text_ja_start_with
のanalyzer type="query"
日本語形態素解析ベースでマッチさせるためのものです。漢字変換をした後の単語でもAuto Completeされることを目指しています。
これも定義を再掲。
<analyzer type="query">
<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>
<filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" />
<filter class="solr.CJKWidthFilterFactory"/>
<filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/>
<filter class="solr.JapaneseReadingFormFilterFactory" useRomaji="false"/>
<filter class="solr.ICUTransformFilterFactory" id="Katakana-Hiragana"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
各定義での動作を箇条書きすると以下のようになります。
-
solr.JapaneseTokenizerFactory
で日本語を分かち書き- ちゃんとした単語(漢字変換済みなど)で入力されたものを取り出せる
-
solr.JapanesePartOfSpeechStopFilterFactory
-
analyzer type="index"
と同様
-
-
solr.StopFilterFactory
-
analyzer type="index"
と同様
-
-
solr.CJKWidthFilterFactory
で半角全角を統一-
analyzer type="index"
と同様
-
-
solr.JapaneseKatakanaStemFilterFactory
-
analyzer type="index"
と同様
-
-
solr.JapaneseReadingFormFilterFactory
-
analyzer type="index"
と同様
-
-
solr.ICUTransformFilterFactory
でカナの長音を統一-
analyzer type="index"
と同様
-
-
solr.LowerCaseFilterFactory
でケースインセンシティブ化-
analyzer type="index"
と同様
-
同様が多いのは手抜きではなくて、実際にfilter
の組み方がanalyzer type="index"
と同じためです。実際の差分は一番最後のsolr.EdgeNGramFilterFactory
があるかないかです。
このanalyzer
を追い
に適用するとおい
になって、追いオリーブオイル
のおい
2つにマッチします。
まとめ
こんな感じで思いつきレベルでSolrでのオートコンプリートを実装してみましたが、動きそうな気配はしています。text_start_with
の方のマッチし過ぎに一抹の不安を覚えますが。。
ただ、どちらにせよ日本語でしっかりとマッチするtext_ja_start_with
に重みを付けた検索が妥当かなと思います。こちらの方が精度も高いはずなので、自然かなと。
あとはそれなりの量のインデックスを作ってクエリを投げてみて〜といった感じで改善をしていけば実用レベルになりそうな気がしています。というか「マッチ率が高いものが上に来るのさ!」という割り切りもアリかなと個人的には思ってます。はい。
こんな感じですね!