CLOVER🍀

That was when it all began.

組み込みSolr(EmbeddedSolrServer)を使う

組み込みで使える全文検索エンジンってないのかなぁと思っていたところ、Apache SolrにEmbeddedSolrServerなるものがあると知りまして。

参考)
EmbeddedSolr

Solrj

Using SolrJ | Apache Solr Reference Guide 6.6

Apache Solr を組み込み実行 - なんとなくな Developer のメモ

Solrjサンプルコード集 | mwSoft

https://github.com/Indoqa/solr-spring-client/blob/master/src/main/java/com/indoqa/solr/spring/client/EmbeddedSolrServerBuilder.java

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  129 23:49 ./
drwxr-xr-x 11 xxxxx xxxxx 4096  129 23:49 ../
-rw-r--r--  1 xxxxx xxxxx 3037 128 02:50 README.txt
drwxr-xr-x  5 xxxxx xxxxx 4096 128 02:50 configsets/
drwxrwxr-x  4 xxxxx xxxxx 4096  129 23:49 mycore/
-rw-r--r--  1 xxxxx xxxxx 1887 128 02:50 solr.xml
-rw-r--r--  1 xxxxx xxxxx  501 128 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