Elasticsearch Java High Level REST Clientで検索を行う

10 min読了の目安(約9400字TECH技術記事

本記事ではローカル開発環境に公式Javaクライアント(High Level REST Client)を使って様々なクエリで検索を行う方法を書きます。

本記事に使ったコードは下記リポジトリでも公開しています。

使用しているソフトウェアのバージョン

環境構築(省略)

環境構築手順については本記事では省略します。以下の記事に記載の環境で実行しています。

テストデータ作成

検索のテストに使うデータをElasticsearchに登録します。以下はKibanaのDev Toolsで発行できるRESTリクエストのサンプルです。

# my_index を削除(マッピングを作り直すため)
DELETE /my_index

# my_index を作成
PUT /my_index

# my_index のマッピング定義
PUT /my_index/_mapping
{
  "properties": {
    "name": {
      "type": "text"
    },
    "age": {
      "type": "integer"
    },
    "favorite_genres": {
      "type": "keyword"
    }
  }
}

次に登録するデータ。

POST /my_index/_doc
{
  "name": "John",
  "age": 40,
  "favorite_genres": ["rock", "r&b"]
}

POST /my_index/_doc
{
  "name": "Paul",
  "age": 78,
  "favorite_genres": ["rock", "r&b", "pop"]
}

POST /my_index/_doc
{
  "name": "George",
  "age": 58,
  "favorite_genres": ["rock", "r&b", "country", "funk", "jazz"]
}

POST /my_index/_doc
{
  "name": "Richard",
  "age": 80,
  "favorite_genres": ["rock", "techno", "classic", "jazz"]
}

Search APIの使用

次に、このデータベースに対してElasticsearch Java High Level REST Clientを使って検索クエリを投げていきます。

ドキュメントは以下を参照してください。

全件取得(matchAllクエリ)

まずは全件取得してみます。

GET /my_index/_search
{
  "query": {
    "match_all": {}
  }
}

Javaだとこう書きます。

package esjava;

import java.io.IOException;
import java.util.Arrays;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;

public class App {

  public static void main(String[] args) {
    try (var client = new RestHighLevelClient(
        RestClient.builder(new HttpHost("localhost", 9200, "http")))) {

      var request = new SearchRequest("my_index");
      request.source(
          SearchSourceBuilder.searchSource()
              .query(QueryBuilders.matchAllQuery())
      );
      var response = client.search(request, RequestOptions.DEFAULT);
      Arrays.stream(response.getHits().getHits())
          .forEach(h -> System.out.println(h.getSourceAsString()));

    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }
}

これを ./gradlew run などで実行すると、以下のような出力が得られます。

{"name":"John","age":40,"favorite_genres":["rock","r&b"]}
{"name":"Paul","age":78,"favorite_genres":["rock","r&b","pop"]}
{"name":"George","age":58,"favorite_genres":["rock","r&b","country","funk","jazz"]}
{"name":"Richard","age":80,"favorite_genres":["rock","techno","classic","jazz"]}

取得するフィールドを指定する

_source で取得するフィールドを指定できます。

GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["name", "age"]
}

Javaではこう(違いはSearchSourceBuilderのメソッド呼び出しだけなので、そこだけ抜粋してます。)。

SearchSourceBuilder.searchSource()
    .query(QueryBuilders.matchAllQuery())
    .fetchSource(new String[]{"name", "age"}, null)

fetchSource() の第1引数はinclude list、第2引数はexclude listなので、どちらか片方を指定すればOKです。

以下のように、指定したフィールドだけを取得できます。

{"name":"John","age":40}
{"name":"Paul","age":78}
{"name":"George","age":58}
{"name":"Richard","age":80}

ページング

from/sizeを指定することで、取得する位置を指定できます。fromは0始まりです。

GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["name", "age"],
  "from": 1,
  "size": 2
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.matchAllQuery())
    .fetchSource(new String[]{"name", "age"}, null)
    .from(1)
    .size(2)

Johnが飛ばされて、PaulとGeorgeの2件が取得できていることがわかります。

{"name":"Paul","age":78}
{"name":"George","age":58}

ソート

ソートに使うフィールドと、昇順/降順を指定します。

GET /my_index/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["name", "age"],
  "sort": [
    {
      "age": {
        "order": "asc"
      }
    }
  ]
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.matchAllQuery())
    .fetchSource(new String[]{"name", "age"}, null)
    .sort("age", SortOrder.ASC)

年齢の若い順に並びます。

{"name":"John","age":40}
{"name":"George","age":58}
{"name":"Paul","age":78}
{"name":"Richard","age":80}

matchクエリ

matchクエリでは、text型のフィールドに対して全文検索を行えます。検索キーワードを空白区切りにして複数指定もできます(デフォルトはOR条件)。

GET /my_index/_search
{
  "query": {
    "match": {
      "name": "john paul"
    }
  },
  "_source": ["name", "age"]
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.matchQuery("name", "john paul"))
    .fetchSource(new String[]{"name", "age"}, null)

JonhとPaulの2件が取得されます。

{"name":"John","age":40}
{"name":"Paul","age":78}

複数キーワードのAND条件にしたい場合はoperatorを指定します。

GET /my_index/_search
{
  "query": {
    "match": {
      "name": {
        "query": "john paul",
        "operator": "and"
      }
    }
  },
  "_source": ["name", "age"]
}
SearchSourceBuilder.searchSource()
    .query(
        QueryBuilders.matchQuery("name", "john paul")
            .operator(Operator.AND)
    )
    .fetchSource(new String[]{"name", "age"}, null)

この条件を満たすドキュメントは存在しないので、取得結果は0件になります。

termクエリ

termクエリではkeyword型のフィールドに対して完全一致で検索を行えます。

GET /my_index/_search
{
  "query": {
    "term": {
      "favorite_genres":  "r&b"
    }
  },
  "_source": ["name", "favorite_genres"]
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.termQuery("favorite_genres", "r&b"))
    .fetchSource(new String[]{"name", "favorite_genres"}, null)

favorite_genres に r&b が含まれるドキュメントだけが取得されます。

{"favorite_genres":["rock","r&b"],"name":"John"}
{"favorite_genres":["rock","r&b","pop"],"name":"Paul"}
{"favorite_genres":["rock","r&b","country","funk","jazz"],"name":"George"}

termsクエリ

キーワードを複数指定するにはtermsクエリを使います。

GET /my_index/_search
{
  "query": {
    "terms": {
      "favorite_genres": ["pop", "techno"]
    }
  },
  "_source": ["name", "favorite_genres"]
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.termsQuery("favorite_genres", "pop", "techno"))
    .fetchSource(new String[]{"name", "favorite_genres"}, null)
{"favorite_genres":["rock","r&b","pop"],"name":"Paul"}
{"favorite_genres":["rock","techno","classic","jazz"],"name":"Richard"}

rangeクエリ

数値型や日付型に対する範囲指定にはrangeクエリを使います。

GET /my_index/_search
{
  "query": {
    "range": {
      "age": {
        "gt": 40,
        "lt": 80
      }
    }
  },
  "_source": ["name", "age"]
}
SearchSourceBuilder.searchSource()
    .query(QueryBuilders.rangeQuery("age").gt(40).lt(80))
    .fetchSource(new String[]{"name", "age"}, null)

年齢が40より大きく、80より小さいドキュメントを取得できます。

{"name":"Paul","age":78}
{"name":"George","age":58}

以上/以下で検索するには gte/lte を使います。

SearchSourceBuilder.searchSource()
    .query(QueryBuilders.rangeQuery("age").gte(40).lte(80))
    .fetchSource(new String[]{"name", "age"}, null)
{"name":"John","age":40}
{"name":"Paul","age":78}
{"name":"George","age":58}
{"name":"Richard","age":80}

複数の検索条件の組み合わせ(boolクエリ)

これまで紹介したクエリを組み合わせるのに使うのがboolクエリです。例えば、以下の条件を満たすユーザーを取得したいします。

  • 必須: rock好き、50歳以上
  • いずれかを満たせばOK: r&b好きまたはpop好き
  • NG: techno好き

これらは must/should/must_not/filter を使って以下のように書けます。

GET /my_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "favorite_genres": "rock"
          }
        }
      ],
      "should": [
        {
          "term": {
            "favorite_genres": "r&b"
          }
        },
        {
          "term": {
            "favorite_genres": "pop"
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "favorite_genres": "techno"
          }
        }
      ],
      "filter": {
        "range": {
          "age": {
            "gte": 50
          }
        }
      }
    }
  }
}
SearchSourceBuilder.searchSource()
    .query(
        QueryBuilders.boolQuery()
            .must(QueryBuilders.termQuery("favorite_genres", "rock"))
            .should(QueryBuilders.termQuery("favorite_genres", "r&b"))
            .should(QueryBuilders.termQuery("favorite_genres", "pop"))
            .mustNot(QueryBuilders.termQuery("favorite_genres", "techno"))
            .filter(QueryBuilders.rangeQuery("age").gte(50))
    )

以下の2件が取得できます。

{"favorite_genres":["rock","r&b","pop"],"name":"Paul","age":78}
{"favorite_genres":["rock","r&b","country","funk","jazz"],"name":"George","age":58}

むすび

以上、Elasticsearch Java High Level REST Clientを使って基本的な検索を行う方法を紹介しました。

本記事では紹介していないクエリ(match_phraseなど)や機能(ソートに使うスコアの計算方法の調整)などもありますので、より応用的な使い方については公式ドキュメントを参照してください。