Ari-Press

エンジニアAriのブログ.

Solrを使ったAuto Completeに挑戦

Solr4.4

以前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の設定例は以下のような感じです。

1
<filter class="solr.EdgeNGramFilterFactory" minGramSize="2" maxGramSize="15" side="front"/>

どんな動作をするのか?はたぶん実例が一番わかりやすいと思うので、文字列 `solr.EdgeNGramFilterFactory' に適用してみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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を定義してみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<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つ定義しているfieldTypetext_ja_start_withtext_start_withで全く同じ定義をしています(つまり冗長。直したいですね。。アイデアをください)。

定義の内容を抜き出すと、以下のようになっています。

1
2
3
4
5
6
7
8
9
10
11
<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_withtext_start_withで違う定義をしています。

定義を分けた理由はsolr.JapaneseReadingFormFilterFactoryです。 このFilterはどうやら形態素解析後の各トークンの読みがなフィールドにアクセスしているだけで、自分で解析などをするわけではない?みたいなので(ちょびっとだけソースも覗いた)、solr.JapaneseTokenizerFactoryなどで形態素解析した後にしか使えなさそうでした。

今回想像していたAuto Completeは

  • おいお追いオリーブオイルがヒットする
  • 追い追いオリーブオイルがヒットする
  • オリーブ追いオリーブオイルがヒットする

といったものだったので、おいおのような未知語を左側からNGram化したり追いを読みがなに変換したりする必要があったわけです。

ここで言うおいおをカバーするfieldTypetext_start_withで、追いをカバーするのがtext_ja_start_withだったりします。ちなみにオリーブはどちらでもいけそうな気がします。

text_start_withanalyzer type="query"

これはスペース区切りで入力されたクエリを左側からNGram化していくものですね。

とりあえず定義を再掲します。

1
2
3
4
5
6
7
<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_withanalyzer type="query"

日本語形態素解析ベースでマッチさせるためのものです。漢字変換をした後の単語でもAuto Completeされることを目指しています。

これも定義を再掲。

1
2
3
4
5
6
7
8
9
10
<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に重みを付けた検索が妥当かなと思います。こちらの方が精度も高いはずなので、自然かなと。

あとはそれなりの量のインデックスを作ってクエリを投げてみて〜といった感じで改善をしていけば実用レベルになりそうな気がしています。というか「マッチ率が高いものが上に来るのさ!」という割り切りもアリかなと個人的には思ってます。はい。

こんな感じですね!

Comments