🍃

SpringBootからElasticsearchを使う方法2選(クライアント/インデックス生成)

2020/12/07に公開

本記事では 2020 年 12 月現在 Java(Spring Boot)を使用したアプリケーションから Elasticsearch に接続する際に主に候補で上がるであろう

  • Elasticsearch Rest High Level Client
  • Spring Data Elasticsearch

の 2 つの方法について、クライアントの生成方法や設定方法、インデックスの作成方法までを比較してみたいと思います。
なお、本稿ではクライアント等は Bean に登録した上で永続化させた状態で使用することを想定しています。

クライアント生成

Rest High Level Client

Rest High Level Client を永続化した上で利用するには、色々手段あるかと思いますが、今回はよく利用している方法で記述してみます。

RestHighLevelClientConfig.java

package com.pakio.demoelasticsearch.RestHighLevelClient.client;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Component
@Configuration
public class RestHighLevelClientConfig {

  @Value("${spring.elasticsearch.host}")
  private String host;

  @Value("${spring.elasticsearch.port}")
  private Integer port;

  @Value("${spring.elasticsearch.https}")
  private Boolean https;

  @Bean(name = "restHighLevelClient", destroyMethod = "close")
  RestHighLevelClient client() {
    return getClient();
  }

  private RestHighLevelClient getClient() {
    RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(new HttpHost(host, port, https ? "https" : "http"))
            .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder)
            .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder)
    );
    return client;
  }
}

ClientRepository.java

package com.pakio.demoelasticsearch.RestHighLevelClient.client;

import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class ClientRepository {
  @Autowired
  private RestHighLevelClientConfig restHighLevelClientConfig;

  private RestHighLevelClient restHighLevelClient;

  @Autowired
  public void setClient(RestHighLevelClient restHighLevelClient) {
    this.restHighLevelClient = restHighLevelClient;
  }
}

上記の設定を行うことにより、クライアントを Bean として登録した上で使い回すことが可能となります。

Bean に登録して使用する注意点として、クライアントが意図せぬ原因で Closed な状態になった際に、Bean に登録された同一のクライアントを利用しようとしエラーとなります。
その回避策として、上記のクライアントを用いた検索処理の部分で RestHighLevelClientConfig.getClient を呼ぶことが可能な public な関数を用意しておき、クライアントを再作成することで回避しています。

RestHighLevelClientConfig.java

...
// クライアント再生成用メソッド
public RestHighLevelClient getRecreateClient() {
  return this.getClient();
}
...

ClientRepository.java

...
public SearchResponse getSearchResult(SearchRequest request) {
  try {
    return restHighLevelClient.search(request, RequestOptions.DEFAULT);
  } catch (Exception e) {
    this.setClient(restHighLevelClientConfig.getRecreateClient()); //何らかの例外が発生した場合、クライアントを差し替える
    return null;
  }
}
...

Spring Data Elasticsearch

Spring Data Elasticsearch は内部的に利用するクライアントを以下から選択することが可能です。

  • Transport Client
  • Rest Low Level Client
  • Rest High Level Client

今回は、この中でも最も高機能な Rest High Level Client を例に実装してみます。

package com.pakio.demoelasticsearch.SpringDataElasticsearch.client;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.stereotype.Component;

@Component
@Configuration
public class ElasticsearchRestTemplateConfig extends AbstractElasticsearchConfiguration {

  @Value("${spring.elasticsearch.host}")
  private String host;

  @Value("${spring.elasticsearch.port}")
  private Integer port;

  @Value("${spring.elasticsearch.https}")
  private Boolean https;

  @Bean
  RestHighLevelClient client() {
    return getClient();
  }

  private RestHighLevelClient getClient() {
    RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(new HttpHost(host, port, https ? "https" : "http"))
            .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder)
            .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder)
    );
    return client;
  }

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return this.getClient();
  }
}

ドキュメントにもある通り、この生成方法を利用すると Bean の特別な定義の必要なしに生成することが可能です。

Create Index Request

Rest High Level Client

Rest High Level Client を用いてのインデックス生成は以下の 3 種類が選択可能です。

  • 生 Json 文字列
  • HashMap
  • XContentBuilder

今回は HashMap での例を示します。

インデックス定義

IndexMapping.java

package com.pakio.demoelasticsearch.RestHighLevelClient.entity;

import java.util.HashMap;
import java.util.Map;

public class IndexMapping {
  public static Map getIndexMapping() {
    Map<String, Object> jsonMap = new HashMap<>();
    Map<String, Object> message = new HashMap<>();
    message.put("type", "text");
    Map<String, Object> properties = new HashMap<>();
    properties.put("message", message);
    jsonMap.put("properties", properties);

    return jsonMap;
  }
}

RestHighLevelClientService.java

package com.pakio.demoelasticsearch.RestHighLevelClient.service;

import com.pakio.demoelasticsearch.RestHighLevelClient.client.ClientRepository;
import com.pakio.demoelasticsearch.RestHighLevelClient.entity.IndexMapping;
import java.util.Map;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class RestHighLevelClientService {
  @Autowired
  ClientRepository clientRepository;

  public String createIndex() {
    Map mapping = IndexMapping.getIndexMapping();
    CreateIndexRequest request = new CreateIndexRequest("dest_index");
    request.source(mapping);
    if (clientRepository.createIndex(request).isAcknowledged()) {
      return "ok";
    } else {
      return "ng";
    }
  }
}

インデックス作成用処理

RestHighLevelClientController.java

package com.pakio.demoelasticsearch.RestHighLevelClient.controller;

import com.pakio.demoelasticsearch.RestHighLevelClient.service.RestHighLevelClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/rest-high-level-client")
public class RestHighLevelClientController {

  @Autowired
  RestHighLevelClientService restHighLevelClientService;

  @RequestMapping("/create-index")
  public String createIndex() {
		return restHighLevelClientService.createIndex();
  }

}

ClientRepository.java

...
public AcknowledgedResponse createIndex(CreateIndexRequest request) {
    try {
      return restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
    } catch (Exception e) {
      System.out.println(e.getCause());
      System.out.println(e.getMessage());
      this.setClient(restHighLevelClientConfig.getRecreateClient());
      return null;
    }
  }
...

Spring Data Elasticsearch

インデックス定義

Spring Data Elasticsearch では、インデックス内部の構造をクラスとして扱います。

DestIndex.java

package com.pakio.demoelasticsearch.SpringDataElasticsearch.entity;

import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "dest_index2")
public class DestIndex {
  @Field(type= FieldType.Text)
  String message;

  public String getMessage() {
    return this.message;
  }
}

インデックス作成処理

内部的には Elasticsearch 公式クライアントの 3 種を全てサポートしていますが、TransportClient に関しては deprecated となっています。
今回のデモでは先ほどと同様に RestHighLevelClient を例に実装してみます。

ElasticsearchRestTemplate.java

package com.pakio.demoelasticsearch.SpringDataElasticsearch.client;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
import org.springframework.stereotype.Component;

@Component
@Configuration
public class ElasticsearchRestTemplate extends AbstractElasticsearchConfiguration {

  @Value("${spring.elasticsearch.host}")
  private String host;

  @Value("${spring.elasticsearch.port}")
  private Integer port;

  @Value("${spring.elasticsearch.https}")
  private Boolean https;

  @Bean
  RestHighLevelClient client() {
    return getClient();
  }

  private RestHighLevelClient getClient() {
    RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(new HttpHost(host, port, https ? "https" : "http"))
            .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder)
            .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder)
    );
    return client;
  }

  @Override
  public RestHighLevelClient elasticsearchClient() {
    return this.getClient();
  }
}

SpringDataElaticsearchService.java

package com.pakio.demoelasticsearch.SpringDataElasticsearch.service;

import com.pakio.demoelasticsearch.SpringDataElasticsearch.entity.DestIndex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.stereotype.Service;

@Service
public class SpringDataElasticsearchService {
  private final ElasticsearchOperations elasticsearchOperations;

  @Autowired
  public SpringDataElasticsearchService(ElasticsearchOperations elasticsearchOperations) {
    this.elasticsearchOperations = elasticsearchOperations;
  }

  public Boolean createIndex() {
    IndexOperations operations = this.elasticsearchOperations.indexOps(DestIndex.class);
    return operations.create();
  }
}

SpringDataElasticsearchController.java

package com.pakio.demoelasticsearch.SpringDataElasticsearch.controller;

import com.pakio.demoelasticsearch.SpringDataElasticsearch.service.SpringDataElasticsearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/spring-data-elasticsearch")
public class SpringDataElasticsearchController {
  @Autowired
  SpringDataElasticsearchService springDataElasticsearchService;

  @RequestMapping("/create-index")
  public String createIndex() {
    return springDataElasticsearchService.createIndex().toString();
  }

}

ここで、AbstractElasticsearchConfiguration は ElasticsearchOperations を継承しており、Bean への登録の明記なしに Autowired で利用することが可能です。

まとめ

本稿では Spring Boot から Elasticsearch に接続する主な 2 方法について紹介しました。
明日はもう少し詳細、CRUD 操作についてまとめてみます。

リポジトリ

ここまで登場したコード類は以下のリポジトリで全て公開しています。
もし間違いや改善案等ありましたらこちらまでご報告お願いいたします。
pakio/spring-elasticsearch-client-comparison

参考

Spring Data Elasticsearch - Reference Documentation
Elasticsearch Java API 入門

Discussion