TransportClientからRestHighLevelClientに移行する


はじめに

この記事はElastic stack (Elasticsearch) Advent Calendar 2018の18日目の記事です。

webアプリケーションエンジニアになり初めての現場でElasticsearchを使った全文検索の実装を任せられた時、
JavaClientを使った記事がなかなかなく辛い思いをしたのでそんな方に役に立つ記事になれば幸いです。

記事の内容

JavaClientを使ってSpringアプリケーションに全文検索の機能を取り入れたものを想定とし、
Elasticsearch7系から非推奨、8系から完全に無くなると噂のTransportClientを
RestHighLevelClientに移行する際のclient接続の変更とクエリ生成の変更点をまとめてみました。

今回は認証を含む接続やBulkAPI,スクロールAPIは対象外です。

環境

macOS
Elasticsearch6.5.2
Java8
Spring Boot 2.1.1

準備

サンプルアプリ

作成したアプリケーションはGitHubに挙げています。
https://github.com/ohanamisan/Elasticsearch_on_Java

Elasticsearchのバージョンが異なる場合は適宜gradleファイルのjarインポート箇所を変更してください。
ちなみにサンプル内には一応Bulkでインサートしている処理も雑に実装してあります。
今回の記事には詳細は書いてません。その他詳細はREADME参照でお願いします。

実装

TransportClient -> RestHighLevelClient

早速ですが今回の主役Clientの移行です。


TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
        .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));

今まで標準として使われてきているTransportClientはこんな感じ。
port番号はElasticsearchデフォルトの9200ではなくTransportとして提供されているのは9300。

ちなみに7系α版で実装したのがこちら。

非推奨になってます。8系からは使えなくなります。

では、今後の標準となるRestHighLevelClientに書き換えます。


RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(new HttpHost("localhost", 9200, "http")));

Transport用の9300は使わずにhttpアクセスで9200を直接叩くような感じになった?んだと思います。
一目でRestを叩いているのがわかるようなシンプルな命名と構成になりました。

同じサーバで複数のElasticsearchを使う際はポートは自動的に次のポートを使用するため、数に合わせてカンマ区切りでHttpHostを追加していきます。


RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(
            new HttpHost("localhost", 9200, "http"),
            new HttpHost("localhost", 9201, "http")
    )
);

TransportClientはnettyで内部的にアクセスしに行くようなログが吐かれていましたがRestHighLevelClientは名前通りRestで叩いているためかデフォルトではログは出ません。

prepareSearch -> SearchSourceBuilder + SearchRequest

続いてクエリ生成の実装を修正していきます。
今回全文検索の際に生成するクエリは以下を想定しています。


{
  "query": {
    "bool": {
      "should": [
        {
          "match_phrase": {
            "title.full_text_search": 検索ワード
          }
        },
        {
          "match_phrase": {
            "body.full_text_search": 検索ワード
          }
        }
      ], "minimum_should_match": 1
    }
  }
}

文章とタイトルに対してmatch_phraseクエリを投げるシンプルなクエリです。
タイトルを含んだ全文検索にしたかったのでshouldクエリを使いタイトルか本文のどちらかにヒットしていたら正とします。

このクエリをJavaで生成していきます。

TransportClient使用時のクエリ生成とリクエストは以下のようになります。


BoolQueryBuilder query = QueryBuilders.boolQuery();     
query.should(QueryBuilders.matchPhraseQuery("title.full_text_search", word))
  .should(QueryBuilders.matchPhraseQuery("body.full_text_search", word));
  .minimumShouldMatch(1));
SearchResponse res = client.prepareSearch("qiita")
                .setQuery(query)
                .setSize(1000)
                .get();
res.getHits();

QueryBuildersでクエリを生成していきclientのprepareSearchメソッドに
リクエストクエリとリクエスト時の設定を渡してあげます。

余談ですがJavaでもQueryBuildersを入れ子にしたりメソッドチェーンしていったりするとJSONでのクエリ同様かなり複雑なクエリを生成することができます。

RestHighLevelClientにはprepareSearchメソッドはなくなり、
SearchRequestを引数とするsearchメソッドが使われます。
以下が修正コードになります。


SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(0);
sourceBuilder.size(1000);
sourceBuilder.query(QueryBuilders.boolQuery()
                    .should(QueryBuilders.matchPhraseQuery("title.full_text_search", word))
                    .should(QueryBuilders.matchPhraseQuery("body.full_text_search", word))
                    .minimumShouldMatch(1));

SearchRequest req = new SearchRequest().indices(INDEX).source(sourceBuilder);

SearchResponse res = client.search(req, RequestOptions.DEFAULT);
res.getHits();

コードの行数でいうと長くなってしまったように思いますが、
クエリ、リクエスト、レスポンスの設定の責務がそれぞれ分けられていて個人的には扱いやすくなった印象です。

実行

gif作りで力尽きてしまい申し訳無いのですがタイトルに検索ワードの「Java」が入ってないのも見て取っていただけたら幸いです。本文から取ってきてヒットしてるような感じです。

まとめ

今回はローカル環境で最小限の全文検索の実装を例にしてみました。

xpack等を入れた際のベーシック認証を伴うRestHighLevelClientや
検索に欠かせないページングのためのScrollAPIなど、まだまだ拡張要素はあるので
今回の(実は)初投稿を機にサンプルを拡張していきながら引き続き記事にしていけたらなと思います。

また、久々にJavaの実装したので何かおかしい点などありましたらご指摘ください。

さいごに

明日は第27回Elasticsearch勉強会「LT&忘年会」がありますね!
https://www.meetup.com/ja-JP/Tokyo-Elastic-Fantastics/events/256619262/