Elasticsearch Java API入門

f:id:vasilyjp:20200908115802p:plain

ZOZOテクノロジーズ ECプラットフォーム部 マイグレーションチームの會田です。

ZOZOTOWNでは先日公開した記事の通り、すべての検索をElasticsearchへリプレイスしました。

検索エンジンのリプレイスに伴い、VBScriptで稼働していた検索システムをJavaへリプレイスすることも併せて行われました。

本記事ではその際に得た知見を、Elasticsearch初心者の方及びElasticsearch Java APIを初めて触る方向けに紹介します。

環境(開発当時)

  • Elasticsearchバージョン:7.3.2
  • Javaバージョン:8
  • Spring Bootバージョン:1.5.15
  • Mavenバージョン:2.17

準備

<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.3.2</version>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.3.2</version>
</dependency>

上記をpom.xmlに記載するとElasticsearchのライブラリ及び後述するRestHighLevelClientが使えるようになります。

基本的なライブラリの説明と使用方法

※以降に出てくるカラム名及びIDなどは全て実在するものではなくダミーデータです。

RestLowLevelClient(RestClient)

RestLowLevelClientはhttp経由でElasticsearchクラスタと通信できるクライアントです。Elasticsearchのすべてのバージョンと互換性があります。

参考:
 Java Low Level REST Clientのリファレンス

RestHighLevelClient

RestHighLevelClientはそれまで利用されていたTransportClientに代わって推奨されている、RESTクライアントです。これを使用することで、Javaアプリケーションからhttpを介してElasticsearchへアクセスできます。

なお、Java High Level REST ClientはJava Low Level REST Client上で動作しています。

final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "password")
);

RestHighLevelClient client = new RestHighLevelClient(
    RestClient.builder(new HttpHost("localhost", 9200, "https"))
        .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
          .setDefaultCredentialsProvider(credentialsProvider))
);

searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);

参考:
 Java High Level REST Clientのリファレンス

SearchSourceBuilder

SearchSourceBuilderは検索動作を制御する大半のオプションを制御できます。

下記ソースコードの詳細は後述のライブラリ紹介の中で別途説明します。

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("FashionItemId", 1));
searchSourceBuilder.from(0);
searchSourceBuilder.size(100);
searchSourceBuilder.sort(new FieldSortBuilder("ItemPrice").order(SortOrder.ASC));

参考:
 SearchSourceBuilderリファレンス

SearchRequest

下記リファレンスより引用。

SearchRequestは、ドキュメント、集約、サジェストを検索する操作に使用され、結果として得られるドキュメントのハイライト表示を要求する方法も提供します。

とありますが、APIを使ってElasticsearchへリクエストを送るための大元になるものという捉え方でよいと思います。
※Elasticsearchでは、データをドキュメントという単位で扱います。

下記コードの詳細は後述のライブラリ紹介の中で説明します。

SearchRequest searchRequest = new SearchRequest();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("FashionItemId", 1));
searchRequest.source(searchSourceBuilder);

参考:
 SearchRequestリファレンス

CountRequest

CountRequestは実行したクエリに一致したドキュメント数を取得するために使用します。

CountRequest countRequest = new CountRequest();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
countRequest.source(searchSourceBuilder);

参考:
 CountRequestリファレンス

QueryBuilders

検索クエリを作成するためのユーティリティクラスです。

参考:
 QueryBuilders一覧

下記に代表的な検索クエリとQueryBuildersユーティリティクラス内の対応するQueryBuilderクラスおよびヘルパーメソッドを紹介します。

matchAllQuery

すべてのドキュメントを取得します。

QueryBuilders.matchAllQuery();

参考:
 matchAllQueryリファレンス

termQuery

条件に合致するか。SQLの=と同義。

QueryBuilders.termQuery("FashionItemId", 1);

参考:
 termQueryリファレンス
 termQuery Javadoc

termsQuery

条件に合致したドキュメントがあるか。SQLのIN句と同義。

Integer[] fashionItemIds = {1, 2};
QueryBuilders.termsQuery("FashionItemId", fashionItemIds);

参考:
 termsQueryリファレンス
 termsQuery Javadoc

rangeQuery

条件で指定した範囲のドキュメントがあるか。SQLの>=<=><と同義。

// StartDatetime >= "2020-01-01 00:00:00"
QueryBuilders.rangeQuery("StartDatetime").gte("2020-01-01 00:00:00");

// EndDatetime <= "2021-01-01 00:00:00"
QueryBuilders.rangeQuery("EndDatetime").lte("2021-01-01 00:00:00");

// StartDatetime > "2020-01-01 00:00:00"
QueryBuilders.rangeQuery("StartDatetime").gt("2020-01-01 00:00:00");

// EndDatetime < "2021-01-01 00:00:00"
QueryBuilders.rangeQuery("EndDatetime").lt("2021-01-01 00:00:00");

参考:
 rangeQueryリファレンス
 rangeQuery Javadoc

matchQuery

全文クエリを実行するための標準クエリ。指定されたテキスト、数値、日付、またはブール値に一致するドキュメントを返します。

第一引数にフィールド名、第二引数に検索ワードを指定します。

QueryBuilders.matchQuery("FashionItemName", "t-shirts");

参考:
 matchQueryリファレンス
 matchQuery Javadoc

multiMatchQuery

前述したmatchQuery()に基づいて構築され、複数フィールドにまたがる検索クエリを可能にします。

第一引数に検索ワード、第二引数以降に検索するフィールドを指定します。

QueryBuilders.multiMatchQuery("t-shirts", "FashionItemName", "FashionItemCategoryName");  

参考:
 multiMatchQueryリファレンス
 multiMatchQuery Javadoc

boolQuery

複数のクエリを組み合わせるために使用します。AND, OR, NOTを組み合わせることができます。

boolQueryには4種類あります。

クエリ 説明
must SQLのANDと同義。指定された条件によってスコアが計算されます。
filter SQLのANDと同義。mustとは異なり、スコアが計算されません。
should SQLのORと同義。
must not SQLのNOTと同義。
Integer[] fashionItemCategoryIds = {1, 2};
QueryBuilders.boolQuery()
    .filter(QueryBuilders.termQuery("FashionItemId", 1)
    .filter(QueryBuilders.termsQuery("FashionItemCategoryId", fashionItemCategoryIds));

FunctionScoreQueryBuilder

Elasticsearchはデフォルトでscoreの高い順にソートされて検索結果が返ってきます。このscoreをチューニングすることによって目的に最適な結果を取得できるようになります。

そのために用いるのがFunctionScoreQueryBuilderです。function_scoreは、queryにヒットするドキュメントに対して、functionsに複数のスコアリングのルール(function)を設定して検索結果のソートを行うことができます。

function_scoreについて詳しく知りたい方は公式リファレンスでご確認ください。

※score…検索条件に適合したドキュメントを返すための基準となる値のこと。

下記は書き方の一例です。

ArrayList<FunctionScoreQueryBuilder.FilterFunctionBuilder> functionScoreArrayList = new ArrayList<>();

filterFunctionList.add(
    new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("FashionItemId", 1),
        ScoreFunctionBuilders.fieldValueFactorFunction("field1").factor(Float.valueOf("0.0254389"
)).missing(0.2)));
filterFunctionList.add(
    new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchAllQuery(),
        ScoreFunctionBuilders.fieldValueFactorFunction("field2").factor(Float.valueOf("0.0937572"
)).missing(0.2)));

// ArrayList型をFunctionScoreQueryBuilder.FilterFunctionBuilder[]にする
FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = functionScoreArrayList.toArray(new FunctionScoreQueryBuilder.FilterFunctionBuilder[functionScoreArrayList.size()]);
FunctionScoreQueryBuilder functionScoreQueryBuilder = new FunctionScoreQueryBuilder(queryBuilder, functions).scoreMode(FunctionScoreQuery.ScoreMode.SUM).boostMode(CombineFunction.REPLACE);
searchSourceBuilder.query(functionScoreQueryBuilder);

参考:
 Function score queryリファレンス
 FunctionScoreQueryBuilder Javadoc

fetchSource

Elasticsearchの_sourceはSQLのSELECT句と同義で取得したいフィールドを指定できます。APIの場合は取得するフィールドをfetchSourceを使って指定します。

取得するフィールドを絞り込むことでデータ量の削減にもつながるので、速度の改善が期待できます。

第一引数には取得するフィールド、第二引数に除外するフィールドを指定します。

searchSourceBuilder.fetchSource(new String[]{"FashionItemId", "ItemPrice", "FashionItemSize",
    "FashionItemLargeCategory", "FashionItemSubCategory"},
    "ExclusionFashionItemId");

参考:
 Source filteringリファレンス

FieldSortBuilder

FieldSortBuilderはSQLのORDER BYと同義で、ソートの制御に使用します。 SearchSourceBuilderオプションの1つで、SearchSourceBuilderに対して.sortでソートを追加できます。

searchSourceBuilder.sort(new FieldSortBuilder("ItemPrice").order(SortOrder.ASC))
    .sort(new FieldSortBuilder("FashionItemId").order(SortOrder.DESC))
    .sort(new FieldSortBuilder("StartDatetime").order(SortOrder.DESC));

参考:
 Sortリファレンス
 FieldSortBuilderリファレンス

from & size

SQLのOFFSETLIMITと同義です。

SearchSourceBuilderのオプションの1つで、SearchSourceBuilderに対して.from .sizeで指定できます。

SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.from(0);
searchSourceBuilder.size(100);

参考:
 ページングリファレンス
 SearchSourceBuilderリファレンス

SQLとElasticsearchとJavaでそれぞれ同じクエリを表現

SQLで以下のように書くクエリをElastiseach、Javaで記述するとどのように書くことができるのか比較してみます。

StartDatetime >= '2019-12-06 17:33:18'
AND (
    (
        FashionItemLargeCategory <> 1
        AND FashionItemSubCategory NOT IN (10,20,30)
        AND FashionItemSize IN (1,2)
    ) OR (
        (
            FashionItemLargeCategory = 2
            OR FashionItemSubCategory IN (40,50,60)
        )
        AND FashionItemSize IN (9,10)
    )
)

Elasticsearchの場合。

"bool": {
  "filter": [
    {
      "range": {
        "StartDatetime": {
          "from": "2019-12-06 17:33:18",
          "to": null,
          "include_lower": true,
          "include_upper": true
        }
      }
    },
    {
      "bool": {
        "filter": [
          {
            "bool": {
              "should": [
                {
                  "bool": {
                    "filter": [
                      {
                        "terms": {
                          "FashionItemSize ": [
                            1,
                            2
                          ]
                        }
                      }
                    ],
                    "must_not": [
                      {
                        "term": {
                          "FashionItemLargeCategory ": {
                            "value": 1,
                            "boost": 1
                          }
                        }
                      },
                      {
                        "terms": {
                          "FashionItemSubCategory": [
                            10,
                            20,
                            30
                          ]
                        }
                      }
                    ]
                  }
                },
                {
                  "bool": {
                    "filter": [
                      {
                        "terms": {
                          "FashionItemSize": [
                            9,
                            10
                          ]
                        }
                      }
                    ],
                    "should": [
                      {
                        "term": {
                          "FashionItemLargeCategory ": {
                            "value": 1
                          }
                        }
                      },
                      {
                        "terms": {
                          "FashionItemSubCategory ": [
                            40,
                            50,
                            60
                          ]
                        }
                      }
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}

Javaの場合。

Integer[] subCategories1 = {10, 20, 30};
Integer[] itemSizes1 = {1, 2};
Integer[] subCategories2 = {40, 50, 60};
Integer[] itemSizes2 = {9, 10};

BoolQueryBuilder qb1 = boolQuery()
    .mustNot(termQuery("FashionItemLargeCategory", 1))
    .mustNot(termsQuery("FashionItemSubCategory", subCategories1))
    .filter(termsQuery("FashionItemSize", itemSizes1));

BoolQueryBuilder qb2 = boolQuery()
    .should(termQuery("FashionItemLargeCategory", 2))
    .should(termsQuery("FashionItemSubCategory", subCategories2))
    .filter(termsQuery("FashionItemSize", itemSizes2));

BoolQueryBuilder qb3 = boolQuery()
    .should(qb1)
    .should(qb2);

BoolQueryBuilder qb4 = boolQuery()
    .filter(rangeQuery("StartDatetime").from("2019-12-06 17:33:18").to(null))
    .filter(qb3);

Javaで生成したクエリの確認方法

BoolQueryBuilderに対してtoString()することでElasticsearchのクエリを取得できます。

取得したクエリを想定していたクエリと照らし合わせたり、Kibanaで実行するなどしてクエリが正しいものか確認しましょう。

System.out.println(qb4.toString());

SearchResponse

SearchResponseはRestHighLevelClientに対して.searchを指定することで使用できます。

SearchResponse searchResponse = null;
// searchRequestにヒットした検索結果をSearchResponseの形式で取得できます。
searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 取得したドキュメントにアクセスするにはまずレスポンスに含まれるSearchHitsを取得する必要があります。
SearchHits searchHits = searchResponse.getHits();
// 個々の検索結果はSearchHits内にネストされているので細かい検索結果を見たい場合はこのように取得します。
SearchHit[] searchHit = searchHits.getHits();
// ドキュメントの総ヒット数はTotalHitsに対して.valueで取得できます。
long totalHitsNum = searchHits.getTotalHits().value;
// ドキュメントの最大スコアはSearchHitsに対して.getMaxScore()で取得できます。
float maxScore = searchHits.getMaxScore();

for (SearchHit hit : searchHit) {
    // 各ヒットのスコアを取得できます。
    float score = hit.getScore();
    // Map<String, Object>形式でドキュメントソースを取得できます。
    Map<String, Object> source = hit.getSourceAsMap();
    // Map形式で取得したドキュメントソース対して、キー名を指定して.get("フィールド名")で値を取得できます。
    Integer fashionItemId = Integer.parseInt(sourceAsObject.get("FashionItemId").toString());
}

参考:
 SearchAPIリファレンス
 SearchResponse Javadoc

CountResponse

CountResponseはRestHighLevelClientに対して.countを指定することで使用できます。

CountResponse countResponse = null;
countResponse = restHighLevelClient.count(countRequest, RequestOptions.DEFAULT);
long count = countResponse.getCount();

このようにするとsearchRequestにヒットした件数をlong型で取得できます。

参考:
 CountAPIリファレンス
 CountResponse Javadoc

開発中苦労したこと

  • Elasticsearchのクエリに自信が持てない中、開発すること。
  • 少し踏み込んだ実践的なElasticsearchのJava APIの参考となる資料が少なく、壁にぶつかると中々乗り越えられない。

私はElasticsearchの経験が無かったのでこの記事を読んでいる方の状況に近かったと思います。少しでもそのような方々の参考になればと思います。

ECプラットフォーム部 マイグレーションチームでは、仲間を募集しています。ご興味のある方は、こちらからご応募ください。 tech.zozo.com

カテゴリー