Tagbangers Blog

Hibernate Search + Elasticsearch を試してみる (Docker 版)

Hibernate Search 5.6 から Elasticsearch がサポートされました。
Hibernate Search は全文検索用のインデックスを Hibernate のエンティティのライフサイクルにあわせてリアルタイムに自動生成してくれます。

前回の記事では Elasticsearch の Docker イメージを使って簡単に動作を確認してみましたが、今回はさらに Maven のライフサイクルあわせて Docker の起動停止を行ってみます。

docker-maven-plugin を使うと、Maven の integration-test 実行時に Docker コンテナのビルドや管理が可能になります。

では早速やってみましょう。(記事中のサンプルコードはここにおいてあります)

環境:
Mac OS X
Docker for Mac
Hibernate Search 5.7
Elasticsearch 2.4

1. pom.xml の作成

1-1. 依存ライブラリーの記述

pom.xml

...
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.2.9.Final</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>5.7.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-elasticsearch</artifactId>
            <version>5.7.0.Final</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.194</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

hibernate-core ... Hibernate 本体
hibernate-search-orm ... Hibernate Search
hibernate-search-elasticsearch ... Hibernate Search で Elasticsearch をインデックスマネージャーとして使用する場合に必要

1-2. integration-test 用のソースディレクトリを Maven ビルドに追加する

build-helper-maven-plugin は Maven の標準ディレクトリレイアウト以外のディレクトリをソースディレクトリやリソースディレクトリとして追加できます。
/src/it は Maven の標準ディレクトリレイアウトですが、/src/it/java は標準ディレクトリレイアウトにはないので、テストソースディレクトリとして追加します。

pom.xml

...
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>build-helper-maven-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <id>add-test-source</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>add-test-source</goal>
                        </goals>
                        <configuration>
                            <sources>
                                <source>src/it/java</source>
                            </sources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

1-3. integration-test を実行できるようにする

maven-failsafe-plugin は Surefire プラグインを使って integration-test を実行できるようにしてくれます。

pom.xml

...
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
                <version>2.20</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>integration-test</goal>
                            <goal>verify</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

1-4. integration-test 実行時に Docker を起動する

docker-maven-plugin は integration-test 実行前後に Docker の起動停止を可能してくれます。
integration-test 実行時に Elasticsearch が Docker で起動しておくようにします。

pom.xml

            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <version>0.20.1</version>
                <configuration>
                    <autoPull>false</autoPull>
                    <images>
                        <image>
                            <alias>elasticsearch</alias>
                            <name>elasticsearch:2.4</name>
                            <run>
                                <ports>
                                    <port>9200:9200</port>
                                </ports>
                                <volumes>
                                    <bind>
                                        <volume>${basedir}/index/data:/usr/share/elasticsearch/data</volume>
                                        <volume>${basedir}/index/plugins:/usr/share/elasticsearch/plugins</volume>
                                        <volume>${basedir}/index/config:/usr/share/elasticsearch/config</volume>
                                    </bind>
                                </volumes>
                                <wait>
                                    <http>
                                        http://localhost:9200/_cluster/health
                                        <method>GET</method>
                                    </http>
                                    <time>10000</time>
                                </wait>
                            </run>
                        </image>
                    </images>
                </configuration>
                <executions>
                    <execution>
                        <id>start</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>build</goal>
                            <goal>start</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>stop</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

<wait> 要素で http://localhost:9200/_cluster/health を指定することにって、Elasticsearch が起動するまで integration-test の実行を待つにように設定しています。
今回は Elasticsearch の Analyzer として kuromoji を使いますので、事前にプラグインとして kuromoij をインストールしておく前提です。
kuromoji のインストールや、その他のオプションの詳細は前回の記事を参考にしてください。

2. エンティティの定義

JPA でエンティティを定義します。
@Indexed アノテーションを使って Hibernate Search で全文検索できるようにしましょう。
kuromoji を使って日本語の形態素解析も有効にします。

src/main/java/sample/Tweet.java

@Entity
@Indexed
@AnalyzerDef(
        name = "japanese",
        tokenizer = @TokenizerDef(
                factory = ElasticsearchTokenizerFactory.class,
                params = {
                        @Parameter(name = "type", value = "'kuromoji_tokenizer'")
                }
        )
)
@Analyzer(definition = "japanese")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Tweet implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Field
    private String content;

    @Override
    public String toString() {
        return content;
    }
}

3. integration-test を使って検証コードを書く

/src/it/java/sample/TweetIT.java

public class TweetIT {

    private SessionFactory sessionFactory;

    private Session session;

    @Before
    public void before() {
        StandardServiceRegistry standardRegistry = new StandardServiceRegistryBuilder()
                .applySetting(AvailableSettings.URL, "jdbc:h2:./db/sample")
                .applySetting(AvailableSettings.USER, "sa")
                .applySetting(AvailableSettings.PASS, "sa")
                .applySetting(AvailableSettings.DRIVER, "org.h2.Driver")
                .applySetting(AvailableSettings.DIALECT, H2Dialect.class)
                .applySetting(AvailableSettings.HBM2DDL_AUTO, "update")
                .applySetting(AvailableSettings.SHOW_SQL, true)
                .applySetting(AvailableSettings.FORMAT_SQL, true)
                .applySetting("hibernate.search.default.indexmanager", "elasticsearch")
                .build();

        Metadata metadata = new MetadataSources(standardRegistry)
                .addAnnotatedClass(Tweet.class)
                .getMetadataBuilder()
                .applyImplicitNamingStrategy(ImplicitNamingStrategyJpaCompliantImpl.INSTANCE)
                .build();

        SessionFactoryBuilder sessionFactoryBuilder = metadata.getSessionFactoryBuilder();
        sessionFactory = sessionFactoryBuilder.build();
        session = sessionFactory.openSession();

        session.getTransaction().begin();

        Tweet tweet1 = Tweet.builder().content("絶対は絶対にない").build();
        Tweet tweet2 = Tweet.builder().content("努力だ。勉強だ。それが天才だ").build();
        Tweet tweet3 = Tweet.builder().content("ゆっくりと急げ").build();

        session.save(tweet1);
        session.save(tweet2);
        session.save(tweet3);

        session.getTransaction().commit();
    }

    @After
    public void after() {
        session.getTransaction().begin();

        session.createQuery("delete from Tweet").executeUpdate();

        session.getTransaction().commit();

        session.close();
        sessionFactory.close();
    }

    @Test
    public void test() {
        FullTextSession fullTextSession = Search.getFullTextSession(session);
        QueryDescriptor query = ElasticsearchQueries.fromQueryString("content:絶対");
        List<Tweet> result = fullTextSession.createFullTextQuery(query, Tweet.class).list();

        Assert.assertEquals(1, result.size());
        Assert.assertEquals("絶対は絶対にない", result.get(0).getContent());
    }
}

@Before でテスト用検索対象のデータを登録して、@After で削除しています。
@Test で実際に全文検索を行い、検索にヒットするか検証しています。

4. integration-test を実行する

./mvnw clean verify
...
[INFO] DOCKER> [elasticsearch:2.4] "elasticsearch": Start container ad78807f5f65
[INFO] DOCKER> [elasticsearch:2.4] "elasticsearch": Waiting on url http://localhost:9200/_cluster/health with method GET for status 200..399.
[INFO] DOCKER> [elasticsearch:2.4] "elasticsearch": Waited on url http://localhost:9200/_cluster/health 7324 ms
...
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
...
[INFO] DOCKER> [elasticsearch:2.4] "elasticsearch": Stop and removed container ad78807f5f65 after 0 ms
/src/it/java/sample/TweetIT.java

以下のようなステップできちんと動きました。

① Elasticsearch 起動
② integration-test 実行
③ Elasticsearch が停止


次回は AWS Elasticsearch Service でやってみましょう。おわり。