LoginSignup
15
15

More than 5 years have passed since last update.

ElasticsearchIntegrationTestを使ってElasticsearchのJUnitテストを書く

Posted at

バージョン情報

  • Elasticsearch 1.7.4

本記事が扱うElasticsearchIntegrationTestはバージョン2系からESIntegTestCaseへと名前が変わっているようです. 伴って大きな変更が入っているかもしれませんので, ご注意ください.

はじめに

概要

この記事はElasticsearch Advent Calendar 2015の3日目のエントリです。

実はElasticsearchはJavaでテストを書くための, ElasticsearchIntegrationTestというクラスを提供しています. が, 日本語の資料がめっちゃ少ない!(というか無い?)ので, 私が導入までに調べたこととかをまとめてみました.

これを使うと, テストを実行したマシン上で簡易Elasticsearchクラスタが立ち上がり, そのクラスタに対してテストケース内からドキュメントを投入したり, 検索したりできるようになります. テスト用にわざわざElasticsearchサーバーを立ち上げる必要がなくなりますよ!

公式ドキュメント (Elasticsearch 1.7)

Elasticsearch公式で触れているのはこのあたりの記事になります.

テストに必要な依存関係

本記事ではMavenを使います. とりあえずelasticsearch 1.7.4ElasticsearchIntegrationTestを試してみるための依存の記述は次のようになります.

pom.xml
<dependencies>
    <dependency>
        <groupId>org.apache.lucene</groupId>
        <artifactId>lucene-test-framework</artifactId>
        <version>4.10.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.carrotsearch.randomizedtesting</groupId>
        <artifactId>randomizedtesting-runner</artifactId>
        <version>2.1.17</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>1.7.3</version>
        <type>test-jar</type>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.elasticsearch</groupId>
        <artifactId>elasticsearch</artifactId>
        <version>1.7.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-all</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
  </dependencies>

記述順序が変わると動かなくなったりするので注意してください.

バージョンの決め方としては, まずelasticsearchのバージョンに自分が使っているものを指定します. <type>test-jar</type>の方も含めると2箇所です. 今回は1.7.3を指定しました.

次に, 使っているElasticsearchが依存しているLuceneのバージョンを調べ, lucene-test-frameworkのバージョンを決めます. EclipseのMaven POM エディターで依存関係を調べたり, Maven Repositoryの当該バージョンのページを見たりするのがいいんじゃないかと思います. 今回は4.10.4を使えばよいことが分かります.

最後に, randomizedtesting-runnerのバージョンを決定するのですが, これは調べ方が分かりませんでした.
lucene-test-framework 4.10.4が依存しているrandomizedtesting-runner 2.1.6を使えばよいのかと思いきや, なんか動きませんでした. 2.1系の最新版である2.1.17を指定したら動きました. 本当は lucene-test-frameworkrandomizedtesting-runner をExcludeした方が良いのかもしれない.

テスト対象コード

以下のような, Elasticsearchクライアントをコンストラクタで受け取り, searchメソッドでごく簡単な検索を行うクラスをテスト対象とします.

src/main/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearch.java
package jp.akimateras.elasticsearch.esintegtest.service;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.index.query.MatchQueryBuilder.Operator;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;

public class SimpleSearch {
    // インデックス・タイプ名定義.
    public static final String INDEX_NAME = "simple-search";
    public static final String TYPE_NAME = "documents";

    // フィールド名定義.
    public static final String TITLE_FIELD_NAME = "title";
    public static final String TEXT_FIELD_NAME = "text";

    // 検索結果の返却型.
    public static class Document {
        public final String title;
        public final String text;

        Document(String title, String text) {
            this.title = title;
            this.text = text;
        }
    }

    // Elasticsearchクライアント.
    private final Client client;

    /**
     * コンストラクタ.
     * 
     * @param client 検索に使用するElasticsearchクライアント
     */
    public SimpleSearch(Client client) {
        this.client = client;
    }

    /**
     * 検索メソッド
     * 
     * @param keywords 検索キーワード. スペース区切りで複数のキーワードを指定した場合はAND検索とする.
     * @return 検索結果.
     */
    public Collection<Document> search(String keywords) throws IOException {
        try {
            // 検索クエリの組み立て.
            MultiMatchQueryBuilder query = QueryBuilders
                .multiMatchQuery(keywords, TITLE_FIELD_NAME, TEXT_FIELD_NAME)
                .operator(Operator.AND);

            // 検索の実行.
            SearchResponse response = client.prepareSearch(INDEX_NAME)
                .setTypes(TYPE_NAME)
                .setQuery(query)
                .execute()
                .get();

            // 検索結果を Collection<Document> に詰め替えてreturnする.
            return Arrays.stream(response.getHits().hits())
                .map(SearchHit::sourceAsMap)
                .map(source -> new Document(
                    (String) source.get(TITLE_FIELD_NAME),
                    (String) source.get(TEXT_FIELD_NAME)
                    ))
                .collect(Collectors.toList());
        } catch(Exception e) {
            throw new IOException(e);
        }
    }
}

シンプルなテスト

上で作ったSimpleSearchクラスを雑にテストしてみましょう.

ElasticsearchIntegrationTestでテストを書くためには,

  1. テストクラスにElasticsearchIntegrationTestを継承させる.
  2. テストメソッド内でclient()を呼び出すとElasticsearchクライアントが取れる.

の2点だけ押さえておけばOKです.

src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
package jp.akimateras.elasticsearch.esintegtest.service;

import static org.hamcrest.Matchers.is;

import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Before;
import org.junit.Test;

@ElasticsearchIntegrationTest.ClusterScope(numDataNodes = 1 /* テスト時に立ち上げるノード数 */)
public class SimpleSearchTest extends ElasticsearchIntegrationTest {
    // テスト用ノードに設定したい内容はこのメソッドの返却値として表現する.
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .build();
    }

    // ここでインデックスを作成する.
    @Before
    public void initializeIndex() throws Exception {
        // マッピング定義.
        // 外部JSONファイルとかでマッピング定義をしているなら, そっちから読んだ方が良いと思う.
        XContentBuilder mapping = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(SimpleSearch.TYPE_NAME)
                    .startObject("properties")
                        .startObject(SimpleSearch.TITLE_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                        .endObject()
                        .startObject(SimpleSearch.TEXT_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();

        // インデックス作成.
        admin().indices() // admin()でアドミンクライアントが取れる!
            .prepareCreate(SimpleSearch.INDEX_NAME)
            .addMapping(SimpleSearch.TYPE_NAME, mapping)
            .execute()
            .actionGet();

        // インデックス作成完了の待機.
        ensureGreen();
    }

    // テスト本体
    @Test
    public void testSearch() throws Exception {
        // ドキュメント投入.
        // これも外部JSONファイルなどを用意して, Bulk APIで投入した方が良いと思う.
        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Alpaca")
                .field(SimpleSearch.TEXT_FIELD_NAME, "An alpaca is a domesticated species of South American camelid.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Llama")
                .field(SimpleSearch.TEXT_FIELD_NAME, "A llama is a domesticated species of South American camelid.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "Vicugna")
                .field(SimpleSearch.TEXT_FIELD_NAME, "A vicugna is a kind of herbivorous mammals that inhabit the Andes of South America.")
            .endObject());

        // ドキュメント投入完了の待機.
        flushAndRefresh();
        ensureGreen();

        // テスト用クライアントに対してSimpleSearchインスタンスを生成.
        SimpleSearch service = new SimpleSearch(client()); // client()でクライアントが取れる!

        // テスト実施.
        // せっかくなので, 依存のおまけで付いてきたhamcrestを使おう.
        assertThat(service.search("alpaca").size(), is(1));
        assertThat(service.search("llama").size(), is(1));
        assertThat(service.search("vicugna").size(), is(1));
        assertThat(service.search("lama").size(), is(0));

        assertThat(service.search("domesticated").size(), is(2));
        assertThat(service.search("alpaca domesticated").size(), is(1));
        assertThat(service.search("llama domesticated").size(), is(1));
        assertThat(service.search("vicugna domesticated").size(), is(0));
    }
}

client()でElasticsearchクライアントが取れるので, 基本的にはもう何でもできますね. インデックス作成などにアドミンクライアントが必要な場合はclient().admin()または, admin()で取得してください.

@Beforeでインデックスを作成していますが, @Afterで自前でインデックスを削除する必要はなく, テストメソッド毎に自動的にまっさらにしてくれるみたいです. 便利.

ensureGreen()flushAndRefresh()など, 普段見かけないメソッドが登場していますが, これらはElasticsearchIntegrationTestが提供してくれる便利メソッドです. たとえば ensureGreen() を自前で書くとすると,

client().admin().cluster().prepareHealth()
    .setWaitForGreenStatus()
    .execute()
    .actionGet();

みたいにちょっと長くなります. もちろん, この手の操作をテストするために, 自前で書いてもOKです. 便利メソッドの一覧は公式: integration tests (1.7)を参照.

ここまでで動かないとき

私が踏んだエラーのログとその対処を書いておきます.

型 <クラス名> の階層は不整合です

Eclipseエラー
型 <クラス名> の階層は不整合です
The hierarchy of the type '<Class Name>' is inconsistent

Elasticsearchのテストフレームワークのjarは見えており, かつ, randomizedtesting-runnerのjarが見えていない時に出たエラーです. EclipseでElasticsearchIntegrationTestをextendsしたテストクラスに赤線が引かれ, 上記エラーメッセージが出ます. 依存が正しく書けているかの確認や, プロジェクトのリフレッシュ/クリーンなど試してみてください.

Test class requires enabled assertions

実行時出力
java.lang.Exception: Test class requires enabled assertions, enable globally (-ea) or for Solr/Lucene subpackages only: jp.akimateras.elasticsearch.esintegtest.test.SampleTest
    __randomizedtesting.SeedInfo.seed([E05E1B5BAB0E8E6]:0)
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:38)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)

ElasticsearchIntegrationTestを使う場合, テスト実行時にアサーションが有効になっていないと駄目なようです. JVMの起動オプションに-eaを付けてあげてください.
Eclipseからテストを実行する場合は, [実行の構成]で対象の構成オプションを開き, [引数]タブの[VM 引数]テキストエリアに-eaと書いてあげればOKです.

tests-framework.jarとlucene-core.jarがどうのこうの

実行時出力
java.lang.AssertionError: fix your classpath to have tests-framework.jar before lucene-core.jar
    __randomizedtesting.SeedInfo.seed([C7B320FCEA3DEB94]:0)
    org.apache.lucene.util.TestRuleSetupAndRestoreClassEnv.before(TestRuleSetupAndRestoreClassEnv.java:211)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:45)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)

pom.xmlの記述順序が間違っています. Javaのクラスパスの順序が問題になるのかな...?
randomizedtesting-runnerへの依存の前にElasticsearchへの依存を書くと発生するようです. Javaに詳しくないので理由はよく分かりませんが, 記述順序で動かないことがあるのはちょっと嫌な感じですね.

randomizedtestingでNoSuchMethodErrorが出る

実行時出力
java.lang.NoSuchMethodError: com.carrotsearch.randomizedtesting.RandomizedContext.runWithPrivateRandomness(Lcom/carrotsearch/randomizedtesting/Randomness;Ljava/util/concurrent/Callable;)Ljava/lang/Object;
    __randomizedtesting.SeedInfo.seed([173369D720B9A36A:9F67560D8E45CE92]:0)
    org.elasticsearch.test.ElasticsearchIntegrationTest.buildWithPrivateContext(ElasticsearchIntegrationTest.java:583)
    org.elasticsearch.test.ElasticsearchIntegrationTest.buildAndPutCluster(ElasticsearchIntegrationTest.java:598)
    org.elasticsearch.test.ElasticsearchIntegrationTest.beforeInternal(ElasticsearchIntegrationTest.java:283)
    org.elasticsearch.test.ElasticsearchIntegrationTest.before(ElasticsearchIntegrationTest.java:1946)
    [...sun.*, org.junit.*, java.lang.reflect.*, com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:50)
    org.apache.lucene.util.TestRuleFieldCacheSanity$1.evaluate(TestRuleFieldCacheSanity.java:51)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleThreadAndTestName$1.evaluate(TestRuleThreadAndTestName.java:49)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)

単純にrandomizedtesting-runnerへの依存を書き忘れている場合に加え, randomizedtesting-runnerのバージョン指定が古い場合にも発生するようです. elasticsearch 1.7.3を使った際は, randomizedtesting 2.1.6ではこのエラーになり, randomizedtesting 2.1.17では正常に動きました.

hamcrestでNoClassDefFoundErrorが出る

実行時出力
java.lang.NoClassDefFoundError: org/hamcrest/Matchers
    __randomizedtesting.SeedInfo.seed([3F50D3975E9C4C58:B704EC4DF06021A0]:0)
    org.elasticsearch.test.ElasticsearchIntegrationTest.afterInternal(ElasticsearchIntegrationTest.java:628)
    org.elasticsearch.test.ElasticsearchIntegrationTest.after(ElasticsearchIntegrationTest.java:1954)
    [...sun.*, org.junit.*, java.lang.reflect.*, com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:50)
    org.apache.lucene.util.TestRuleFieldCacheSanity$1.evaluate(TestRuleFieldCacheSanity.java:51)
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleThreadAndTestName$1.evaluate(TestRuleThreadAndTestName.java:49)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:46)
    org.apache.lucene.util.TestRuleStoreClassName$1.evaluate(TestRuleStoreClassName.java:42)
    [...com.carrotsearch.randomizedtesting.*]
    org.apache.lucene.util.TestRuleAssertionsRequired$1.evaluate(TestRuleAssertionsRequired.java:43)
    org.apache.lucene.util.TestRuleMarkFailure$1.evaluate(TestRuleMarkFailure.java:48)
    org.apache.lucene.util.TestRuleIgnoreAfterMaxFailures$1.evaluate(TestRuleIgnoreAfterMaxFailures.java:65)
    org.apache.lucene.util.TestRuleIgnoreTestSuites$1.evaluate(TestRuleIgnoreTestSuites.java:55)
    [...com.carrotsearch.randomizedtesting.*]
    java.lang.Thread.run(Unknown Source)

ドキュメントには書かれていなかったと思うのですが, hamcrestが必要なようです. elasticsearch 1.7.3を使った際は, hamcrest 1.3で動きました.

プラグインが必要な機能のテスト

シンプルなテストは動いたでしょうか. 実は, ここからが本番です.
Elasticsearchで日本語を扱おうとした場合, 日本語アナライザなどのプラグインがないと何も始まりません. テストを走らせるために自前でElasticsearchサーバーを立ち上げていると, プラグイン周りの管理が面倒だったりするので, このあたりからElasticsearchIntegrationTestのありがたみが出てきます.

サーバーとして立ち上がっているElasticsearchの中でプラグインを使おうとした場合, $ bin/plugin --install <使用したいプラグイン> とかを実行したと思います. 一方で, ElasticsearchIntegrationTestの中でプラグインを使う場合は,

  1. 使用したいプラグインを依存関係に追加する.
  2. ElasticsearchIntegrationTestクラスから継承したnodeSettingsメソッドをオーバーライドして, プラグインを使用する旨を記述する.

の2つをやればOKです.

使用したいプラグインを依存関係に追加する

今回は日本語アナライザプラグインとしてお馴染みのelasticsearch-analysis-kuromojiを使ってみましょう. pom.xmlに次の依存を追加します.

pom.xml
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch-analysis-kuromoji</artifactId>
    <version>2.7.0</version>
</dependency>

Elasticsearchとkuromojiプラグインとのバージョンの対応は, サーバーにインストールするときと同様に, elasticsearch-analysis-kuromojiのGitHubで確認できます. 今回はes-1.7に対応するelasticsearch-analysis-kuromoji 2.7.0を使えばよいと分かります.

nodeSettingsをオーバーライドしてプラグインを使用する旨を記述する

これには2通りやり方があります.

クラスパスに存在するプラグインをすべて読み込む

java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, true)
            .build();
    }

"plugins."に続けてorg.elasticsearch.plugins.PluginsService.LOAD_PLUGIN_FROM_CLASSPATHで指定される文字列定数を連結した設定名に対してtrueを与えると, クラスパス上に存在するすべてのプラグインを読みに行くようになります.
ちなみに, elasticsearch 1.7.3ではこの設定名は"plugins.load_classpath_plugins"と展開されるようです.

読み込むプラグインを指定する

java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .build();
    }

"plugin.types"にプラグインのクラス名を与えると, 与えた名前のプラグインが読み込まれます. テスト時に何のプラグインが必要なのかが明示されるので, 個人的にはこっちの方が好きです.

複数のプラグインを読み込ませたい場合は, putメソッドの代わりにputArrayメソッドを使って次のように書きます.

java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .putArray("plugin.types",
                AnalysisKuromojiPlugin.class.getName(),
                AnalysisICUPlugin.class.getName()) // elasticsearch-analysis-icu プラグイン
            .build();
    }

なお, elasticsearch 2.1からはnodePluginsというプラグインを指定するための専用メソッドをオーバーライドするように変更されているようです( 公式: integration tests (2.1) ).

日本語を使ったテストケース

実際にkuromojiのプラグインが読み込めていることを確認してみましょう.

src/main/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearch.java
package jp.akimateras.elasticsearch.esintegtest.service;

import static org.hamcrest.Matchers.is;

import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.plugin.analysis.kuromoji.AnalysisKuromojiPlugin;
import org.elasticsearch.test.ElasticsearchIntegrationTest;
import org.junit.Before;
import org.junit.Test;

@ElasticsearchIntegrationTest.ClusterScope(numDataNodes = 1)
public class SimpleSearchTest extends ElasticsearchIntegrationTest {
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .build();
    }

    @Before
    public void initializeIndex() throws Exception {
        // インデックス作成.
        XContentBuilder mapping = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(SimpleSearch.TYPE_NAME)
                    .startObject("properties")
                        .startObject(SimpleSearch.TITLE_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                            .field("analyzer", "kuromoji") // kuromojiがないとこれがエラーになる.
                        .endObject()
                        .startObject(SimpleSearch.TEXT_FIELD_NAME)
                            .field("type", "string")
                            .field("source", true)
                            .field("analyzer", "kuromoji")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();

        admin().indices().prepareCreate(SimpleSearch.INDEX_NAME)
            .addMapping(SimpleSearch.TYPE_NAME, mapping)
            .execute()
            .actionGet();

        // インデックス作成完了の待機.
        ensureGreen();
    }

    @Test
    public void testSearch() throws Exception {
        // ドキュメント投入. 日本語を使います.
        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "アルパカ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "アルパカは, 南アメリカのラクダ科動物の家畜です.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "リャマ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "リャマは, 南アメリカのラクダ科動物の家畜です.")
            .endObject());

        index(SimpleSearch.INDEX_NAME, SimpleSearch.TYPE_NAME, XContentFactory.jsonBuilder()
            .startObject()
                .field(SimpleSearch.TITLE_FIELD_NAME, "ビクーニャ")
                .field(SimpleSearch.TEXT_FIELD_NAME, "ビクーニャは, 南アメリカのアンデス山脈に生息する草食哺乳類です.")
            .endObject());

        // ドキュメント投入完了の待機.
        flushAndRefresh();
        ensureGreen();

        // テスト用 client を使ってSimpleSearchインスタンスを生成.
        SimpleSearch service = new SimpleSearch(client());

        // テスト実施
        assertThat(service.search("アルパカ").size(), is(1));
        assertThat(service.search("リャマ").size(), is(1));
        assertThat(service.search("ビクーニャ").size(), is(1));
        assertThat(service.search("ラマ").size(), is(0));

        assertThat(service.search("家畜").size(), is(2));
        assertThat(service.search("アルパカ 家畜").size(), is(1));
        assertThat(service.search("リャマ 家畜").size(), is(1));
        assertThat(service.search("ビクーニャ 家畜").size(), is(0));
    }
}

どうでもいいんですが, このテスト, kuromojiを使わずともデフォルトのstandard analyzerでも全部通ります...なかなか優秀ですね.
投入文書のタイトルとテキスト両方の"アルパカ""アルパカアルパカ"とかに変えてあげると, standard analyzerはこれを1語として認識するので, "アルパカ"で検索しても引っかからなくなります. kuromojiを使っている場合は"アルパカ"x2と認識するので, "アルパカ"で検索できます.

スクリプトが必要な機能のテスト

最後にgroovyとかのディスクスクリプトを必要とする機能のテスト方法を紹介します.

Elasticsearchのディスクスクリプトは, サーバーとして起動する場合は/etc/elasticsearch/scriptsのような場所に置くと自動的に読み込まれるものでした.
ElasticsearchIntegrationTestからスクリプトを見えるようにするには, サーバーのときと同様に特定のディレクトリにスクリプト群を設置し, nodeSettingsメソッドで設置場所を記述すればOKです.

ただし, elasticsearch 1.7.3ではスクリプトディレクトリを直接指定することができません. "path.conf"で指定できる設定ディレクトリ直下のscriptsという名前のディレクトリを決め打ちで探しに行きます.
そのため, 例えばsrc/test/resources/scriptsというディレクトリを読みに行かせたい場合は, "path.conf""src/test/resources"を指定する, といったやり方になります.

src/test/java/jp/akimateras/elasticsearch/esintegtest/service/SimpleSearchTest.java
    @Override
    protected Settings nodeSettings(int nodeOrdinal) {
        return ImmutableSettings.settingsBuilder()
            .put(super.nodeSettings(nodeOrdinal))
            .put("plugin.types", AnalysisKuromojiPlugin.class.getName())
            .put("path.conf", "src/test/resources")
            .build();
    }

なお. elasticsearch 2.1からは"path.script"という設定名でスクリプトディレクトリを直接指定できるように改善されているようです( 公式: Directory Layout (2.1) ).

おわりに

四苦八苦してテストフレームワークがやっと動いたよという記事ですので, 間違いなどありましたらご指摘を宜しくお願い致します...
苦労はしたものの, これのおかげでテスト用Elasticsearchサーバーの管理が不要になったのはかなり嬉しく, 導入した甲斐はあったと感じています.

明日は, takakikuさんのMySQLスローログ可視化の記事です!

15
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
15