こんにちは。サイオステクノロジー OSS サポート担当 山本 です。
今回も solr のお話です。
solr スキーマの数多ある “フィルタ” から、比較的効果が実感しやすいと思われる応用フィルタ Synonym (同義語・類義語) についてお話してみようと思います。
■solr の Synonym ってどういうもの?
Synonym は先述のとおり同義語または類義語を示す言葉です。
文字通り、同じような意味の言葉や似たような意味の言葉のことで、
例えば「青」と「ブルー」は同義語ですし、「お皿」と「プレート」は類義語と言えるでしょう。
ところで、改めてですが solr はインデックスという仕組みを使って予め登録しておいた一連のデータであるドキュメントから高速な文字列検索を行うことに特化した、全文検索 OSS です。
そう、solr はインデックスの文字列検索によってドキュメントを検索しています。
なので、当たり前と言えば当たり前ですがデフォルトでは「青」で検索しても「ブルー」のドキュメントは出てきません。
でも、仮に自分が検索する側として考えてみてください。
例えばショッピングサイトで、「カーテン 青」で検索した場合と「カーテン ブルー」で検索した場合の検索結果が全く違っていたら?
それらの検索結果で出てきたもの以外にも、「カーテン 藍色」「カーテン ネイビー」などで検索すると別の青色系カーテンのラインナップがあったりしたら?
きっと不便と言うか機会損失というか、「いや、まとめて出してくれよ…」と思うことでしょう。
solr の Synonym は、こういった同一の検索ワードとして扱ってほしい同義語・類義語の組を予め登録しておくことで、検索ワードの同義語・類義語も一緒に検索できるようにする仕組みです。
Synonym はまた、「パソコン」と「PC」、「パーソナルコンピュータ」といったような略記・略称・通称・正式名称などが混在するドキュメント群をいい感じに検索できるようにするためなどにも使うことができます。
■Synonym を使ってみる
というわけで Synonym を試してみましょう。
今回も前回までで使用した環境 “test-new-core” に Synonym の設定を追加して試していきます。
(前回までの内容は省略するため、ベースとなるコアの作り方などのお話は以前の記事を参照してください。)
Synonym を使うには、Synonym 用の辞書の作成をした上で、Synonym を使用するようにスキーマファイルの設定を変更する必要があります。
■辞書の作成
まずは辞書を作ってみます。
例によってデモ用環境のコア “demo” にサンプルが入っているので、まずはこちらを確認してみましょう。
以下のコマンドで前回までに作成した環境を起動し、サンプルを手元の環境にコピーします。
## (デモ用環境起動) $ podman start test-solr ## (Synonym 辞書サンプルのコピー) $ podman cp test-solr:/var/solr/data/demo/conf/synonyms.txt ./
手元の環境にコピーした Synonym のサンプルファイル “synonyms.txt” を適当なテキストエディタで開き、まずは内容を確認してみます。
行頭が “#” の行はコメント行扱いとなるので、(ファイルの先頭のユーザ辞書の説明コメント部分を除くと) このファイルはこのような記述がされています。
#some test synonym mappings unlikely to appear in real input text aaafoo => aaabar bbbfoo => bbbfoo bbbbar cccfoo => cccbar cccbaz fooaaa,baraaa,bazaaa # Some synonym groups specific to this example GB,gib,gigabyte,gigabytes MB,mib,megabyte,megabytes Television, Televisions, TV, TVs #notice we use "gib" instead of "GiB" so any WordDelimiterGraphFilter coming #after us won't split it into two words. # Synonym mappings can be used for spelling correction too pixima => pixma
ざっくりと 2種類の書式がありそうな、普通のテキストファイルであることがわかりますね。
それぞれの書式についてまず見ておきましょう。
[類義語A](,[類義語B],…) => [類義語C](,[類義語D],…)
まず “=>” があるほうのこちらの書式ですが、[類義語A] (または [類義語B]…) を、[類義語C] (および [類義語D]…) に変換するというものです。
例えば、上記のサンプルファイルの
cccfoo => cccbar cccbaz
は、「cccfoo」が「cccbar」+「cccbaz」に変換されるようにする記述です。(Synonym 処理後、元の「cccfoo」は残りません。)
※ 上記サンプルファイルの抜粋では半角スペース区切りで類義語を列挙していますが、公式ドキュメントの Synonym の書式では “, (カンマ)” で分割するように記載されています。試したバージョンではいずれの記法でも機能しましたが、本記事では “, (カンマ)” 区切りで記載していきます。
つづいてもう片方の書式です。
[類義語E],[類義語F](,[類義語G],…)
“, (カンマ)” 区切りでただ列挙するこちらの書式では、列挙した類義語のいずれかが、列挙した類義語全てに変換されます。
例えば上記サンプルファイルの
GB,gib,gigabyte,gigabytes
は、「GB」「gib」「gigabyte」「gigabytes」のいずれも「GB」+「gib」+「gigabyte」+「gigabytes」に変換されるようにする記述になります。
さて、書き方はなんとなくわかったかと思いますので、試しに何か Synonym を追加してみましょう。
今回は果物の名前で試してみることにします。
今確認している、手元に持ってきた “synonyms.txt” の末尾に以下の記述を追加してみます。
ぶどう,葡萄,グレープ 林檎,アップル => りんご strawberry,苺 => ストロベリー,いちご
設定を追加したら、この “synonyms.txt” を “test-new-core” のディレクトリに配置します。
配置箇所はスキーマファイル “managed-schema.xml” から相対パスで指定できる場所なら大丈夫なはずですが、今回は “managed-schema.xml” と同じ conf 直下に配置しましょう。
$ podman cp ./synonyms.txt test-solr:/var/solr/data/test-new-core/conf/synonyms.txt
これで Synonym 設定ファイルの準備は OK です。
■Synonym フィルタの設定
次に、スキーマファイル “managed-schema.xml” に Synonym を使用する設定を追加します。
実験用コア “test-new-core” のスキーマファイル “managed-schema.xml” を一旦手元に持ってきて…(デモ用コンテナにはテキストエディタがないため)
$ podman cp test-solr:/var/solr/data/test-new-core/conf/managed-schema.xml ./managed-schema.xml
このスキーマで日本語解析の設定を行なっている部分の “<analyzer>” 要素の中に、Synonym を処理する “synonymGraph” の <filter> 要素を追加します。
ここで、この <filter> 要素を追加する位置は極めて重要になりますが、今回は “cjkWidth” フィルタの後ろに置いてみましょう。
<fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100">
<analyzer>
<tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" />
<filter name="japaneseBaseForm"/>
<filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/>
<filter name="cjkWidth"/>
<filter ignoreCase="true" synonyms="synonyms.txt" name="synonymGraph" expand="true"/>
<filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/>
<filter name="japaneseKatakanaStem" minimumLength="4"/>
<filter name="lowercase"/>
</analyzer>
</fieldType>
設定を変更したら、この “managed-schema.xml” をコア “test-new-core” に戻します。
$ podman cp ./managed-schema.xml test-solr:/var/solr/data/test-new-core/conf/managed-schema.xml
これで Synonym を使うように設定ができました。
■設定の適用と Synonym 処理の確認
それでは、Synonym の処理を確認してみます。
変更したスキーマの設定を読み込むため、”test-new-core” をリロードします。
コマンドでリロードするなら以下のようなコマンドを
$ curl -X POST http://(IP or ホスト名):8984/api/cores/test-new-core/reload
管理画面でリロードするなら “Core Admin” の画面から対象のコアを選択して、”Reload” ボタンを押してください。
これで “test-new-core” で Synonym を処理する設定を適用できました。
早速、管理画面で試してみましょう。
“Core Selector” タブで test-new-core を選び、”Analysis” を開いて、Synonym を適用した FieldType “text_ja” で先ほど追加した Synonym が処理されるのかを確認してみましょう。
ぶどう,葡萄,グレープ
「ぶどう」「グレープ」(と「葡萄」)のいずれを解析した場合でも、解析結果が「ぶどう」「葡萄」「グレープ」の3単語になることが確認できます。
林檎,アップル => りんご
“=>” の左辺にある「林檎」(または「アップル」) を解析した場合、解析結果が “=>” 右辺の「りんご」になることが確認できます。
逆に、”=>” の右辺にある「りんご」を解析した場合は「りんご」のままです。
strawberry,苺 => ストロベリー,いちご
“=>” の左辺にある「strawberry」(または「苺」) を解析した場合、解析結果は “=>” 右辺の「ストロベリー」「いちご」の2単語になることが確認できます。
逆に、”=>” の右辺にある「いちご」(または「ストロベリー」)を解析した場合は「いちご」(または「ストロベリー」)のままです。
このように、先にお話ししたとおりの結果になることが確認できるかと思います。
ということで、これで Synonym の設定方法は大丈夫そうですね。
ただし、Synonym を追加した場合 (フィルタの追加などで解析方法を変更した場合)、当然ながら解析結果は変わります。
そして、前回長々とお話ししたとおり、solr の検索の要であるインデックスはドキュメントの登録時の解析結果を元にしており、後の設定変更には対応していません。
このため、原則として Synonym を追加した場合 (勿論、他のフィルタを追加した場合などにも) は、登録済みのドキュメント・インデックスを一度削除して登録処理をやり直す再インデックスが必要になります。
■Synonym とフィルタ順序
スキーマで “<filter>” 要素を追加する位置が重要である、と先にお話ししましたが、こちらについて少し見てみましょう。
まず、今回行なった設定の状態で全角の「グレープ」と半角の「グレープ」をそれぞれ解析してみてください。
どちらも問題なく今回設定した Synonym により「ぶどう」「葡萄」「グレープ」の3単語に解析されるはずです。
では、”test-new-core” の “managed-schema.xml” で以下のように設定を変更してみてください。
(“synonymGraph” フィルタを、一行前の “cjkWidth” フィルタの前に持ってきます)
#### 変更前 <fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100"> <analyzer> <tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" /> <filter name="japaneseBaseForm"/> <filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/> <filter name="cjkWidth"/> <filter ignoreCase="true" synonyms="synonyms.txt" name="synonymGraph" expand="true"/> <filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/> <filter name="japaneseKatakanaStem" minimumLength="4"/> <filter name="lowercase"/> </analyzer> </fieldType> #### 変更後 <fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100"> <analyzer> <tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" /> <filter name="japaneseBaseForm"/> <filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/> <filter ignoreCase="true" synonyms="synonyms.txt" name="synonymGraph" expand="true"/> <filter name="cjkWidth"/> <filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/> <filter name="japaneseKatakanaStem" minimumLength="4"/> <filter name="lowercase"/> </analyzer> </fieldType>
設定変更の後に “test-new-core” のリロードを行ったら、再度全角の「グレープ」と半角の「グレープ」をそれぞれ解析してみてください。
このとおり、半角の「グレープ」は今回設定した Synonym が適用されなくなっています。
なぜこのようなことになるかと言うと……
1. 解析処理は “managed-schema.xml” の設定の上から順に行われる
2. Synonym フィルタは日本語の半角文字と全角文字を区別している
3. 順序を入れ替えた “cjkWidth” フィルタが日本語の半角文字を全角文字に変換するフィルタだった
……からです。
このことからわかるように、フィルタの設定順序は解析結果に大きく影響を与える可能性があります。(これは即ち、検索結果がおかしくなる可能性があるということです。)
そして、Synonym については Synonym フィルタを適用する時点での解析結果に合致させるように記述しなければ効果がないということもわかるかと思います。
このため、Synonym を使いこなすには、もといフィルタを追加・変更する場合には、事前に各フィルタの処理内容を把握しておくことが極めて重要と言えます。 → Solr Reference Guide
■ちょっと高度な設定:インデックス登録時と検索時の解析方法を別々にする
ここからは大分はみ出したお話しです。
まずは今回 Synonym に設定したこれをもう一度見てください。
ぶどう,葡萄,グレープ
これは今回見てきたとおり「ぶどう」または「葡萄」「グレープ」を、これら3単語の全てを含むように変換する設定です。
ところで、solr は (解析結果で出てきた単語では) 原則 or 検索をします。
なので、よくよく考えてみると……
・インデックス作成時にこの Synonym 処理をした場合、検索時には「ぶどう」「葡萄」「グレープ」のうちいずれか1つの単語が含まれていればよいため、検索時にはこの Synonym 処理をしなくても正常に意図したとおりの検索できる
・検索時にこの Synonym 処理をしていれば、「ぶどう」「葡萄」「グレープ」のどれで検索しても、インデックスに「ぶどう」「葡萄」「グレープ」のいずれかを含むドキュメント全てに hit するため、インデックス作成時にこの Synonym 処理をしていなくても正常に意図したとおり検索できる
……ということで、実はインデックス時か検索時のどちらかでだけこの Synonym 処理をすれば事足りています。
特に、検索時だけ Synonym 処理をする場合は再インデックス処理の必要もなくなるので、Synonym 設定変更時の手間も減りそうです。(インデックスの容量削減も見込めます。)
これを実現するための設定方法があるので、一応ここで紹介しておきます。
今回の設定変更を施した “test-new-core” の “managed-schema.xml” の変更例を見てみましょう。
#### 変更前 <fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100"> <analyzer> <tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" /> <filter name="japaneseBaseForm"/> <filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/> <filter ignoreCase="true" synonyms="synonyms.txt" name="synonymGraph" expand="true"/> <filter name="cjkWidth"/> <filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/> <filter name="japaneseKatakanaStem" minimumLength="4"/> <filter name="lowercase"/> </analyzer> </fieldType> #### 変更後 <fieldType name="text_ja" class="solr.TextField" autoGeneratePhraseQueries="false" positionIncrementGap="100"> <analyzer type="index"> <tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" /> <filter name="japaneseBaseForm"/> <filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/> <filter name="cjkWidth"/> <filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/> <filter name="japaneseKatakanaStem" minimumLength="4"/> <filter name="lowercase"/> </analyzer> <analyzer type="query"> <tokenizer mode="search" name="japanese" userDictionary="lang/userdict_ja.txt" /> <filter name="japaneseBaseForm"/> <filter tags="lang/stoptags_ja.txt" name="japanesePartOfSpeechStop"/> <filter name="cjkWidth"/> <filter ignoreCase="true" synonyms="synonyms.txt" name="synonymGraph" expand="true"/> <filter ignoreCase="true" words="lang/stopwords_ja.txt" name="stop"/> <filter name="japaneseKatakanaStem" minimumLength="4"/> <filter name="lowercase"/> </analyzer> </fieldType>
大規模に変更しているように見えますが、概ね <analyzer> ~ </analyzer> をコピーして 2つにしただけです。
<analyzer> にはそれぞれ “type” オプションを設定します。“index” はインデックス時、”query” のほうは検索時の解析方法を設定します。
この例では更に、”query” 側にだけ “synonymGraph” フィルタを設定しています。
折角なのでこの設定も一応試してみましょう。
“managed-schema.xml” の配置と設定のリロードを行なったら、例によって “Analysis” ページで確認していきます。
実はこのページ、左側がインデックス時の解析、右側が検索時の解析となっているため、この変更のチェックに最適だったりします。
このとおり、設定どおりに検索時にのみ Synonym 処理がされていることが確認できるかと思います。
ただ、解析方法を変えるということは言うまでもなく解析結果も変わるということです。
solr はインデックス時と検索時それぞれの解析結果をもって検索を行うため、これはつまり下手をすれば検索が機能しなくなる危険性があるということです。
例えば今回のこの Synonym 設定です。
林檎,アップル => りんご
このセクションでの “managed-schema.xml” の設定下では、
・インデックス時は Synonym 処理がされないので、インデックスには「林檎」や「アップル」で登録される
・検索時には Synonym 処理されるので、「林檎」で検索しても「りんご」のインデックスを持つドキュメントしか出てこない (「林檎」で検索しているのに「林檎」のインデックスを持つドキュメントが出てこない)
というおかしなことになってしまいます。
この解析方法の分割設定を実施したい場合、自分が何をしようとしているのか、本当に設定しても大丈夫なのかを十分に検討した上で、細心の注意を払い、入念に検証を行なった上で取り掛かることをおすすめします。
■最後に
今回は solr の Synonym とフィルタ設定についてお話ししてみました。
ここまで数回に渡って solr についてお話しして何となく察してもらえたかと思いますが、
solr は導入して適当にデータを入れれば即便利!というものではなく、
入念な準備と工夫をすることで検索時の利便性・快適さを提供できるツールです。
なので、solr が力を発揮するのは例えばマニュアルサイトや EC サイトなど、不特定多数の人が検索を行い得る環境であると言えるでしょう。
今回の一連の記事で一通りの基本的な要素をお話しできたかとは思いますが、他にもあいまい検索や N-Gram 検索を実装したりなど様々な設定・フィルタがありますので、気になった方は是非一度試して自分なりの最強の検索環境を作ってみてはいかがでしょうか。