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
title
とtag
というフィールドを追加しました。
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.gradle
のdependencies
に、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を扱うクラスの管理
作成したSolrServer
をGrails/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を利用する方法をご紹介しました。
インデックスを登録、更新、削除したりするコードも載せたかったのですが時間切れということで。。。時間を見つけてこの記事に追記したいと思います。