Speee DEVELOPER BLOG

Speee開発陣による技術情報発信ブログです。 メディア開発・運用、スマートフォンアプリ開発、Webマーケティング、アドテクなどで培った技術ノウハウを発信していきます!

Apahce Solr 4.6.0 入門編

はじめに

はじめまして、社会人3年目の博士です。
博士と呼ばれていますが、実際には修士なので、なんちゃってハカセです。
業務外ではカメラとフィジカルコンピューティングに興味があります。
どうぞよろしくお願い致します。

f:id:bino98ty:20180104175821p:plain

さて、今回はApache Solrの入門編ということで、全文検索の設定MySQLからデータのインポートを行っていきたいと思います。

※執筆開始時点でのApache Solrのバージョンは4.6.0でしたので、4.6.0を対象にしていますが、執筆中に出た4.6.1, 4.7.0でも内容が有効な事を確認しています。ファイル名等のバージョン番号を読み替えてお試しください。

そもそも

Apache Solrとはなんでしょうか。
実は、僕は知らなかったのですが、業務でMySQLのデータベース内のデータから全文検索を行う機能を実装する事になり、いろいろと教えてもらったり調べたりしました。
簡単に言うと、Apache Solrは後付けと負荷分散が比較的簡単にできる全文検索エンジンです。
MySQLのデータを使える全文検索エンジンといえば、他にはMySQLに組み込んで使うTrittonやmroonga, MySQL 5.6から標準で載ったInnoDB FullTextSearchなどが挙げられるのでしょうか。

cf. MySQL Casual Talks Vol.4 「MySQL-5.6で始める全文検索 〜InnoDB FTS編〜」 (Slideshare)

Apache Solrの良いところは、負荷分散を最初から考えられるところではないでしょうか。 負荷分散については、スライド等を共有してくださっている他社様もおられます。

cf. pixiv サイバーエージェント共同勉強会 solr導入記 (Slideshare) cf. アメーバサーチにApache Solr 1.4をつかってみた

検索に使うインデックスデータをコピーすれば、別のインスタンスで動かし始められたりしますし、シャーディングがサポートされています。

ただ、今回は、僕が全文検索機能を作ることが初めてだったため、うまくいかなければ最初のリリースから外して後のリリースに回すか機能から外すかも、という話もあり、データベース(MySQL)に直接依存しないで済み、後付けしやすいという理由から、Apache Solrを使うことにしました。
なので、タイトルに入門編とありますが、僕自身がSolr初心者のため、入門編という事だったのです。

Ω ΩΩ <ナ、ナンダッテー

ためしてみる

下記サイトから、Apache Solrをダウンロードしてください。

Apache Solr公式サイト

DOWNLOADをクリックすると,近くのミラーを探すページに飛ばされます。

僕の場合は山形大学のミラーでした。
(こういったミラーを提供してくださっている大学や企業様には、大変感謝しております。速いし。)

とりあえず起動してみる

Apache Solrは、動かすのに1.6以上のJavaが必要になります。

上記のように、ファイルの展開と、jarの実行をしてください。動きます。 (動かなかった場合、Javaが1.5系だったりするかもしれません。また、Apache Solrのバージョンアップでバージョン番号が変わっていた場合は、フォルダ等を読み替えてください。)

動いたら、

http://localhost:8983/solr/ (ApacheSolrのローカル管理画面)

にアクセスします。

どうですか?何かでてきましたか?

恐らく、上記の画面が出てくるはずです。
少しだけ説明しますと、ダウンロードして展開したディレクトリの下に、exampleというフォルダが有ります。
ここには、Apache SolrをRESTっぽいAPIで使えるサーバと、そのサーバを動かすサンプル設定が数種類入っています。
デフォルトのまま(つまり、‘java -jar start.jar’)で起動した場合、solr/のディレクトリ内の設定が使われます。

さて、以下から、今回僕が使った機能である、MySQLからのデータインポート(フルインポート、差分インポート)と、マルチコア化について、紹介していきます。
ちなみに、先ほどのコマンドで起動していたApache SolrはCtrl+Cで、後処理をして落ちてくれます。

MySQLからデータを入れてみる

Apache Solrは、設定ファイルに様々な処理を担当するハンドラを記述することで、それらの処理をREST風なAPIで呼び出す事が可能になります。
その中の一つに、データベースからデータをインポートしてくるハンドラがあり、こちらを利用します。
データの入れ方としてはフルインポートと、差分インポートの二種類を利用出来ます。
まずは、Apache Solr側の事前準備を書いた後、MySQLからデータをフルインポートする方法から紹介します。

事前準備

今回は、exampleフォルダ内のsolrフォルダ内のcollection1の中の設定を変更して、設定したいと思います。

データのインポートを行う前にやらなければならないことの一つとして、schema.xmlの設定があります。
schema.xmlには、全文検索機能を実装するにあたり、どの項目を検索インデックスに登録し、どの項目をレスポンスとして返すか、といった設定が書かれています。

例えば、以下の様なデータベースのテーブルを持つ、メッセージと、そのメッセージが書かれた場所を記録する様なアプリケーションに対して、全文検索機能を持たせようと考えたとします。

CREATE DATABASE solr_test;

CREATE TABLE `location` (
  `id` int NOT NULL,
  `name` text NOT NULL,
  `modified` datetime NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `message` (
  `id` int NOT NULL,
  `location_id` int NOT NULL,
  `contents` mediumtext NOT NULL,
  `disable` tinyint NOT NULL DEFAULT '0',
  `modified` datetime NOT NULL,
  `created` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

上記のテーブルから、地域とメッセージ内容どちらかからメッセージIDをレスポンスとして返す事を考えると、以下の様なschema.xmlになります。

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="message" version="1.5">
<fields>
  <field name="message_id" type="int" indexed="true" stored="true" required="true" multiValued="false" />
  <field name="contents" type="text_en" indexed="true" stored="false" required="true" />
  <field name="location" type="text_en" indexed="true" stored="false" />

  <field name="modified" type="date" indexed="true" stored="false" />
  <field name="created" type="date" indexed="true" stored="false" />

  <!-- reserved -->
  <field name="_version_" type="long" indexed="true" stored="true" />

  <!-- copy field -->
  <field name="text" type="text_en" indexed="true" stored="false" multiValued="true" />
</fields>
<uniqueKey>message_id</uniqueKey>

<copyField source="contents" dest="text" />
<copyField source="location" dest="text" />

<types>
  <fieldType name="string" class="solr.StrField" sortMissingLast="true" />
  <fieldType name="int" class="solr.TrieIntField" precisionStep="0" positionIncrementGap="0"/>
  <fieldType name="date" class="solr.TrieDateField" precisionStep="0" positionIncrementGap="0"/>
  <fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/>
  <fieldType name="text_en" class="solr.TextField" positionIncrementGap="100">
    <analyzer>
      <tokenizer class="solr.StandardTokenizerFactory"/>
      <filter class="solr.LowerCaseFilterFactory"/>
      <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_en.txt" />
    </analyzer>
  </fieldType>
</types>
</schema>

簡単に説明すると、fieldsタグで囲まれた中に、全文検索で利用する項目をfieldタグで記述して行きます。
fieldタグの属性のなかで重要な物は、name, type, indexed, stored, requiredでしょうか。

name
Solr上で用いる名前です。データインポートの設定にも出てきます。
type
型です。同じschema.xmlのtypesタグ内に記述されている型を使う事が出来ます。
indexed
検索項目に含めるかどうかを設定します。trueにしておくと、設定した項目で検索が可能です。
stored
レスポンスに含めるかどうかを設定します。trueにしておくと、検索結果を返す際にレスポンスに含まれるようになります。
required
必須項目として指定します。データインポートの際に、ちょっと関わってきます。

また、コピーフィールドという物を使って、地域名とメッセージのどちらでも引っかかる様にしています。それが、以下の部分になります。

  <!-- copy field -->
  <field name="text" type="text_en" indexed="true" stored="false" />
...
<copyField source="contents" dest="text" />
<copyField source="location" dest="text" />

typesタグで囲まれた箇所は、型を定義するところになります。
int型なんかはシンプルなのですが、text_en型は何やら複雑な設定がされています。
この設定は、検索する際にゴミになってしまう様なストップワードや大文字小文字といった差を吸収するためのフィルタを入れています。
この型の項目にデータを入れる際に、これらの処理が行われるようになります。
日本語を扱いたい場合、形態素解析を行うような設定をここで指定する必要がありますが、今回はtext_enからもお察しの通り、英語で行きます。
ちなみに、3.6から日本語形態素解析の機能がデフォルトで追加されているようです。

cf. Solrの日本語対応 -新しく追加されたトークナイザ・トークンフィルタ-

修正後、java -jar start.jarで、起動させてみてください。以下の様なエラーが出ます(これ以外の場合、schema.xmlのどこかで間違っている可能性があります。)。

org.apache.solr.common.SolrException: Invalid Number: MA147LL/A

このエラーは、サンプル設定でQueryElevationComponentというコンポーネントが有効になっているために起こったエラーです。このコンポーネントは、スポンサードサーチの様な、実際に検索した結果に追加して、別で結果を出せることを実現するために使いますが、今回は外します。
solrconfig.xmlを開き、elevate.xmlで検索をかけると、以下の様な箇所が見つかります。

...
  <!-- Query Elevation Component

       http://wiki.apache.org/solr/QueryElevationComponent

       a search component that enables you to configure the top
       results for a given query regardless of the normal lucene
       scoring.
    -->
  <searchComponent name="elevator" class="solr.QueryElevationComponent" >
    <!-- pick a fieldType to analyze queries -->
    <str name="queryFieldType">string</str>
    <str name="config-file">elevate.xml</str>
  </searchComponent>

  <!-- A request handler for demonstrating the elevator component -->
  <requestHandler name="/elevate" class="solr.SearchHandler" startup="lazy">
    <lst name="defaults">
      <str name="echoParams">explicit</str>
      <str name="df">text</str>
    </lst>
    <arr name="last-components">
      <str>elevator</str>
    </arr>
  </requestHandler>
...

ここを、ごそっと削除します。

起動出来ますか?出来ましたね。
さて、やっと事前準備がととのいました。次はフルインポートの設定です。

フルインポート (Full-Import)

フルインポートはSQLサーバからデータを取得してSolrの検索用インデックスを作り直す処理になります。
このフルインポートの処理は重いので、基本的には最初の1回実行し、あとは次で紹介する差分インポートを実行することになると思います。
まずは、DataImportHandlerの設定をsolrconfig.xmlに追記します。
このDataImportHandlerが、SQLサーバからデータインポートの処理をしてくれます。

<?xml version="1.0" encoding="UTF-8" ?>
<config>
...
  <lib dir="../../../dist/" regex="solr-dataimporthandler-\d.*\.jar" />
  <lib dir="../../../dist/" regex="mysql-connector-java-\d*\.jar" />
...
  <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
      <str name="config">data-config.xml</str>
    </lst>
  </requestHandler>
...
</config>

libタグで、DataImportHandlerのjarを読み込む設定を追加し、requestHandlerタグでREST APIで使われるパスと処理を紐付けています。
また、これは余談ですが、requestHandlerタグにstartup="lazy"と入れておくと、Solrの起動時にはコンポーネントは読み込まれず、URLにリクエストがあった場合に、コンポーネントが読み込まれていなければ読みこんでから結果を返す動きになります。

また、SQLJDBCドライバもlibタグで読み込むように指定しておきます。上記サンプルでは、solr-dataimporthanderのjarがあるdistにMySQLJDBCドライバをコピーして置いた状態で、指定してあります。

次に、data-config.xmlを作ります。これは、上記requestHandlerの設定の中のname=configなstrタグで指定しているファイルです。
事前準備で作ったschema.xmlの、solrで用いるフィールドと、SQLで取得してくるカラムを紐付けるためのファイルになります。

<dataConfig>
  <dataSource type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/solr_test" user="hogehoge" password="*******" readOnly="true" />
  <document>
    <entity name="entity"
      query="SELECT m.id AS message_id, m.contents, l.name AS location, m.modified, m.created FROM message AS m LEFT JOIN location AS l ON (m.location_id = l.id) WHERE m.disable = 0"
    >
      <field column="message_id" name="message_id" />
      <field column="contents" name="contents" />
      <field column="location" name="location" />
      <field column="modified" name="modified" />
      <field column="created" name="created" />
    </entity>
  </document>
</dataConfig>

dataSourceタグでJDBCドライバの設定を行います。
solrのフィールドとの紐付けは、documentタグ内で行います。
entityタグのquery属性に、発行するSQLを記述します。
そして、そのタグの中のfieldタグで、紐付けをしています。
上記サンプルでは同じにしてしまいましたが、column属性にはSQL側のカラム名、name属性にsolr側のフィールド名を記述します。
また、JOINを使って一つのSQLで済ませていますが、下記のように、2つに分けて取得する事も可能です。

...
    <entity name="entity"
      query="SELECT m.id AS message_id, m.contents, m.location_id, m.modified, m.created FROM message AS m WHERE m.disable = 0"
    >
...
    </entity>
    <entity name="location" query="SELECT * FROM location WHERE id = ${entity.location_id}">
      <field column="name" name="location" />
    </entity>

最後に、いくつかデータをDB側に入れておいてください。

さて、これでフルインポートの準備ができました。Solrを再起動しましょう。

http://localhost:8983/solr/collection1/dataimport?comamnd=full-import このURLにアクセスすると、REST APIとして、フルインポートが実行されます。
アクセスした後、下記URLにアクセスする事で状況を確認出来ます。
http://localhost:8983/solr/collection1/dataimport?comamnd=status

Total Documents Processedというところの数が0でなければ成功です。
数が0の場合、Total Rows Fetchedも0であれば、取得対象になっているデータが無いと思われるので、データを入れてください。
Total Rows Fetchedが0で無い場合は、フィールドとの紐付け等に失敗していて、データが入らなかった可能性が高いので、設定を確認してみてください

フルインポートをしたデータが実際に検索に引っ掛かるか、試してみましょう。
下記URLにアクセスしてください。ここから、検索クエリを発行する事が出来ます。
http://localhost:8983/solr/#/collection1/query (collection1の検索クエリ発行画面)

commonのqに、引っ掛けたいキーワードを入れてください。

図の様な形でコピーフィールドであるtextを指定する事で、場所の名前でも検索に引っ掛かるようになります。

Execute Queryボタンを押すと、実際にクエリが発行され、結果が帰ってきます。
ここで帰ってくる結果は、指定しなければschema.xmlでstored=trueに指定したフィールドになります。

これでフルインポートは完了です。
次は、差分インポートです。

差分インポート (Delta-Import)

差分インポートは、前回DBからのインポートを行った時刻以降に更新されたものを対象にするインポートです。
インデックスは、前回のインポートで作られたインデックスに追加される形で作られます。
設定は、フルインポートで作ったdata-config.xmlを修正する形で行います。
entityタグを以下のように修正してください。

...
    <entity name="entity"
      query="SELECT m.id AS message_id, m.contents, l.name AS location, m.modified, m.created FROM message AS m LEFT JOIN location AS l ON (m.location_id = l.id) WHERE m.disable = 0"
      deltaQuery="SELECT id FROM message WHERE disable = 0 AND modified &amp;gt; '${dih.last_index_time}'"
      deltaImportQuery="SELECT m.id AS message_id, m.contents, l.name AS location, m.modified, m.created FROM message AS m LEFT JOIN location AS l ON (m.location_id = l.id) WHERE m.id = '${dih.delta.id}'"
      deletedPkQuery="SELECT id AS message_id FROM message WHERE disable = 1 AND modified &amp;gt; '${dih.last_index_time}'"
    >
...

deltaQuery属性のSQLで、前回インポート実行時刻以降に追加・修正されたメッセージのidを取得します。
deltaImportQuery属性のSQLで、取得したidのデータを更新分としてインデックスに追加します。

今回のサンプルでは、常にdisable=0のものを登録するようにしているため、disable=1になったものはインデックスから削除する必要が有ります。
それを担当しているのが、deletedPkQueryになります。

DBに新しくデータを追加・修正し、Solrを再起動しましょう。

http://localhost:8983/solr/collection1/dataimport?comamnd=delta-import このURLにアクセスすると、REST APIとして、差分インポートが実行されます。

実行後、フルインポートと同様に検索してみてください。
検索結果に、新しく追加したものが引っ掛かったり、disable=1にしたデータが消えていたらバッチリです。
もし、テキストの内容や、削除したデータが反映されていない場合、modifiedの時刻を修正し忘れていることが考えられます。

これで、差分インポートも完了です。

マルチコアにしてみる

最後に、マルチコアにしてみます。
マルチコアは、複数のコアを同一インスタンス上で動かす事ができるようになる機能です。
今使っているcollection1と同じ物をcollection2として同時に動かしてみましょう。

実は、ものすごく簡単です。

exampleフォルダ内のsolrフォルダ内のcollection1を、同じフォルダにcollection2としてコピーします。
次に、collection2の中の、core.propertiesというファイルを開いて、collection1と書かれているところをcollection2に修正します。

修正したら、Solrを再起動します。

起動したら、以下にアクセスします。
コア管理画面

2つに増えています。完了です。
Ω ΩΩ < ナンダッテー!

増やしたcollection2は、以下のURLで検索する事が出来ます。
http://localhost:8983/solr/#/collection2/query (collection2の検索クエリ発行画面)
これは、フルインポートや差分インポートでも同じで、collection1というコア名が入っていた箇所をcollection2に変えるだけになります。
もし、コア名を変更したりした場合は、collection2の箇所に、変更した名前が入ります。

今回はコピーして設定する手間を省きましたが、collection2内のsolrconfig.xmlやschema.xml, data-config.xml等を別にすることでコア別に検索機能を変える事ができます。

これで、マルチコア化も完了です。

まとめ

今回は、入門編という事で、起動とMySQLからデータを投入する方法、複数コアを同一インスタンスで動かす方法を書いてみました。
冒頭にも追記していますが、記事を書いている間に4.7.0が出ましたが、上記内容はそのまま使える事を確認しています。

最後までお付き合いいただきありがとうございました。

おまけ:今回の内容のSolr(太陽)繋がりで、自分で撮った写真を載っけてみます。

OLYMPUS E-P1 + ZUIKO 50mm f1.4