📢 この記事は gemini-2.5-flash によって翻訳されました
Elasticsearch シリーズ
ドキュメントのクエリも同じく RestHighLevelClient オブジェクトを使うよ。
match_all
リクエストを発行するのはこんな感じ。
1
2
3
4
5
6
7
8
9
10
11
| @Test
public void testMatchAll() throws IOException {
// request を準備
SearchRequest request = new SearchRequest("hotel");
// DSL パラメータを組み立て
request.source().query(QueryBuilders.matchAllQuery());
// リクエストを送信
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
System.out.println(response);
}
|
レスポンスのパース
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @Test
public void testMatchAll() throws IOException {
// request を準備
SearchRequest request = new SearchRequest("hotel");
// DSL パラメータを組み立て
request.source().query(QueryBuilders.matchAllQuery());
// リクエストを送信
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 結果をパース
SearchHits searchHits = response.getHits();
// クエリの合計件数
long total = searchHits.getTotalHits().value;
// クエリの結果配列
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println(json);
}
}
|
Elasticsearch が返す結果は JSON 文字列で、こんな内容が含まれてるんだ:
- hits:ヒットした結果
- total:合計件数。value が具体的な合計件数だよ。
- max_score:全結果の中で最もスコアの高いドキュメントの関連性スコア
- hits:検索結果のドキュメント配列。それぞれのドキュメントは JSON オブジェクトだよ。
- source:ドキュメント内の元データ。これも JSON オブジェクトだね。
だから、レスポンス結果をパースするっていうのは、JSON 文字列を階層的に解析していくってこと。流れはこんな感じだよ:
SearchHits:response.getHits() で取得できるよ。これは JSON の一番外側の hits で、ヒットした結果を表してる。SearchHits.getTotalHits().value:合計件数情報を取得するんだ。SearchHits.getHits():SearchHit 配列、つまりドキュメント配列を取得するよ。SearchHit.getSourceAsString():ドキュメント結果の _source、つまり元の JSON ドキュメントデータを取得するんだ。
match と multi_match
match_all と似てるけど、違いはクエリ条件だよ。
match のコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Test
public void testMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchQuery("all", "如家"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
System.out.println(total);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println(json);
}
}
|
multi_match のコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| @Test
public void testMultiMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.multiMatchQuery("如家", "brand", "name"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
System.out.println(total);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println(json);
}
}
|
コードの重複が多いのがわかるよね。Ctrl+Alt+M でコードを抽出できるよ。term のコードで抽出の例を見せるね。
精密クエリ
term 用語の精密一致クエリ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @Test
public void testTerm() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.termQuery("city", "上海"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
responseHandle(response);
}
// レスポンス処理のコード抽出
private static void responseHandle(SearchResponse response) {
SearchHits searchHits = response.getHits();
long total = searchHits.getTotalHits().value;
System.out.println(total);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
System.out.println(json);
}
}
|
range 範囲クエリ
1
2
3
4
5
6
7
8
9
10
11
| @Test
public void testRange() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders
.rangeQuery("price")
.gte(100)
.lte(400));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
responseHandle(response);
}
|
ブールクエリ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| @Test
public void testBool() throws IOException {
SearchRequest request = new SearchRequest("hotel");
// bool クエリを構築
BoolQueryBuilder booledQuery = QueryBuilders.boolQuery();
// must 条件を追加
booledQuery.must(QueryBuilders.termQuery("city", "上海"));
// filter コンポーネントを追加
booledQuery.filter(QueryBuilders.rangeQuery("price").lte(300));
request.source().query(booledQuery);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
responseHandle(response);
}
|
ソートとページネーション
1
2
3
4
5
6
7
8
9
10
| @Test
public void testSort() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source().query(QueryBuilders.matchAllQuery());
request.source().from(10).size(10);
request.source().sort("price", SortOrder.ASC);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
responseHandle(response);
}
|
ハイライト
ハイライトは上記のコードと結構違うから、リクエストの構築から見ていこう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Test
public void testHigh() throws IOException {
SearchRequest request = new SearchRequest("hotel");
// DSL
request.source().query(QueryBuilders.matchQuery("all", "汉庭"));
// ハイライト
request.source().highlighter(
new HighlightBuilder()
.field("name")
.requireFieldMatch(false)
);
// リクエストを送信
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// パース
responseHandle(response);
}
|
ドキュメントのクエリ結果とハイライトは別々だから、結果のパースは追加で処理が必要だよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| @Test
public void testHigh() throws IOException {
SearchRequest request = new SearchRequest("hotel");
// DSL
request.source().query(QueryBuilders.matchQuery("all", "汉庭"));
// ハイライト
request.source().highlighter(
new HighlightBuilder()
.field("name")
.requireFieldMatch(false)
);
// リクエストを送信
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// パース
SearchHits searchHits = response.getHits();
// 合計件数
long total = searchHits.getTotalHits().value;
System.out.println(total);
// ドキュメント配列
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
// 逆シリアル化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
// ハイライト結果を取得
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if(!CollectionUtils.isEmpty(highlightFields)){
// ハイライト結果を取得
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null){
String name = highlightField.getFragments()[0].toString();
// ハイライトされていないものを上書き
hotelDoc.setName(name);
}
}
System.out.println(hotelDoc);
}
}
|
ホテル検索の事例
4つの機能を実装するよ:
- ホテルの検索とページネーション
- ホテル結果のフィルタリング
- 自分の周辺のホテル
- ホテルの入札ランキング
検索とページネーション
検索リクエスト:
- リクエスト方式:POST
- リクエストパス:/hotel/list
- リクエストパラメータ:JSON オブジェクトで、4つのフィールドが含まれるよ:
- key:検索キーワード
- page:ページ番号
- size:1ページあたりのサイズ
- sortBy:ソート。今はまだ実装しないよ。
- 戻り値:ページングクエリで、ページング結果の PageResult を返す必要があるよ。2つのプロパティが含まれる:
total:合計件数List<HotelDoc>:現在のページデータ
まず、エンティティクラスを定義してパラメータを受け取るよ。
1
2
3
4
5
6
7
| @Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
}
|
戻り値のクラスを定義するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Data
public class PageResult {
private Long total;
private List<HotelDoc> hotels;
public PageResult(){
}
public PageResult(Long total, List<HotelDoc> hotels) {
this.total = total;
this.hotels = hotels;
}
}
|
Controller を定義するよ。
1
2
3
4
5
6
7
8
9
10
11
| @RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
private IHotelService hotelService;
@PostMapping("/list")
public PageResult search(@RequestBody RequestParams params){
return hotelService.search(params);
}
}
|
検索ビジネスを実装するんだけど、まず Bean オブジェクトを登録するんだ。
1
2
3
4
5
6
| @Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient
.builder(HttpHost.create("http://ip:9200")
));
}
|
ロジックを書くよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| public PageResult search(RequestParams params) {
// request
SearchRequest request = new SearchRequest("hotel");
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// DSL
String key = params.getKey();
if (key == null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
}else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// ページネーション
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// クエリ
request.source().query(boolQuery);
// リクエストを送信
try {
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// レスポンスパース
SearchHits searchHits = response.getHits();
// 合計数
long total = searchHits.getTotalHits().value;
// ドキュメント
SearchHit[] hits = searchHits.getHits();
// イテレート
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
hotels.add(hotelDoc);
}
return new PageResult(total, hotels);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
|
結果のフィルタリング
含まれるフィルタリング条件は:
- brand:ブランド値
- city:都市
- minPrice~maxPrice:価格範囲
- starName:星評価
エンティティクラスを修正するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
// 下は追加したフィルタリング条件パラメータだよ
private String city;
private String brand;
private String starName;
private Integer minPrice;
private Integer maxPrice;
}
|
クエリ条件を修正するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| @Override
public PageResult search(RequestParams params) {
// request
SearchRequest request = new SearchRequest("hotel");
basicQuery(params, request);
// ページネーション
int page = params.getPage();
int size = params.getSize();
request.source().from((page - 1) * size).size(size);
// リクエストを送信
try {
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// レスポンスパース
SearchHits searchHits = response.getHits();
// 合計数
long total = searchHits.getTotalHits().value;
// ドキュメント
SearchHit[] hits = searchHits.getHits();
// イテレート
List<HotelDoc> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
hotels.add(hotelDoc);
}
return new PageResult(total, hotels);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void basicQuery(RequestParams params, SearchRequest request) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 入力内容
String key = params.getKey();
if (key == null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
}else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// brand
if (params.getBrand() != null && !params.getBrand().equals("")){
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// starName
if (params.getStarName() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// city
if (params.getCity() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// price
if (params.getMinPrice() != null && params.getMaxPrice() != null){
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(params.getMinPrice()).lte(params.getMaxPrice()));
}
// クエリ
request.source().query(boolQuery);
}
|
近くのホテル
location 座標に基づいて、周囲のホテルを距離でソートするよ。
エンティティクラスを修正するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Data
public class RequestParams {
private String key;
private Integer page;
private Integer size;
private String sortBy;
private String city;
private String brand;
private String starName;
private Integer minPrice;
private Integer maxPrice;
// 自分の現在の地理座標
private String location;
}
|
距離によるソートを追加するよ。
1
2
3
4
5
6
7
8
| if (params.getLocation() != null) {
// 距離ソート
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint(params.getLocation()))
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS)
);
}
|
距離表示
HotelDoc を修正して、距離を追加するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| @Data
@NoArgsConstructor
public class HotelDoc {
private Long id;
private String name;
private String address;
private Integer price;
private Integer score;
private String brand;
private String city;
private String starName;
private String business;
private String location;
private String pic;
// 距離
private Object distance;
public HotelDoc(Hotel hotel) {
this.id = hotel.getId();
this.name = hotel.getName();
this.address = hotel.getAddress();
this.price = hotel.getPrice();
this.score = hotel.getScore();
this.brand = hotel.getBrand();
this.city = hotel.getCity();
this.starName = hotel.getStarName();
this.business = hotel.getBusiness();
this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
this.pic = hotel.getPic();
}
}
|
レスポンス処理を修正するよ。
1
2
3
4
5
6
7
8
9
10
| for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
Object[] sortValues = hit.getSortValues();
if (sortValues.length > 0){
Object sortValue = sortValues[0];
hotelDoc.setDistance(sortValue);
}
hotels.add(hotelDoc);
}
|
広告ホテルの追加
要件:指定したホテルを検索結果で上位に表示させたい。
指定ホテルにフラグを追加して、フィルタリング条件でこのフラグに基づいて function_score を上げるかどうかを判断するんだ。
HotelDoc に広告フラグフィールドを追加するよ。
DSL を使って、いくつかのホテルにフラグを追加するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # 広告を追加
POST /hotel/_update/607915
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/728461
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/7094829
{
"doc": {
"isAD": true
}
}
POST /hotel/_update/198323591
{
"doc": {
"isAD": true
}
}
|
スコアリング関数クエリを追加して、basicQuery() メソッドを修正するよ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| private static void basicQuery(RequestParams params, SearchRequest request) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 入力内容
String key = params.getKey();
if (key == null || "".equals(key)){
boolQuery.must(QueryBuilders.matchAllQuery());
}else {
boolQuery.must(QueryBuilders.matchQuery("all", key));
}
// brand
if (params.getBrand() != null && !params.getBrand().equals("")){
boolQuery.filter(QueryBuilders.termQuery("brand", params.getBrand()));
}
// starName
if (params.getStarName() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("starName", params.getStarName()));
}
// city
if (params.getCity() != null && !params.getStarName().equals("")){
boolQuery.filter(QueryBuilders.termQuery("city", params.getCity()));
}
// price
if (params.getMinPrice() != null && params.getMaxPrice() != null){
boolQuery.filter(QueryBuilders
.rangeQuery("price")
.gte(params.getMinPrice())
.lte(params.getMaxPrice())
);
}
// スコアリング function_score
FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
// 元のクエリ
boolQuery,
// 配列
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// function score の要素の一つ
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
// フィルタリング条件
QueryBuilders.termQuery("isAD", true),
// スコアリング関数
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
// クエリ
request.source().query(functionScoreQuery);
}
|