CLOVER🍀

That was when it all began.

Hibernate Search × Elasticsearch

Hibernate Searchの5.6に向けて、Alphaリリースが続いています。現在は、Alpha3です。

Hibernate Search 5.6では、Elasticsearchとの統合が試験的に追加されます。

Hibernate Search 5.6.0.Alpha2 introduces Elasticsearch integration - In Relation To

Third milestone for Elasticsearch support - In Relation To

まあ、けっこう前からRoadmapに載っていたので、それほど驚くことではないのですが。

Roadmap - Hibernate Search

ただ、前はSolrも対象になっていた気がするのですが…記憶違いかな?

Hibernate Search × Elasticsearch

要するに、何かというとこういう感じらしいです。

Hibernate Searchは、これまでLuceneを直接利用していました。いわゆるEmbeddedな使い方で、インデキシング時などにはバックグラウンド処理が走るなどのオーバーヘッドがあったりしました。これに対して今回追加されようとしているElasticsearchとの統合は、Luceneのインデックス管理の部分をElasticsearchに任せる形のものになります。インデキシングやクエリを投げるのにRPCが必要になりますが、Elasticsearchによるスケールアウトのメリットを得ることができます。

まだまだ実験的サポート段階

とはいえ、今のHibernate SearchのEmbeddedな機能に比べると、まだまだ見劣りします。基本的なCRUDとクエリは実行できますが、未サポートなものもたくさんあります。

Integration with Elasticsearch

現状では、以下が制限として挙げられています。

  • Analyzer support(5.6.0 Alpha3でクリアしたっぽいですが)
  • Filters
  • Faceting
  • Optimisation
  • Timeouts
  • Delete by queries
  • Resolution for Date type mapping is ignored
  • Scrolling on large results
  • MoreLikeThis queries
  • Mixing Lucene based indexes and Elasticsearch based indexes

ファセットが使えないのは、なかなか痛いような…。ソースコードを見ていると、対応していきそうな雰囲気があるので、気長に待つとしましょう。

なお、Elasticsearchとの統合には、Jestが利用されています。

Jest/jest at master · searchbox-io/Jest · GitHub

https://github.com/hibernate/hibernate-search/blob/5.6.0.Alpha3/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/client/impl/JestClient.java#L62

このあたり見ると、HTTP通信のタイムアウトとかは、今は決め打ちですねぇ。

前置きはこれくらいにして、使ってみます。

準備

まずは、依存関係の定義から。Hibernate Searchは、JPAと統合する形で使います。

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.0.6.Final</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.spec.javax.transaction</groupId>
            <artifactId>jboss-transaction-api_1.2_spec</artifactId>
            <version>1.0.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>5.6.0.Alpha3</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-backend-elasticsearch</artifactId>
            <version>5.6.0.Alpha3</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.191</version>
            <scope>test</scope>
        </dependency>

「hibernate-search-backend-elasticsearch」が、Elasticsearchとの統合に必要なモジュールです。

データベースは、H2としました。

また、テスト向けにJUnitとAssertJも利用します。

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.3.0</version>
            <scope>test</scope>
        </dependency>

設定

JPAおよびHibernate Searchの設定を。

こちらを見ながら、以下のように設定しました。

Configuration

src/test/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="hibernate.search.pu" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:h2:mem:test"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="update"/>

            <!-- Hibenate Search -->
            <property name="hibernate.search.lucene_version" value="LUCENE_CURRENT"/>
            <property name="hibernate.search.elasticsearch.host" value="http://localhost:9200"/>
            <property name="hibernate.search.default.indexmanager" value="elasticsearch"/>
            <property name="hibernate.search.elasticsearch.index_management_strategy" value="NONE"/>
        </properties>
    </persistence-unit>
</persistence>

JPAの部分は端折りまして、Hibernate Searchの部分について。

            <property name="hibernate.search.lucene_version" value="LUCENE_CURRENT"/>
            <property name="hibernate.search.elasticsearch.host" value="http://localhost:9200"/>
            <property name="hibernate.search.default.indexmanager" value="elasticsearch"/>
            <property name="hibernate.search.elasticsearch.index_management_strategy" value="NONE"/>

「hibernate.search.lucene_version」はLuceneのバージョンを指定しますが、特筆事項はありません。Elasticsearchとの連携部分は、それぞれ以下のように。

  • hibernate.search.elasticsearch.host … Elasticsearchとの接続先URLを指定
  • hibernate.search.default.indexmanager … Elasticsearchと統合する場合、「elasticsearch」を指定
  • hibernate.search.elasticsearch.index_management_strategy … インデックスの作成方法を指定
  • hibernate.search.elasticsearch.index_management_wait_timeout … (今回設定していませんが)インデックス作成後、利用可能になるまで待つタイムアウトを設定

「hibernate.search.elasticsearch.index_management_strategy」ですが、NONE、MERGE、CREATE、CREATE_DELETEが指定できます。NONEだと何もしませんが、MERGEだとインデックス定義とマッピング定義を更新(コンフリクトしていなければ)、CREATEだとインデックスを1度削除してから作り直し、CREATE_DELETEだとCREATEに加えてシャットダウン時にインデックスを削除します。

CREATE_DELETEがテスト向けなので、最初これを指定していたらElasticsearchから応答が返らなくて…NONEにしました。

というか、マッピング定義は自分で書いた方がいい気もするので、まあいいかなぁと。Elasticsearchをシングルノードのデフォルトで起動するとクラスタのステータスがYELLOWなので、Hibernate Search 5.6.0 Alpha3で更新された「We’ll wait for Elasticsearch to be "green" before attempting to use it at boot」とぶつかっている気がします…。

ちなみに、今回は使っていませんがEntityの定義からマッピングは作成しようとはしてくれるみたいです。
https://github.com/hibernate/hibernate-search/blob/5.6.0.Alpha3/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchIndexManager.java#L224
https://github.com/hibernate/hibernate-search/blob/5.6.0.Alpha3/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchIndexManager.java#L284

Entity

続いて、JPAのEntityを定義します。
src/test/java/org/littlewings/hibernate/elasticsearch/Contents.java

package org.littlewings.hibernate.elasticsearch;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;

@Entity
@Table(name = "contents")
@Indexed(index = "myindex")
public class Contents implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    @Field
    private String content;

    public static Contents create(String content) {
        Contents c = new Contents();
        c.setContent(content);
        return c;
    }

    public Contents() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

ちょっとstaticなファクトリメソッド設けてますが、基本的には通常のJPA&Hibernate Searchで使うEntityです。

今回はインデックスの作成方針をNONEにしていないので、インデックス定義やマッピング定義をHibernate Searchは自動的に定義しません。とはいえ、ドキュメント登録時にインデックス名とタイプ名は必要なわけです。

この名前ですが、@IndexedアノテーションおよびEntityのクラス名で決まります。

Directory configuration

デフォルトでは、インデックス名はEntityのFQCNを小文字にしたものです。変更したい場合は@Indexedアノテーションのindexを設定します。

今回のEntity名は「org.littlewings.hibernate.elasticsearch.Contents」なので、デフォルトのインデックス名は「org.littlewings.hibernate.elasticsearch.contents」です。ですが、これだと長いのでindexを設定して「myindex」としました。

タイプについては、Entityのクラス名で決まってしまいます。
https://github.com/hibernate/hibernate-search/blob/5.6.0.Alpha3/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/impl/ElasticsearchIndexWorkVisitor.java#L177-L184

つまり、今回は「org.littlewings.hibernate.elasticsearch.Contents」となります。

マッピング

今回はKuromojiを導入し、以下のマッピングとしました。

{
  "settings": {
    "index": {
      "number_of_shards" : 1,
      "number_of_replicas" : 1,
      "analysis": {
        "tokenizer": {
          "kuromoji_tokenizer_search": {
            "type": "kuromoji_tokenizer",
            "mode": "search",
            "discard_punctuation" : "true",
            "user_dictionary" : "userdict_ja.txt"
          }
        },
        "analyzer": {
          "kuromoji_analyzer": {
            "type": "custom",
            "tokenizer": "kuromoji_tokenizer_search",
            "filter": ["kuromoji_baseform",
                       "kuromoji_part_of_speech",
                       "cjk_width",
                       "stop",
                       "ja_stop",
                       "kuromoji_stemmer",
                       "lowercase"]
          }
        }
      }
    }
  },
  "mappings": {
    "org.littlewings.hibernate.elasticsearch.Contents": {
      "_source": { "enabled": true },
      "_all": { "enabled": true },
      "properties": {
        "content": { "type": "string", "store": "yes", "index": "analyzed", "analyzer": "kuromoji_analyzer" }
      }
    }
  }
}

ElasticsearchおよびKuromojiプラグインは、インストール済み、起動済みとします。

$ curl -XPUT 'http://localhost:9200/myindex' -d @index.json

動かしてみる

では、実際にHibernate SearchとElasticsearchをつなげてみましょう。以下のテストコードを埋めていきます。
src/test/java/org/littlewings/hibernate/elasticsearch/SeparatedElasticsearchTest.java

package org.littlewings.hibernate.elasticsearch;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.hibernate.search.backend.elasticsearch.ElasticsearchQueries;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.engine.spi.QueryDescriptor;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SeparatedElasticsearchTest {
    @Test
    public void gettingStarted() {
        // ここに、処理を書く
    }
}

まずは、ふつうにJPAのAPIでEntityを登録します。

        EntityManager em = Persistence.createEntityManagerFactory("hibernate.search.pu").createEntityManager();

        ///// create data
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        em.persist(Contents.create("東京都は、日本の首都です。"));
        em.persist(Contents.create("明日は、晴れるでしょう。"));
        em.persist(Contents.create("Java EE 7 徹底入門"));
        em.persist(Contents.create("高速スケーラブル検索エンジン ElasticSearch Server"));
        em.persist(Contents.create("[改訂新版] Apache Solr入門 オープンソース全文検索エンジン"));

        transaction.commit();

Hibernate SearchのFullTextEntityManagerを取得します。ここまでは、通常のHibernate Searchの使い方と同じです。

        ///// get FullTextEntityManager
        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);

違うのはここからで、ElasticsearchQueriesからQueryStringのJSONを元にQueryDescriptorを作成し、その後にFullTextQueryを取得して検索するという流れになります。

        ///// search
        QueryDescriptor queryDescriptor1 =
                ElasticsearchQueries.fromQueryString("content:東京都 OR content:elasticsearch");
        FullTextQuery query1 =
                ftem
                        .createFullTextQuery(queryDescriptor1, Contents.class)
                        .setSort(new Sort(new SortField("id", SortField.Type.LONG)));

        List<Contents> results1 = query1.getResultList();
        assertThat(results1).hasSize(2);
        assertThat(results1.get(0).getContent()).isEqualTo("東京都は、日本の首都です。");
        assertThat(results1.get(1).getContent()).isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");

ちゃんとソートは指定できるところがナイス。

Query Stringを使ったクエリの書き方は、こちらを参照してください。
https://www.elastic.co/guide/en/elasticsearch/reference/2.x/query-dsl-query-string-query.html

JSON文字列として、クエリ自体のリクエストを渡すこともできます。

QueryDescriptor query = ElasticsearchQueries.fromJson(
      "{ 'query': { 'match' : { 'lastName' : 'Brand' } } }");

形態素解析が効いているか確認するために、「東京都」を「京都」で検索してみます。

        ///// search
        QueryDescriptor queryDescriptor2 =
                ElasticsearchQueries.fromQueryString("content:京都 OR content:elasticsearch");
        FullTextQuery query2 =
                ftem
                        .createFullTextQuery(queryDescriptor2, Contents.class)
                        .setSort(new Sort(new SortField("id", SortField.Type.LONG)));

        List<Contents> results2 = query2.getResultList();
        assertThat(results2).hasSize(1);
        assertThat(results2.get(0).getContent()).isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");

引っかからなくなりました。

削除すると、Elasticsearch側のインデックスにも反映されます。

        ///// delete
        transaction.begin();

        em.remove(em.find(Contents.class, 4L));

        transaction.commit();

        ///// search
        QueryDescriptor queryDescriptor3 =
                ElasticsearchQueries.fromQueryString("content:elasticsearch");
        FullTextQuery query3 =
                ftem
                        .createFullTextQuery(queryDescriptor3, Contents.class);

        List<Contents> results3 = query3.getResultList();
        assertThat(results3).isEmpty();

できないのは、クエリを使用した削除、です。

なお、curlでインデックスに保存された値を見ると、こんな感じになっています。

$ curl 'http://localhost:9200/myindex/_search?pretty&q=*'
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "hits" : {
    "total" : 4,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "myindex",
      "_type" : "org.littlewings.hibernate.elasticsearch.Contents",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "content" : "東京都は、日本の首都です。"
      }
    }, {
      "_index" : "myindex",
      "_type" : "org.littlewings.hibernate.elasticsearch.Contents",
      "_id" : "2",
      "_score" : 1.0,
      "_source" : {
        "content" : "明日は、晴れるでしょう。"
      }
    }, {
      "_index" : "myindex",
      "_type" : "org.littlewings.hibernate.elasticsearch.Contents",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "content" : "Java EE 7 徹底入門"
      }
    }, {
      "_index" : "myindex",
      "_type" : "org.littlewings.hibernate.elasticsearch.Contents",
      "_id" : "5",
      "_score" : 1.0,
      "_source" : {
        "content" : "[改訂新版] Apache Solr入門 オープンソース全文検索エンジン"
      }
    } ]
  }
}

typeが長い…。
※「高速スケーラブル検索エンジン ElasticSearch Server」がないのは、テストコードで削除しているからです

とりあえず、動かせましたね。

Embedded Elasticsearchと合わせて使う

こういうテストコードで書くと、Embeddedな構成で書きたくなるものです。

というわけで、書いてみました。

Maven依存関係に、以下を追加。

        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>2.2.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.plugin</groupId>
            <artifactId>analysis-kuromoji</artifactId>
            <version>2.2.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>4.2.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
            <scope>test</scope>
        </dependency>

Kuromojiプラグインも、もちろん使用します。Commons IOは、ディレクトリ削除用です。

src/test/resourcesディレクトリに、persistence.xmlを含めて以下のようにファイルを用意。

$ find src/test/resources -type f
src/test/resources/META-INF/persistence.xml
src/test/resources/config/userdict_ja.txt
src/test/resources/sources.json
src/test/resources/mappings.json
src/test/resources/plugins/analysis-kuromoji/plugin-descriptor.properties

EmbeddedなElasticsearchと、Kuromojiプラグインの設定については以下を参照してください。

Elasticsearch 2.xをEmbeddedableに使う - CLOVER

では、これらのファイルを使ってテストコードを書いていきます。
src/test/java/org/littlewings/hibernate/elasticsearch/EmbeddedElasticsearchTest.java

package org.littlewings.hibernate.elasticsearch;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.IndicesAdminClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.hibernate.search.backend.elasticsearch.ElasticsearchQueries;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.engine.spi.QueryDescriptor;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class EmbeddedElasticsearchTest {
    protected Node node;

    protected Path dataDirectory;

    // 以降を埋めていく
}

データディレクトリについては、起動時に一時ディレクトリとして作成、終了時に破棄する方針とします。また、マッピング定義、フィールド定義はファイルに保存したものを読み込んで設定するようにします。

というわけで、こういうメソッドを作成。

    protected Path createTemporaryDirectory(String prefix) {
        try {
            return Files.createTempDirectory(prefix).toAbsolutePath();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    protected String loadResourceAsString(String path) {
        try (InputStream is = getClass().getClassLoader().getResourceAsStream(path);
             InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
             BufferedReader reader = new BufferedReader(isr)) {
            StringBuilder builder = new StringBuilder();

            int c;
            while ((c = reader.read()) != -1) {
                builder.append((char) c);
            }

            return builder.toString();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

マッピング定義については、以下のように用意。
src/test/resources/mappings.json

{
  "analysis": {
    "tokenizer": {
      "kuromoji_tokenizer_search": {
        "type": "kuromoji_tokenizer",
        "mode": "search",
        "discard_punctuation": "true",
        "user_dictionary": "userdict_ja.txt"
      }
    },
    "analyzer": {
      "kuromoji_analyzer": {
        "type": "custom",
        "tokenizer": "kuromoji_tokenizer_search",
        "filter": [
          "kuromoji_baseform",
          "kuromoji_part_of_speech",
          "cjk_width",
          "stop",
          "ja_stop",
          "kuromoji_stemmer",
          "lowercase"
        ]
      }
    }
  }
}

フィールド定義については、以下のように用意。
src/test/resources/sources.json

{
  "org.littlewings.hibernate.elasticsearch.Contents": {
    "_source": {
      "enabled": true
    },
    "_all": {
      "enabled": true
    },
    "properties": {
      "content": {
        "type": "string",
        "store": "yes",
        "index": "analyzed",
        "analyzer": "kuromoji_analyzer"
      }
    }
  }
}

ちなみに、Kuromojiプラグイン用のplugin-descriptor.propertiesの中身も。
src/test/resources/plugins/analysis-kuromoji/plugin-descriptor.properties

description=The Japanese (kuromoji) Analysis plugin integrates Lucene kuromoji analysis module into elasticsearch.
version=2.2.1
name=analysis-kuromoji

site=false
jvm=true
classname=org.elasticsearch.plugin.analysis.kuromoji.AnalysisKuromojiPlugin
java.version=1.7
elasticsearch.version=2.2.1
isolated=true

あとは、@BeforeでElasticsearchを起動して、インデックス、マッピングの定義を作成。

    @Before
    public void setUp() {
        dataDirectory = createTemporaryDirectory("elasticsearch-");

        Settings settings =
                Settings
                        .settingsBuilder()
                        .put("cluster.name", "test-cluster")
                        .put("network.host", "127.0.0.1")
                        .put("http.port", 9200)
                        .put("transport.tcp.port", 9300)
                        .put("discovery.zen.ping_timeout", "3ms")
                        .put("path.data", dataDirectory.toFile().getPath())
                        .put("path.home", ".")
                        .put("path.plugins", "./src/test/resources/plugins")
                        .put("path.conf", "./src/test/resources/config")
                        .put("index.number_of_shards", 1)
                        .put("index.number_of_replicas", 0)
                        .build();

        node =
                NodeBuilder
                        .nodeBuilder()
                        .settings(settings)
                        .node();

        Client client = node.client();
        IndicesAdminClient indicesAdminClient = client.admin().indices();
        indicesAdminClient
                .prepareCreate("myindex")
                .setSettings(loadResourceAsString("mappings.json"))
                .get();
        indicesAdminClient
                .preparePutMapping("myindex")
                .setType(Contents.class.getName())
                .setSource(loadResourceAsString("sources.json"))
                .get();
    }

小さく構成したいので、number_of_shardsやnumber_of_replicasを絞り、discovery.zen.ping_timeoutも短くしています。

        Settings settings =
                Settings
                        .settingsBuilder()
                        .put("cluster.name", "test-cluster")
                        .put("network.host", "127.0.0.1")
                        .put("http.port", 9200)
                        .put("transport.tcp.port", 9300)
                        .put("discovery.zen.ping_timeout", "3ms")
                        .put("path.data", dataDirectory.toFile().getPath())
                        .put("path.home", ".")
                        .put("path.plugins", "./src/test/resources/plugins")
                        .put("path.conf", "./src/test/resources/config")
                        .put("index.number_of_shards", 1)
                        .put("index.number_of_replicas", 0)
                        .build();

HTTPリッスンは有効にしています。Jestがつなぐので。

@Afterでは、Elasticsearchのノードを落として、データディレクトリとして利用していた一時ディレクトリを削除します。

    @After
    public void tearDown() {
        node.close();

        try {
            FileUtils.deleteDirectory(dataDirectory.toFile());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

これで、先ほどのElasticsearchを別プロセスで起動していたテストコードが動作します。

    @Test
    public void gettingStarted() {
        EntityManager em = Persistence.createEntityManagerFactory("hibernate.search.pu").createEntityManager();

        ///// create data
        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        em.persist(Contents.create("東京都は、日本の首都です。"));
        em.persist(Contents.create("明日は、晴れるでしょう。"));
        em.persist(Contents.create("Java EE 7 徹底入門"));
        em.persist(Contents.create("高速スケーラブル検索エンジン ElasticSearch Server"));
        em.persist(Contents.create("[改訂新版] Apache Solr入門 オープンソース全文検索エンジン"));

        transaction.commit();

        ///// get FullTextEntityManager
        FullTextEntityManager ftem = Search.getFullTextEntityManager(em);

        ///// search
        QueryDescriptor queryDescriptor1 =
                ElasticsearchQueries.fromQueryString("content:東京都 OR content:elasticsearch");
        FullTextQuery query1 =
                ftem
                        .createFullTextQuery(queryDescriptor1, Contents.class)
                        .setSort(new Sort(new SortField("id", SortField.Type.LONG)));

        List<Contents> results1 = query1.getResultList();
        assertThat(results1).hasSize(2);
        assertThat(results1.get(0).getContent()).isEqualTo("東京都は、日本の首都です。");
        assertThat(results1.get(1).getContent()).isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");

        ///// search
        QueryDescriptor queryDescriptor2 =
                ElasticsearchQueries.fromQueryString("content:京都 OR content:elasticsearch");
        FullTextQuery query2 =
                ftem
                        .createFullTextQuery(queryDescriptor2, Contents.class)
                        .setSort(new Sort(new SortField("id", SortField.Type.LONG)));

        List<Contents> results2 = query2.getResultList();
        assertThat(results2).hasSize(1);
        assertThat(results2.get(0).getContent()).isEqualTo("高速スケーラブル検索エンジン ElasticSearch Server");

        ///// delete
        transaction.begin();

        em.remove(em.find(Contents.class, 4L));

        transaction.commit();

        ///// search
        QueryDescriptor queryDescriptor3 =
                ElasticsearchQueries.fromQueryString("content:elasticsearch");
        FullTextQuery query3 =
                ftem
                        .createFullTextQuery(queryDescriptor3, Contents.class);

        List<Contents> results3 = query3.getResultList();
        assertThat(results3).isEmpty();
    }

OKですー。

オマケ)
Elasticsearchとテストを統合したい人は、以下を使うとよいのかもしれません。
https://github.com/alexcojocaru/elasticsearch-maven-plugin

Hibernate Search自身が使っていました。