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));
SearchRequest
下記リファレンスより引用。
SearchRequestは、ドキュメント、集約、サジェストを検索する操作に使用され、結果として得られるドキュメントのハイライト表示を要求する方法も提供します。
とありますが、APIを使ってElasticsearchへリクエストを送るための大元になるものという捉え方でよいと思います。
※Elasticsearchでは、データをドキュメントという単位で扱います。
下記コードの詳細は後述のライブラリ紹介の中で説明します。
SearchRequest searchRequest = new SearchRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.termQuery("FashionItemId", 1)); searchRequest.source(searchSourceBuilder);
CountRequest
CountRequest
は実行したクエリに一致したドキュメント数を取得するために使用します。
CountRequest countRequest = new CountRequest(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); searchSourceBuilder.query(QueryBuilders.matchAllQuery()); countRequest.source(searchSourceBuilder);
QueryBuilders
検索クエリを作成するためのユーティリティクラスです。
参考:
QueryBuilders一覧
下記に代表的な検索クエリとQueryBuildersユーティリティクラス内の対応するQueryBuilderクラスおよびヘルパーメソッドを紹介します。
matchAllQuery
すべてのドキュメントを取得します。
QueryBuilders.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");
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のOFFSET
とLIMIT
と同義です。
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