saba1024のブログ

どうでも良い思いついた事とかプログラミング関係のメモとか書いていきます。

Apache GroovyとGrailsでApache Solrを利用する(組み込み)

これはG* Advent Calendar 2017の1日目の記事です。

さて、検索エンジンといえばElasticSearchの勢いが凄いですが、やはりApache Groovyで利用するなら同じApacheファミリーのApache Solrですよね!
ただ、RESTful APIを利用してSolrとやりとりするともうソレでネタが終了、という事情もありつつ、態々Apache Solr自体をサーバとして起動したくない、出来ない状態も当然有ると思いますので、今回Apache Solrを組み込みモードでApache Groovy、そしてGrailsから利用する方法をまとめてみました。

Apache Solrの準備

インストール

この記事を書いている時点で最新は7.1なのですが、どうも7.0から検索のための条件指定などの仕様がだいぶ変わっているようで、まだキャッチアップできていないので今回はApache Solr 6.6を利用します。
インストールは簡単で、単純にzipを落としてきて解凍するだけです。

[koji:advent]$ pwd
/home/koji/work/advent
[koji:advent]$ wget http://archive.apache.org/dist/lucene/solr/6.6.2/solr-6.6.2.zip
[koji:advent]$ unzip solr-6.6.2.zip
[koji:advent]$ cd solr-6.6.2
[koji:solr-7.1.0]$ pwd
/home/koji/work/advent/solr-6.6.2

起動

では実際に起動してみましょう。
bin/solrを使って、Solrの制御を行います。
起動するにはsolr startを実行します。

[koji:solr-6.6.2]$ bin/solr start
Waiting up to 180 seconds to see Solr running on port 8983 [\]  
Started Solr server on port 8983 (pid=29077). Happy searching!

[koji:solr-6.6.2]$ 

これでApache Solrが起動しました。
http://localhost:8983にブラウザでアクセスすると、Apache Solrの管理画面が確認できます。

停止

同様に、solrコマンドを使います。
solr stop -allを実行します。

[koji:solr-6.6.2]$ bin/solr stop -all
Sending stop command to Solr running on port 8983 ... waiting up to 180 seconds to allow Jetty process 29077 to stop gracefully.
[koji:solr-6.6.2]$

SOLR_HOMEの作成

デフォルトだと、Solrのインストールディレクトリ/server/solrが、SOLR_HOMEとなります。
今回の私の環境であれば/home/koji/work/advent/solr-6.6.2/server/solrです。
SOLR_HOMEには、Apache Solr全体の設定(起動ポートとか)を管理するsolr.xmlを設置して、実際のSolrコア(インデックス)はこのSOLR_HOME配下に作成されることになります。

折角なので自分でSOLR_HOMEを作ってみます。
とりあえず、Apache Solrのインストールディレクトリに、my_homeディレクトリを作って、そこにデフォルトのsolr.xmlをコピーして起動します。

# ディレクトリを作ってデフォルトのsolr.xmlをコピー
[koji:solr-6.6.2]$ pwd
/home/koji/work/advent/solr-6.6.2
[koji:solr-6.6.2]$ mkdir my_home
[koji:solr-6.6.2]$ cp server/solr/solr.xml my_home/.
[koji:solr-6.6.2]$ 

# -sオプションを使って、自分で作ったSOLR_HOMEでApache Solrを起動
[koji:solr-6.6.2]$ bin/solr start -s my_home 
Waiting up to 180 seconds to see Solr running on port 8983 [\]  
Started Solr server on port 8983 (pid=30227). Happy searching!

[koji:solr-6.6.2]$ 

これでオリジナルのSOLR_HOMEが作成できました。

コアの作成

次に、実際に検索対象となるデータを保存するコアを作成します。
DBMSで言うところのデータベースに当たります。
管理コマンドsolrを使って作成します。
具体的にはsolr create -c コア名になります。
作成されたコアは、起動時に指定したSOLR_HOME配下に作成されます。
では、todoという名前でコアを作成してみます。

[koji:solr-6.6.2]$ pwd
/home/koji/work/advent/solr-6.6.2
[koji:solr-6.6.2]$ bin/solr create -c todo

Copying configuration to new core instance directory:
/home/koji/work/advent/solr-6.6.2/my_home/todo

Creating new core 'todo' using command:
http://localhost:8983/solr/admin/cores?action=CREATE&name=todo&instanceDir=todo

{
  "responseHeader":{
    "status":0,
    "QTime":1367},
  "core":"todo"}


[koji:solr-6.6.2]$ cd my_home 
[koji:my_home]$ ls
solr.xml  todo #<---- todo ディレクトリが作成されている
[koji:my_home]$ 

これでコアの作成は完了です。

フィールドの作成

さて、SOLR_HOMEとコア作成が完了しました。次に実際に検索データのフィールドを定義していきます。
RDBMSで言うところのテーブルとカラムみたいな感じです。
管理画面からGUIで設定することも可能ですが、ココは標準のRESTful API経由で操作してみます。

curl -X POST -H 'Content-type: application/json' -d '
{"add-field": {
    "name": "title", "type": "text_ja", "indexed": "true", "stored": "true"
}}' http://localhost:8983/solr/todo/schema

curl -X POST -H 'Content-type: application/json' -d '
{"add-field": {
    "name": "tag", "type": "string", "indexed": "true", "stored": "true", "multiValued": "true"
}}' http://localhost:8983/solr/todo/schema

titletagというフィールドを追加しました。
titleがToDoのタイトルで、そのToDoは複数のtagを持てる感じです。
このフィールドタイプの設定などは非常に複雑なので公式ドキュメントなどを参照してください。
なお、自動的にidというフィールドがApache Solrによって作成されています。

インデックスの登録

同様に専用のAPI経由でテストデータを登録してみます。

curl -X POST -H 'Content-Type: application/json' -d '[{"id": "1", "title":"アドベントカレンダー2018登録", "tag":["タグ1", "タグ2"]}]' http://localhost:8983/solr/todo/update?commit=true
curl -X POST -H 'Content-Type: application/json' -d '[{"id": "2", "title":"卵を買って帰る", "tag":["家事"]}]' http://localhost:8983/solr/todo/update?commit=true
curl -X POST -H 'Content-Type: application/json' -d '[{"id": "3", "title":"抱負を決める", "tag":["2018"]}]' http://localhost:8983/solr/todo/update?commit=true

コレにてApache Solrの準備は完了です。

Groovyから組み込みSolrを利用する

@Grab('org.apache.solr:solr-core:6.6.2')
@Grab('org.slf4j:slf4j-simple:1.7.12')
import org.apache.solr.core.CoreContainer
import org.apache.solr.client.solrj.SolrQuery
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer
import org.apache.solr.client.solrj.response.QueryResponse

Closure displayResult = {QueryResponse qr ->
    qr.results.each {
        println it
    }
}

String solrHome = "/home/koji/work/advent/solr-6.6.2/my_home"
def cores = new CoreContainer(solrHome)
cores.load()

String coreName = "todo"
def server = new EmbeddedSolrServer(cores, coreName)

// 全件取得のクエリ
// http://localhost:8983/solr/todo/select?defType=dismax&q=2018&qf=tag+title&wt=json

def q = new SolrQuery("抱負")

// もし細かくパラメータを自分で指定したいなら以下のようにsetメソッドで指定できる
//q.set("q", "抱負")
//q.set("defType", "dismax") // qfを使うために。
//q.set("qf", "id title tag") // 検索対象のフィールド。7系以降は手動で指定しないとダメっぽい?

displayResult(server.query(q))

// 検索条件を変える
q = new SolrQuery("2018")
displayResult(server.query(q))

// 形態素解析も大丈夫
q = new SolrQuery("帰る")
displayResult(server.query(q))

// ちゃんとCloseしましょう
server.close()

上記のコードを実行すれば、Apache Solrが組み込みモードで起動してGroovyから利用できます。
特に難しい部分はありません。

Grabで実際に利用するSolrを指定してあげます。

@Grab('org.apache.solr:solr-core:6.6.2')

そして、作成したSOLR_HOMEとコアを指定してあげます。

String solrHome = "/home/koji/work/advent/solr-6.6.2/my_home"
def cores = new CoreContainer(solrHome)
cores.load()

String coreName = "todo"
def server = new EmbeddedSolrServer(cores, coreName)

後は基本的にSolrQueryのインスタンスで検索条件等を作成して、それをEmbeddedSolrServerインスタンスのquery()メソッドに渡してあげるだけです。

なお、Apache Solr7.0以降、どうも検索条件に指定する方法が変わったらしく、コメントに示しているようにワザワザ検索対象フィールド名を指定しないといけなくなったようです。

Grailsから組み込みSolrを利用する

ではGrailsからApache Solrを組み込みモードで利用してみます。
やることは当然Groovyの方と同じなのですが、Grails自体がサーバとして動いているので、当然Grailsの起動/終了時に併せてApache Solrの起動と停止もして上げる必要が有ります。

今回、Grails 3.3.1で検証しました。

[koji:advent]$ grails create-app advent2017

依存関係の指定

build.gradledependenciesに、Solr-CoreとSolrjを追加します。

dependencies {
    ...
        compile 'org.apache.solr:solr-core:6.6.2'
        compile 'org.apache.solr:solr-solrj:6.6.2'
}

ココがGroovyスクリプトの方と少し違うところで、Grails 3.3.1の場合はsolr-solrjの方も指定して上げる必要が有ります。
これは、どうもGrailsが乗っているSpringBootが使っているstarter-data**みたいなライブラリが古いSolrjをデフォルトで指定しているためのようです。

Apache Solrを扱うクラス

実際にSolrを組み込みモードで起動して、検索などの操作を行うクラスを用意します。
src/advent2017/solr配下にSolrServer.groovyとして、以下のコードを用意します。
内容自体はGroovyスクリプトの方と基本的に同じです。

package advent2017.solr

import org.apache.solr.client.solrj.SolrQuery
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer
import org.apache.solr.client.solrj.response.QueryResponse
import org.apache.solr.common.SolrInputDocument
import org.apache.solr.core.CoreContainer

class SolrServer {

    EmbeddedSolrServer embeddedSolrServer
    String solrHome
    String solrCoreName

    SolrServer(String solrHome, String solrCoreName) {
        this.solrHome = solrHome
        this.solrCoreName = solrCoreName
    }

    def start() {
        def cores = new CoreContainer(solrHome)
        cores.load()
        embeddedSolrServer = new EmbeddedSolrServer(cores, solrCoreName)
    }

    def stop() {
        embeddedSolrServer.close()
    }
    def commit() {
        embeddedSolrServer.commit()
    }

    QueryResponse query(SolrQuery solrQuery) {
        embeddedSolrServer.query(solrQuery)
    }

    QueryResponse search(String query, Integer rows = 10) {
        def solrQuery = new SolrQuery(query)
        solrQuery.setRows(rows)
        embeddedSolrServer.query(solrQuery)
    }

    def save(SolrInputDocument solrInputDocument) {
        embeddedSolrServer.add(solrInputDocument)
        commit()
    }

    def save(List<SolrInputDocument> solrInputDocumentList) {
        solrInputDocumentList.each {
            embeddedSolrServer.add(it)
        }
        commit()
    }
}

Apache Solrを扱うクラスの管理

作成したSolrServerGrails/SpringにDIしてもらえるようにします。
Grails3なので、@Component等を指定してもいいのですが個人的にresoruces.groovyの方が好きなので、reseources.groovyに以下を追記します。

import advent2017.solr.SolrServer

beans = {
    solrServer(SolrServer, "/home/koji/work/advent/solr-6.6.2/my_home", "todo")
}

組み込みApache Solrの起動と終了

さて、Grailsなので当然Grails起動中はずっとApache Solrに組み込みモードで起動していてもらう必要が有ります。
そしてGrails終了時にはキチンとApache Solrにも終了してもらはなければなりません。
ということでBootstrap.groovyを以下のようにします。

package advent2017

class BootStrap {

    def solrServer

    def init = { servletContext ->
        solrServer.start()
    }
    def destroy = {
        solrServer.stop()
    }
}

確認用コードの追加

最後に、実際に検索するControllerとviewを作成します。

grails> create-controller index

controller:

package advent2017

import org.apache.solr.client.solrj.response.QueryResponse
import org.apache.solr.common.SolrDocument

class IndexController {

    def solrServer

    def index() {
        forward action: "search"
    }

    def search() {
        String searchQuery = params.query

        if (!searchQuery) {
            render view: "index", model: [searchQuery: "", records: []]
            return
        }

        QueryResponse queryResponse = solrServer.search(searchQuery)

        List<SolrDocument> records = queryResponse.results.collect {
            it
        }

        render view: "index", model: [searchQuery: searchQuery, count: queryResponse.results.numFound, records: records]
    }
}

view:

<!doctype html>
<html>
<head>
    <meta name="layout" content="main"/>
    <title>G* アドカレ2017</title>
    <asset:link rel="icon" href="favicon.ico" type="image/x-ico" />
</head>
<body>
    <div id="content" role="main">
        <section class="row colset-2-its">
            <h1>G* アドカレ2017</h1>
            <g:form controller="index" action="search" class="centered">
                <g:textField name="query" value="${searchQuery}" />
                <g:submitButton name="検索" />
            </g:form>
            <p>以下に検索結果をそのまま出すよ</p>
            <ul>
            <g:each in="${records}" var="record">
                <li>${record}</li>
            </g:each>
            </ul>
        </section>
    </div>
</body>
</html>

コレで準備は完了です!
実際にGrailsを起動してhttp://localhost:8080/indexにアクセスすれば、Grailsが組み込みモードでSolrを利用できていることが確認できます。

まとめ

駆け足になりましたが、Apache GroovyとGrailsで組み込みモードでApache Solrを利用する方法をご紹介しました。
インデックスを登録、更新、削除したりするコードも載せたかったのですが時間切れということで。。。時間を見つけてこの記事に追記したいと思います。