Apache Solrについて
はじめに
業務でApache Solrを使用していますが、検索エンジンの仕組みやSolrが持つ機能が分かっていませんでした。
そこで今回は[改訂第3版]Apache Solr入門――オープンソース全文検索エンジン の要約や業務を通しての自分の考えを述べせて頂きます。
対象読者
- Apache Solr初心者
Apache Solrとは
Apache Lucene(アパッチルシーン)をベースに作成された検索エンジンサーバーあり、高速な全文検索を可能としてます。全文検索とは特定のキーワードを含む文章を検索する機能です。
Apache Solr(以下Solr)では高速な全文検索を実現するために転置インデックスやキャッシュなどを使用し、また検索結果についてもファセットやハイライトなどを使用してユーザーが求めている情報に誘導する仕組みを持っています。
以降は高速な全文検索を実現するための仕組みや要素に述べさせて頂きます。
転置インデックス
全文検索には順次検索または転置インデックスが使用されます。順次検索は文章の先頭から終わりまでを検索キーワードに該当するか調べます。DBのLIKE検索やLinuxコマンドのgrepがそれに当たります。ただし、DBのLIKE検索の中間一致や後方一致、Linuxのgrepではインデックスが使用できないため高速な検索はできません。
一方でSolrなどの検索エンジンでは転置インデックスを作成し高速な検索を実現しています。
転置インデックスは以下の流れで作成します。
- ドキュメントは以下の様にドキュメント毎に割り振られた一意のドキュメントIDと紐づいて管理されています。
ドキュメントID | ドキュメント |
---|---|
1 | 花粉症シーズン |
2 | 春の到来 |
3 | 鼻水の薬 |
4 | 花粉症の薬 |
- ドキュメントを単語に分割しドキュメントIDとの組にします。
単語 | ドキュメントID | 単語 | ドキュメントID | 単語 | ドキュメントID |
---|---|---|---|---|---|
花粉症 | 1 | シーズン | 1 | ||
春 | 2 | の | 2 | 到来 | 2 |
鼻水 | 3 | の | 3 | 薬 | 3 |
花粉症 | 4 | の | 4 | 薬 | 4 |
- 単語をキーにしドキュメントIDのリストを値とした連想配列を作成
単語 | ドキュメントIDのリスト |
---|---|
花粉症 | 1,4 |
シーズン | 1 |
春 | 2 |
の | 2,3,4 |
到来 | 2 |
鼻水 | 3 |
薬 | 3,4 |
上記の連想配列が転置インデックスです。
では、転置インデックスを利用した検索について説明します。
「花粉症」AND「薬」で検索すると以下の転置インデックスの「花粉症」と「薬」がヒットします。
単語 | ドキュメントIDのリスト |
---|---|
花粉症 | 1,4 |
シーズン | 1 |
春 | 2 |
の | 2,3,4 |
到来 | 2 |
鼻水 | 3 |
薬 | 3,4 |
この時、対象となるドキュメントIDのリストはヒットした「花粉症」が持つ[1,4]と「薬」が持つ[3,4]です。
検索キーワードはANDを使用しているため、[1,4]と[3,4]で共通しているドキュメントIDである「4」が抽出されます。 (ORの場合は「1,3,4」が抽出されます。)
結果、ドキュメントIDが4のドキュメントである「花粉症の薬」が検索結果として返却されます。
ドキュメントID | ドキュメント |
---|---|
1 | 花粉症シーズン |
2 | 春の到来 |
3 | 鼻水の薬 |
4 | 花粉症の薬 |
上記の流れからSolrでは転置インデックスを使用することでDBやgrep検索よりも高速な全文検索を可能としています。
転置インデックスは高速検索ができるメリットがありますが、デメリットとしてはドキュメントとは別で転置インデックス用のメモリやディスクの領域を確保する必要があったり、ドキュメントの更新後にインデックスを更新しオプティマイズする必要があります。私の経験では開発環境などでオプティマイズした際にファイルシステムの容量不足でエラーになったことがあります。
アナライザ
転置インデックスを作成する上でドキュメントを単語単位に分割する必要があります。同時に検索キーワードも同様に単語に分割します。それを担っているのがアナライザであり、ドキュメントや検索キーワードを正規化し単語に分割するのをアナライズと言います。
アナライズは文字フィルタ、トークナイズ(単語分割)、トークンフィルタの順で行われますが、単語分割以外は必須ではありません。
それぞれ実行するFactoryが存在しますが、使用するFactoryによって検索のパフォーマンスに影響を与えます。
そのためFactoryを設定する時は要件やドキュメントの形式や言語を考慮する必要があります。
以降では各工程の説明やFactoryの一例を記載しています。
文字フィルタ
ドキュメント中の単語の置換を行います。例えば全角の文字を半角、または半角の文字を全角にする。漢字の旧字体を新字体に置換などを行います。
置換対象の文字や置換文字は使用する文字フィルタによって変わります。
例
-
MappingCharFileterFactory
- 「①」を「1」に置換
- 「齋藤」を「斉藤」に置換
-
HTMLStripCharFilterFactory
- 「<h1>タイトル</h1>」から「タイトル」の様にドキュメントのHTMLタグを除去します。
トークナイズ
文章を単語単位に分割するのをトークナイズと言い、トークナイズを実行するものをトークナイザと言います。
どのトークナイザを使用するかで単語の分割の仕方が変わってきます。
例
-
JapaneseTokenizerFactory
- 日本語のドキュメントに対して使用されるトークナイザ
- 分割例 「私は花粉症です。」→ 「私」、「は」、「花粉症」、「です」
- modeオプションの設定で単語の分割の仕方が変化します。
- 特定の単語に分割についてはユーザ辞書で定義することできます。(modeオプションがsearchモードの時のみ)
-
WhitespaceTokenizerFactory
- 英語の様に単語が半角スペース区切りになっている文章に使用される。
- 分割例 「I have hey fever」→ 「I」、「have」、「hey」、「fever」
トークンフィルタ
トークン(単語)の変更や削除を行います。例えば英語の大文字を小文字にしたり、テキストファイルに定義された特定の単語を削除したりすることができます。
トークンフィルタによって同じ単語で変更の仕方や削除する観点が異なります。
例
- JapanesePartOfSpeechFilterFactory
- 日本語に含まれる助詞や助動詞などの不要な品詞を除去します。
- 例 「花粉症の薬」→ 「花粉症 薬」
- JapaneseKatakanaStemFilterFactory
- 日本語のカタカタにおける末尾の長音記号(ー)の揺れを解消します。
- 「サーバー」と「サーバ」でどちらでも検索することが可能になります。
フィールド
DBのカラムと同様にSolrもデータを格納するためにフィールドを設定する必要があります。
DBとの相違点としてはDBはテーブル毎にカラム名、型、桁を定義しますが、Solrの場合はインスタンス毎に存在するファイル(shema.xmlまたはmanaged-schema)にフィールドを定義します。
フィールドはfildTypeタグで設定をし、オプションでフィールド名や型などを定義します。
必須項目と私が業務で使用しているオプションを一覧にしてみました。
オプション | デフォルト | 必須 | 内容 |
---|---|---|---|
name | なし | ○ | フィールド名 |
class | なし | ○ | フィールドの型となるクラスを定義 |
indexed | true | 任意 | 検索対象または検索条件にする場合はtrue |
stored | true | 任意 | 値を検索結果に表示する場合はtrue、表示しない場合はfalse |
multiValued | false | 任意 | フィールドに複数の値を持たせる場合はtrue |
また、フィールドには非テキスト系フィールドタイプとテキスト系フィールドタイプの2種類があります。
非テキスト系フィールドタイプは価格、距離などの数値、トークナイズが不要な文字列が該当します。
テキスト系フィールドタイプはフィルタリングやトークナイズが行われる文字列が対象になります。
両者で設定できるclassが決まっています。
非テキスト系フィールドタイプ
- 価格や距離などの数値、トークナイズが不要な文字列
- classに設定できるのがsolr.StrField(string)、solr.BoolField(boolean)、solr.TrielntField(int)など多岐に渡ります。
テキスト系フィールドタイプ
- フィルタリングやトークナイズが行われる文字列が対象
- classに設定できるのがsolr.TextFieldのみです。
ダイナミックフィールド
上記の各フィールドタイプはフィールド名が固定して設定されていますが、ダイナミックフィールドの場合は正規表現に一致する場合に動的にフィールド名を変更することが可能です。これにより設定ファイルでのフィールドの定義を少なくすることができます。
例を交えて説明します。
例えばはschema.xmlに以下の設定がされていた場合
<dynamicField name="*_search" type="str" indexed="true" stored="true"/>
上記の設定の場合、投入されるドキュメントのフィールド名の末尾が「_search」で終わる値はindexdがtrueかつstoredがtrueの文字列としてSolrに管理されます。「title_search」や「name_search」なども同様にSolrに登録されます。そのため、ダイナミックフィールドを使用することで新たにフィールドを追加する場合にschema.xmlなどの設定ファイルの追記や修正が不要になります。
ユニークキーフィールド
Solrのドキュメントをユニークにするためのフィールドです。
ユニークキーを指定することでドキュメントの更新や削除が可能になりますが、指定しなかった場合は更新や削除はできなくなります。
schema.xmlなどに定義する場合は以下の様になります。
<uniqueKey>id</uniqueKey>
ユニークキーフィールドはindexedをtrueに設定する必要があります。
ドキュメントの登録、更新、削除
Solrはjson、xml、csvでドキュメントの登録や更新が可能です。
実際の業務ではxmlで登録などをしているため、xmlを例に各機能について説明します。
登録
ドキュメントを登録する際は以下のようなファイルを作成します。
<add>
<doc>
<field name="id">1</field>
<field name="title_search">本のタイトル(検索用)</field>
<field name="price_search">1000</field>
</doc>
<doc>
<field name="id">2</field>
<field name="title_search">本のタイトル2(検索用)</field>
<field name="price_search">1000</field>
</doc>
</add>
構造としては全体を<add>、</add>で囲み、ドキュメントごとに<doc>、</doc>で囲む構造になっています。
またこの時にtypeがintのフィールドに対して空文字やnullなどを設定して登録するとエラーが発生しますので値がない場合でも0などの値を必ず設定してください。
続いてドキュメントの登録方法です。
ドキュメントの登録方法は二つあります。一つはSolr付属のツールを使用して登録する方法こと、もう一つはcurlを使用して登録する方法です。業務ではcurlを使用してますのcurlでの登録方法について説明します。
curlで登録する際は以下のコマンドを実行します。
curl http://IPアドレス:ポート番号/solr/<コア名>/update?commit=true --data-binary@登録するドキュメントが定義されたファイル名 -H 'Content-type:text/xml; charset=utf-8
実行後レスポンスとして下記が返却されます。
正常に登録された場合
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">ステータスコード(正常なら0)</int>
<int name="QTime">処理時間</int>
</lst>
<doc>
<str name="id">unique_id</str>
<str name="field1">value1</str>
<str name="field2">value2</str>
...
</doc>
</response>
エラーがあった場合
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader">
<int name="status">エラーコード(例:400)</int>
<int name="QTime">処理時間</int>
</lst>
<lst name="error">
<str name="msg">エラーメッセージ</str>
</lst>
</response>
更新
ドキュメントの更新については登録と同様に付属ツールとcurl及び同じURLを使用します。
ファイルの構造についても登録時と同様です。
注意点としては更新対象のドキュメントがユニークキーフィールドを持つこと、投入するファイルに必ずユニークキーフィールドと更新対象外のフィールドも定義されていることです。
理由としてはユニークキーフィールドの値から更新対象のドキュメント特定し削除します。その後、ファイルに定義されている内容をそのまま登録するためです。
この時にユニークキーフィールドが含まれていない場合、ドキュメントは更新されず投入されたドキュメントが新規登録されます。更新となっていますが内部では「削除」と「登録」が実行されています。
また、更新内容が定義されているファイルに更新対象外のフィールドが定義されていないと場合はそのフィールドがドキュメントから削除されます。
部分更新
前述の通り、更新する際は更新対象外のフィールドを含める必要があります。しかし、ドキュメントを構成するフィールドが全ての属性がstored=tureかつdocValue=trueの場合はSolrに投入するファイルには更新対象のフィールドのみ記入で問題ありません。削除から登録の流れは更新と同じですが、上記の場合は足りないフィールドについてはSolr側で補完されるため更新対象のフィールドのみをSolrに渡すだけでいいです。
削除
削除する際も登録及び更新と同様に付属のツールやcurlなどで行います。curl時のURLも登録と更新と同様です。
また、更新と同様に削除対象のドキュメントはユニーキーフィールドを持つ必要があります。
Solrに渡すファイルの構造については登録と更新とは異なり、以下のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<delete>
<id>削除するドキュメントのユニークキーフィールドの値</id>
</delete>
検索
Solrの検索について
Solrは転置インデックスを使用してドキュメントを検索しますが、この時ヒットしたドキュメントを検索結果としてただ表示するわけではなく、検索キーワードにマッチしたランキング順にドキュメントを表示します。
ランキングを実現するためにSolrに管理されている全ドキュメントの単語に重みが付けられていて、検索キーワードと単語の重みをもとにランキングを作成しています。
このようにヒットした結果をそのまま表示せずユーザーのことを考慮して結果を表示することがDBにはないSolrなどの検索エンジンが持つ強みではないかと考えてます。
Solrで検索する際は管理画面から検索を行うか、URLを作成して実行して検索結果を取得します。
検索する際は以下のURLは使用します。
http://IPアドレス:ポート番号/solr/<コア名>/select?q=検索条件&wt=xml&indent=true
URLの各パラメーターについては以下の通りです。
パラメータ | 値 | 備考 |
---|---|---|
q | 検索条件 | 例 titleフィールドの値がジャンプのドキュメントを検索する場合 q=title:ジャンプ |
wt | 検索結果の表示形式 | xmlで検索結果を表示する場合はwt=xml 対応形式はjson、csv、python、rubyなど |
indent | インデント有無 | trueの場合は検索結果にインデントを付ける |
Solrには通常の検索に加えてハイライト、ファセット、絞りこみなどの検索機能があります。ハイライト、ファセット、絞りこみについて説明させていただきます。
ハイライト
ハイライト使うことで検索キーワードがドキュメントのどの部分にヒットしたかを強調表示することができます。
ファセット
対象のフィールドをグルーピングして値ごとに件数を表示します。使用例としてはECサイトなどで商品のカテゴリごとに件数を表示する際に使用します。
絞りこみ検索
qパラメータを使用して得た検索結果からさらに検索を行います。
絞り込み検索を使用する時はqパラメータとfqパラメータを使用します。qパラメータのみの複数の条件で検索した時と比較してqパラメータとfqパラメータを使用して絞り込み検索をするとSolrのキャッシュを利用するため検索速度が向上します。
コミット
DBと同様にSolrもドキュメントの登録、更新、削除した際にコミットすることで検索結果に反映されます。
コミットする際はURLにcommit=trueをつける必要があります。
・コミットのみする場合
curl "http://IPアドレス:ポート番号/solr/update?commit=true"
・登録、更新と同時にコミットする場合
curl "http://IPアドレス:ポート番号/solr/update?commit=true" --data-binary@登録するドキュメントが定義されたファイル名 -H 'Content-type:text/xml; charset=utf-8
また、ファイルでのコミットも可能です。
・xmlの場合
<commit/>
Solrにはハードコミットとソフトコミットの2種類のコミットがあります。
-
ハードコミット
- ドキュメント追加後にコミットを実行した際にインデックスファイルに書き込まれます。
- インデックスファイルに書き込まれるため、Solrサーバーが落ちてもコミットした内容は保持されます。
- ディスクへの書き込みやキャッシュのウォームアップを伴うため比較高負荷
-
ソフトコミット
- コミットを実行してもインデックスファイルに書き込まれる処理がないため即座に検索結果に反映されます。
- メモリ上でコミット操作を行うためI/Oが低負荷
- ソフトコミットする際はURLにsoftCommit=trueを付けます。
- インデックスファイルに書き込みまることがないためSolrサーバーが落ちた際ソフトコミットした内容は消失します。
- ソフトコミット後でもロールバックが可能
上記からソフトコミットはドキュメントを登録直後に検索結果に反映することができます。
これを擬似リアルタイム検索と言います。
また、ハードコミット及びソフトコミットともに自動コミットすることが可能です。
コミットするタイミングとしてはインデックスされたドキュメント数またはドキュメントの追加、更新、削除されてからの時間で決めることができます。
ロールバック
SolrでもDBと同様にロールバックが可能です。
ロールバックする場合は以下の様にURLにrollback=trueを付けます。
curl "http://IPアドレス:ポート番号/solr/update?rollback=true"
ファイルでのロールバックも可能です。
・xmlの場合
<rollback/>
前述した通り、ソフトコミットの場合のみコミット後のロールバックが可能です。ハードコミットではロールバック不可です。
オプティマイズ
Solrはコミットを実行する度にインデックスファイルが作成されます。
Sorlは検索時に全てのインデックスファイルを参照するためインデックスファイルが多いと検索性能が劣化してしまいます。
そこでオプティマイズを実行することで複数に分かれたインデックスファイルをマージし検索性能を向上させます。
オプティマイズはデフォルトではインデックスファイルが一つになるまでマージを実行します。マージ後に古いインデックスファイルは削除されます。
トランザクションログ
Solrはドキュメントが投入されるとトランザクションログにドキュメントの内容が書き込まれます。
トランザクションログへの書き込まれた後にコミットが実行されます。
ソフトコミットで登録したドキュメントが消失した場合などでトランザクションログを利用してインデックスのリカバリを実行します。
また、設定次第ではコミット前でもトランザクションログを参照してドキュメントを検索することが可能です。
上記をリアルタイムGetと言います。
キャッシュ
検索結果キャッシュ
検索リクエストに対する検索結果をキャッシュしています。
仕組みとしては「q(検索式)」、「sort(ソート条件)」、「fq(フィルタクエリ)」の組み合わせをキーにしドキュメントに紐ずくドキュメントIDのリストを管理しています。
ここに出てくるドキュメントIDはSolrが内部でドキュメントを管理する為に使用しているIDでユニークキーフィールドとは異なります。
検索結果キャッシュはSolrにリクエストが来た際に該当する検索結果のデータがないか確認されます。キャッシュ上にデータがあった場合はインデックスファイルを参照せずに検索結果を返却します。しかし、flパラメータを使用して検索結果に表示するフィールドを指定している場合はインデックスファイルを参照して指定されたデータを取得します。
フィルタキャッシュ
「fq(フィルタクエリ)」をキーにし、該当するドキュメントIDを管理しています。
ドキュメントキャッシュ
ドキュメントIDをキーにし、ドキュメントを管理しています。
上記以外にもファセット、ソートで使用されるフィールド値キャッシュやユーザ自身でキャッシュを作成できるユーザ定義キャッシュがあります。
自動ウォームアップ
キャッシュを再利用する仕組みです。
Solrのキャッシュが持つドキュメントIDはコミットされたタイミングでリセットされキャッシュが破棄されます。
キャッシュ破棄により検索のパフォーマンスが劣化しますが、自動フォームアップを利用することでキャッシュで使用されていたキーで新規作成されたインデックスファイルを検索して値を設定します。キャッシュキーはそのまま引き継がれるため、キャッシュのヒット率は変わらず検索のパフォーマンス劣化も起きないです。
まとめ
Solrは転置インデックスを利用した高速な全文検索が可能であり、検索だけでなくファセットやハイライトなどが多様な検索ができることがわかりました。データを更新する際はSQLの様な言語を学習する必要がないため、導入コストは低いのかなと感じました。
記載した内容以外にもSolrはレコメンド機能、分散検索、分散インデクシングなどの機能があるので機会があれば実践したいです。
また、DBとSolrの違いについてどこかでまとめてみたいです。
今回は記事の内容が長くなってしまい読みにくため次回の記事からは分けて書いた方がいいなと感じました。。。
Discussion