組み込みで使える全文検索エンジンってないのかなぁと思っていたところ、Apache SolrにEmbeddedSolrServerなるものがあると知りまして。
参考)
EmbeddedSolr
Using SolrJ | Apache Solr Reference Guide 6.6
Apache Solr を組み込み実行 - なんとなくな Developer のメモ
TestSolrEmbeddedServer.java · GitHub
ちょっと調べてみた感じ、solr homeとかsolrconfig.xmlとかを指定して使うものみたいです。
せっかくなので、試してみましょう。
準備
プログラムは、Scalaで書きます。sbtの設定は、以下の通り。
build.sbt
name := "solr-embedded-server" version := "0.0.1-SNAPSHOT" scalaVersion := "2.11.7" organization := "org.littlewings" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) libraryDependencies ++= Seq( "org.apache.solr" % "solr-core" % "5.4.1" excludeAll( ExclusionRule("org.apache.hadoop"), ExclusionRule("org.eclipse.jetty") ), "org.scalatest" %% "scalatest" % "2.2.6" % "test" )
けっこうな数の依存関係が入ってきたので、要らないだろうと思ってHadoopとJettyは外してみました…。今回は問題が起きませんでしたけど、何かあったら戻します。
ScalaTestは、テストコード用です。
テストコードの雛形
今回のサンプルで使うテストコードの雛形は、こんな感じ。
src/test/scala/org/littlewings/solr/EmbeddedSolrServerSpec.scala
package org.littlewings.solr import java.io.File import java.nio.file.{Files, Paths} import java.util.Properties import org.apache.commons.io.FileUtils import org.apache.solr.client.solrj.SolrQuery import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer import org.apache.solr.common.SolrInputDocument import org.apache.solr.core.{CoreContainer, CoreDescriptor, NodeConfig, SolrResourceLoader} import org.scalatest.{FunSpec, Matchers} class EmbeddedSolrServerSpec extends FunSpec with Matchers { describe("Embedded Solr Server Spec") { // ここに、テストコードを書く! } protected def createTemporaryDir(dirName: String): String = Files.createTempDirectory(Paths.get(System.getProperty("java.io.tmpdir")), dirName).toString protected def toAbsolutePath(path: String): String = new File(path).getAbsolutePath }
Solrのコアを作る
EmbeddedSolrServerを使うには、事前にSolrのコアに関する情報が必要です。
なので、いったん通常のSolrを使ってコアを作成し、設定ファイルなどを用意しておきます。今回作成するコアの名前は、「mycore」とします。
$ solr-5.4.1/bin/solr start $ solr-5.4.1/bin/solr create -c mycore
ここで作った、このあたりのディレクトリやファイルを利用します。
$ ll solr-5.4.1/server/solr 合計 28 drwxr-xr-x 4 xxxxx xxxxx 4096 1月 29 23:49 ./ drwxr-xr-x 11 xxxxx xxxxx 4096 1月 29 23:49 ../ -rw-r--r-- 1 xxxxx xxxxx 3037 12月 8 02:50 README.txt drwxr-xr-x 5 xxxxx xxxxx 4096 12月 8 02:50 configsets/ drwxrwxr-x 4 xxxxx xxxxx 4096 1月 29 23:49 mycore/ -rw-r--r-- 1 xxxxx xxxxx 1887 12月 8 02:50 solr.xml -rw-r--r-- 1 xxxxx xxxxx 501 12月 8 02:50 zoo.cfg
solr homeを指定して使う
まずは、比較的単純なパターン。solr homeを使って、EmbeddedSolrServerを使います。
先ほど作成したsolr homeを、適当なディレクトリにコピーします。今回は、src/test/resources配下に「embedded-solr-home」として配置しました。
$ cp -R /path/to/solr-5.4.1/server/solr src/test/resources/embedded-solr-home
このディレクトリを、CoreContainerのコンストラクタに与えてインスタンスを生成します。CoreContainer#load後、EmbeddedSolrServerのコンストラクタに渡すと共に、先ほど作成した「mycore」を指定します。
it("spec solr-home") { val coreContainer = new CoreContainer("src/test/resources/embedded-solr-home") coreContainer.load() val solrServer = new EmbeddedSolrServer(coreContainer, "mycore")
あとは、SolrjのAPIでドキュメントを登録したり
val doc1 = new SolrInputDocument doc1.addField("id", "100") doc1.addField("text_txt_ja", "東京都へ遊びに行く") solrServer.add(doc1) val doc2 = new SolrInputDocument doc2.addField("id", "200") doc2.addField("text_txt_ja", "すもももももももものうち") solrServer.add(doc2) solrServer.commit()
検索したりできます。
val solrQuery1 = new SolrQuery solrQuery1.setQuery("text_txt_ja:東京へ行こう") val response1 = solrServer.query(solrQuery1) val docList1 = response1.getResults docList1 should have size (1) docList1.get(0).get("text_txt_ja") should be("東京都へ遊びに行く") val solrQuery2 = new SolrQuery solrQuery2.setQuery("text_txt_ja:すもも") val response2 = solrServer.query(solrQuery2) val docList2 = response2.getResults docList2 should have size (1) docList2.get(0).get("text_txt_ja") should be("すもももももももものうち")
最後に、EmbeddedSolrServerをクローズします。
solrServer.close() }
EmbeddedSolrServer#closeの中では、CoreContainer#shutdownを呼び出しています。
インデックスは、コアのディレクトリ内にある「data」ディレクトリに保存されます。通常のSolrと同じですね。
ちなみに、
doc1.addField("text_txt_ja", "東京都へ遊びに行く")
みたいに「_txt_ja」としておくと、デフォルトのコアの状態だとKuromojiで形態素解析してくれます。managed-schemaがこういう定義なっていますからね。
<dynamicField name="*_txt_ja" type="text_ja" indexed="true" stored="true"/> <fieldType name="text_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false"> <tokenizer class="solr.JapaneseTokenizerFactory" mode="search"/> <!--<tokenizer class="solr.JapaneseTokenizerFactory" mode="search" userDictionary="lang/userdict_ja.txt"/>--> <!-- Reduces inflected verbs and adjectives to their base/dictionary forms (辞書形) --> <filter class="solr.JapaneseBaseFormFilterFactory"/> <!-- Removes tokens with certain part-of-speech tags --> <filter class="solr.JapanesePartOfSpeechStopFilterFactory" tags="lang/stoptags_ja.txt" /> <!-- Normalizes full-width romaji to half-width and half-width kana to full-width (Unicode NFKC subset) --> <filter class="solr.CJKWidthFilterFactory"/> <!-- Removes common tokens typically not useful for search, but have a negative effect on ranking --> <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_ja.txt" /> <!-- Normalizes common katakana spelling variations by removing any last long sound character (U+30FC) --> <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/> <!-- Lower-cases romaji characters --> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> </fieldType>
コア内のファイルを指定して使う
先ほどは、solr homeを指定してEmbededSolrServerを使いましたが、今度はコア内のファイルを使ってもうちょっとカスタマイズして使ってみたいと思います。
dataはコア内に保存せず、一時ディレクトリ内に保存し、終了後に削除することとします。
ちなみに、先ほどのコードの続きに書く場合は、先ほどのsolr homeがあると、ここでCoreContainerがsolr homeにあるコアを見つけてしまって面倒なことになるので削除しておきます。
$ rm -rf src/test/resources/embedded-solr-home
まず、作ったコアに関するディレクトリの内容だけコピーします。
$ cp -R /path/to/solr-5.4.1/server/solr/mycore src/test/resources/embedded-solr-core
$ rm -rf src/test/resources/embedded-solr-core/data
$ rm src/test/resources/embedded-solr-core/core.properties
dataディレクトリと、core.propertiesは不要なので削除しておきます。
こちらの場合は、SolrResourceLoaderとNodeConfigを使ってCoreContainerを作成します。
it("spec core") { val solrResourceLoader = new SolrResourceLoader(Paths.get(".")) val nodeConfig = new NodeConfig.NodeConfigBuilder(null, solrResourceLoader).build val coreContainer = new CoreContainer(nodeConfig) coreContainer.load()
dataディレクトリと、solrconfig.xmlをPropertiesで指定します。dataディレクトリは、一時ディレクトリとします。
作成したCoreContainerからCoreDescriptor、そしてSolrCoreを作成して、これらを使いEmbeddedSolrServerを生成します。
val coreDescriptor = new CoreDescriptor(coreContainer, "embedded-solr", "src/test/resources/embedded-solr-core", properties) val solrCore = coreContainer.create(coreDescriptor) val solrServer = new EmbeddedSolrServer(solrCore)
CoreDescriptorの第2引数はCoreDescriptorの名前、第3引数はコアのディレクトリ?みたいです。
あとは、先ほどと同様に使って
val doc1 = new SolrInputDocument doc1.addField("id", "100") doc1.addField("text_txt_ja", "東京都へ遊びに行く") solrServer.add(doc1) val doc2 = new SolrInputDocument doc2.addField("id", "200") doc2.addField("text_txt_ja", "すもももももももものうち") solrServer.add(doc2) solrServer.commit() val solrQuery1 = new SolrQuery solrQuery1.setQuery("text_txt_ja:東京へ行こう") val response1 = solrServer.query(solrQuery1) val docList1 = response1.getResults docList1 should have size (1) docList1.get(0).get("text_txt_ja") should be("東京都へ遊びに行く") val solrQuery2 = new SolrQuery solrQuery2.setQuery("text_txt_ja:すもも") val response2 = solrServer.query(solrQuery2) val docList2 = response2.getResults docList2 should have size (1) docList2.get(0).get("text_txt_ja") should be("すもももももももものうち")
シャットダウンと一時ディレクトリを削除しておしまい。
solrServer.close()
FileUtils.deleteDirectory(new File(temporaryDataDir))
}
まとめ
ちょっと癖はありますが、組み込みでSolrを使えることがわかりました。自分としては、solr homeよりはコアの範囲くらいで使うのがいいのかなーと思います。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/lucene-examples/tree/master/solr-embedded-server