본문 바로가기
ElasticSearch

Elasticsearch API conventions

by Salt-Fn 2024. 10. 4.

Elasticsearch Java Client 문서의 번역입니다. GPT를 이용해 번역하였고 잘못된 부분이 있을수도 있습니다. 잘못된 부분은 댓글로 달아주시면 수정하겠습니다. 감사합니다.
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/package-structure.html


Java API 클라이언트는 복잡한 요청을 더 쉽게 작성하고 복잡한 응답을 더 쉽게 처리할 수 있도록 하는 최신 코드 패턴을 사용하여 매우 일관된 코드 구조를 사용합니다. 아래 세센에서는 이에 대해 자세히 설명합니다.

Package structure and namesapce clients

Elasticsearch API 문서에서 볼 수 있듯이 Elasticsearch API는 규모가 크고 기능 그룹으로 구성되어 있습니다.

 

Java API 클라이언트는 다음 구조를 따릅니다. 기능 그룹을 namespace라고 하며 각 namespace는 co.elastic.clients.elasticsearch의 하위 패키지에 있습니다.

 

각 namespace client는 최상위 Elasticsearch client에서 엑세스할 수 있습니다. 유일한 예외는 핵심 하위 패키지에 있고 기본 Elasticsearch client 개체에서 엑세스할 수 있는 "search" 와 "document" API 입니다.

 

아래 코드블럭은 index namespace client를 사용하여 인덱스를 생성하는 방법을 보여줍니다.

// Create the "products" index
ElasticsearchClient client = ...
client.indices().create(c -> c.index("products"));

Namespace clients는 즉시 생성할 수 있는 매우 가벼운 객체입니다.

Method naming conventions

Java API 클라이언트의 클래스에는 두 가지 종류의 메서드와 속성이 포함되어 있습니다.

  • Elasticsearch.search() 나 SearchResponse.maxScore()와 같은 API의 메서드와 속성들은 Elasticsearch JSON API에서 해당 이름을 가져와 표준 camelCase 명명 규칙을 사용하였습니다.
  • Query._kind()와 같이 Java API 클라이언트가 빌드되는 프레임워크의 일부인 메서드 및 속성입니다. 이러한 메서드와 속성에는 API 이름과의 이름 충돌을 방지하고 API와 프레임워크를 쉽게 구별할 수 있도록 밑줄이 앞에 붙습니다.

Blocking and asynchronous clients

API 클라이언트는 차단과 비동기라는 두 가지 형태로 제공됩니다. 비동기 클라이언트의 모든 메서드는 표준 CompletableFuture를 반환합니다.

ElasticsearchTransport transport = ...

// Synchronous blocking client
ElasticsearchClient client = new ElasticsearchClient(transport);

if (client.exists(b -> b.index("products").id("foo")).value()) {
    logger.info("product exists");
}

// Asynchronous non-blocking client
ElasticsearchAsyncClient asyncClient =
    new ElasticsearchAsyncClient(transport);

asyncClient
    .exists(b -> b.index("products").id("foo"))
    .whenComplete((response, exception) -> {
        if (exception != null) {
            logger.error("Failed to index", exception);
        } else {
            logger.info("Product exists");
        }
    });

Java의 비동기 프로그래밍에 대해 더 자세히 설명하지는 않지만 비동기 작업의 실패를 처리하는 것을 기억하세요. 이를 간과하고 오류를 간과하기 쉽습니다.

위 예제의 소스 코드는 Java API 클라이언트 테스트에서 찾을 수 있습니다.

Building API objects

Builder objects

Java API 클라이언트의 모든 데이터 유형은 변경할 수 없습니다. 객체 생성은 빌더 패턴을 사용합니다.

ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices().create(
    new CreateIndexRequest.Builder()
        .index("my-index")
        .aliases("foo",
            new Alias.Builder().isWriteIndex(true).build()
        )
        .build()
);

build() 메서드가 호출된 후에는 빌더를 재사용하면 안 됩니다.

Builder lambda expressions

위 방법은 훌륭하게 작동하지만 빌더 클래스를 인스턴스화하고 build() 메서드를 호출해야 하는 것은 약간 장황합니다. 따라서 Java API 클라이언트의 모든 속성 설정자는 새로 생성된 빌더를 매개변수로 사용하고 채워진 빌더를 반환하는 람다 식도 허용합니다. 위의 스니펫은 다음과 같이 작성할 수도 있습니다.

ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices()
    .create(createIndexBuilder -> createIndexBuilder
        .index("my-index")
        .aliases("foo", aliasBuilder -> aliasBuilder
            .isWriteIndex(true)
        )
    );

이 접근 방식은 훨씬 더 간결한 코드를 작성할 수 있게 해주며, 메서드의 매개변수 시그니처(형식 정보)에서 타입이 추론되기 때문에 클래스를 따로 임포트하거나 이름을 기억할 필요가 없다는 장점도 있습니다.


위의 예에서 빌더 변수는 속성 설정자 체인을 시작하는 데만 사용된다는 점에 유의하세요. 따라서 이러한 변수의 이름은 중요하지 않으며 가독성을 높이기 위해 단축될 수 있습니다.

ElasticsearchClient client = ...
CreateIndexResponse createResponse = client.indices()
    .create(c -> c
        .index("my-index")
        .aliases("foo", a -> a
            .isWriteIndex(true)
        )
    );

빌더 람다(builder lambdas)는 intervals 쿼리 API 문서에서 가져온 아래와 같은 복잡한 중첩 쿼리에 특히 유용합니다.

 

이 예시는 깊게 중첩된 구조에서 빌더 매개변수를 위한 유용한 명명 규칙을 보여줍니다. 단일 인수를 가진 람다 표현식의 경우, Kotlin은 암시적인 it 매개변수를 제공하고, Scala는 _를 사용할 수 있습니다. 이는 Java에서 언더스코어나 단일 문자 접두어 뒤에 깊이 수준을 나타내는 숫자를 붙여서 비슷하게 구현할 수 있습니다(예: _0, _1 또는 b0, b1 등). 이는 일회성 변수 이름을 만들 필요를 없애줄 뿐만 아니라 코드의 가독성도 향상시킵니다. 올바른 들여쓰기는 쿼리의 구조를 더욱 두드러지게 만듭니다.

ElasticsearchClient client = ...
SearchResponse<SomeApplicationData> results = client
    .search(b0 -> b0
        .query(b1 -> b1
            .intervals(b2 -> b2
                .field("my_text")
                .allOf(b3 -> b3
                    .ordered(true)
                    .intervals(b4 -> b4
                        .match(b5 -> b5
                            .query("my favorite food")
                            .maxGaps(0)
                            .ordered(true)
                        )
                    )
                    .intervals(b4 -> b4
                        .anyOf(b5 -> b5
                            .intervals(b6 -> b6
                                .match(b7 -> b7
                                    .query("hot water")
                                )
                            )
                            .intervals(b6 -> b6
                                .match(b7 -> b7
                                    .query("cold porridge")
                                )
                            )
                        )
                    )
                )
            )
        ),
    SomeApplicationData.class // 1
);

1. 검색 결과는 애플리케이션에서 쉽게 사용할 수 있도록 SomeApplicationData 인스턴스에 매핑됩니다.

my_text 필드에서 "my favorite food"라는 구를 정확히 찾고, 그 후에 "hot water" 또는 "cold porridge" 중 하나가 나오면 그 문장이 검색 결과로 반환됩니다.

"my favorite food"는 추가적인 단어가 끼어들지 않고, 지정된 단어들이 순서대로 나와야 하지만, "hot water"와 "cold porridge"는 순서와 상관없이 둘 중 하나만 있으면 됩니다.

List and maps

Additive builder setters

List와 Map 타입의 속성은 객체 빌더에서 오버로딩된 추가 전용 메서드로 노출됩니다. 이 메서드는 리스트에 항목을 추가하고, 맵에는 새로운 항목을 추가하거나 기존 항목을 대체하면서 속성 값을 업데이트합니다.

 

객체 빌더는 불변 객체를 생성하며 이는 객체 생성 시 불변으로 만들어진 목록 및 맵 속성에도 적용됩니다.

// index name list 준비
List<String> names = Arrays.asList("idx-a", "idx-b", "idx-c");

// "foo" 및 "bar" 필드에 cardinality 집계 준비
Map<String, Aggregation> cardinalities = new HashMap<>();
cardinalities.put("foo-count", Aggregation.of(a -> a.cardinality(c -> c.field("foo"))));
cardinalities.put("bar-count", Aggregation.of(a -> a.cardinality(c -> c.field("bar"))));

// "size" 필드의 평균을 계산하는 집계를 준비합니다.
final Aggregation avgSize = Aggregation.of(a -> a.avg(v -> v.field("size")));

SearchRequest search = SearchRequest.of(r -> r
    .index(names)    // list에 있는 모든 index에 검색 쿼리 수행
    .index("idx-d")    // "idx-d" 단일 index에서 검색 쿼리 수행
    .index("idx-e", "idx-f", "idx-g")    // "idx-e", "idx-f", "idx-g" index에서 검색 쿼리 수행

    // 정렬 순서 리스트 : 빌더 람다로 정의된 요소들을 추가. 여러 정렬 기준 설정.
    .sort(s -> s.field(f -> f.field("foo").order(SortOrder.Asc)))
    .sort(s -> s.field(f -> f.field("bar").order(SortOrder.Desc)))

    .aggregations(cardinalities) // 집계 맵: 기존 맵의 모든 항목을 추가함
    .aggregations("avg-size", avgSize)    // 키/값 항목을 추가함
    .aggregations("price-histogram",    // 빌더 람다로 정의된 키/값 항목을 추가함
        a -> a.histogram(h -> h.field("price")))
);

List and map values are never null

  • 단일 값 속성은 null로 처리:
    • Elasticsearch API의 단일 값 속성이 제공되지 않을 경우, Java API Client에서는 이를 null로 나타냅니다. 그래서 단일 값에 대해서는 애플리케이션에서 null 체크가 필요합니다.
  • 리스트와 맵은 null이 아닌 빈 컬렉션:
    • List와 Map과 같은 컬렉션 속성의 경우, 값이 제공되지 않더라도 null이 아니라 빈 컬렉션으로 반환됩니다. 이로 인해, null 체크를 하는 대신, 리스트가 비어있는지만 확인하면 됩니다.
  • ApiTypeHelper로 '정의되지 않음'을 구분:
    • ApiTypeHelper 클래스는 정의되지 않은(즉, 제공되지 않은) 컬렉션과 빈 컬렉션을 구분할 수 있는 유틸리티 메서드를 제공합니다.
    • 이 메서드는 failures 리스트가 단순히 비어 있는지, 또는 아예 정의되지 않은 값인지를 구분하기 위해 사용됩니다.
NodeStatistics stats = NodeStatistics.of(b -> b
    .total(1)
    .failed(0)
    .successful(1)
);

// The `failures` list was not provided.
// - it's not null
assertNotNull(stats.failures());
// - it's empty
assertEquals(0, stats.failures().size());
// - and if needed we can know it was actually not defined
assertFalse(ApiTypeHelper.isDefined(stats.failures()));

Variant types

Elasticsearch API에는 쿼리, 집계, 필드 매핑, 분석기 등 다양한 변형 유형이 있습니다. 이렇게 큰 컬렉션에서 올바른 클래스 이름을 찾는 것은 어려울 수 있습니다.

 

Java API 클라이언트 빌더를 사용하면 이를 쉽게 수행할 수 있습니다. 쿼리와 같은 변형 유형의 빌더에는 사용 가능한 각 구현에 대한 메소드가 있습니다. 위에서는 intervals(쿼리의 일종)과 allOf, match 및 anyOf(다양한 종류의 간격)를 사용하여 이를 확인했습니다.

 

이는 Java API 클라이언트의 변형 개체가 "tagged union"의 구현이기 때문입니다. 여기에는 보유한 변형의 식별자(또는 태그)와 해당 변형의 값이 포함됩니다. 예를 들어 Query 개체에는 태그 intervals가 있는 IntervalsQuery, 태그 term이 있는 TermQuery 등이 포함될 수 있습니다. 이 접근 방식을 사용하면 IDE 완성 기능을 통해 복잡한 중첩 구조를 구축하고 탐색할 수 있는 유연한 코드를 작성할 수 있습니다.

 

변형 빌더에는 사용 가능한 모든 구현에 대한 setter 메서드가 있습니다. 이는 일반 속성과 동일한 규칙을 사용하고 빌더 람다 식과 실제 변형 유형의 미리 만들어진 개체를 모두 허용합니다. 다음은 term 쿼리를 작성하는 예입니다.

Query query = new Query.Builder()
    .term(t -> t                          // 1
        .field("name")                    // 2
        .value(v -> v.stringValue("foo"))
    )
    .build();				  // 3

 

1. term 쿼리 변형을 선택하여 term query를 만듭니다.

2. 빌더 람다 표현식을 사용하여 terms query를 만듭니다.

3. 이제 TermQuery 객체를 Query 안에 담아 쿼리를 완성합니다.

 

변형 객체에는 사용 가능한 모든 구현에 대한 getter 메서드가 있습니다. 이러한 메소드는 객체가 실제로 해당 종류의 변형을 보유하고 있는지 확인하고 올바른 유형으로 다운캐스트된 값을 반환합니다. 그렇지 않으면 IllegalStateException이 발생합니다. 이 접근 방식을 사용하면 변형을 탐색하는 유창한 코드를 작성할 수 있습니다.

 

assertEquals("foo", query.term().value().stringValue());

또한 변형 개체는 현재 보유하고 있는 변형 종류에 대한 정보도 제공합니다.

 

  • isTerm(), isIntervals(), isFuzzy() 등 각 변형 종류에 대한 메서드가 있습니다.
  • 모든 변형 종류를 정의하는 중첩된 Kind 열거형이 있습니다.

이 정보는 실제 종류를 확인한 후 특정 변형을 탐색하는 데 사용될 수 있습니다.

if (query.isTerm()) { // 1
    doSomething(query.term());
}

switch(query._kind()) { // 2
    case Term:
        doSomething(query.term());
        break;
    case Intervals:
        doSomething(query.intervals());
        break;
    default:
        doSomething(query._kind(), query._get()); // 3
}

1. 변형이 특정 종류인지 테스트합니다.

2. 더 큰 변형 종류 세트를 테스트합니다.

3. 변형 개체가 보유한 종류와 값을 가져옵니다.

Custom extensions provided by Elasticsearch plugins

Elasticsearch는 여러 유형에 대한 가용 변형을 확장할 수 있는 플러그인을 수용합니다. 여기에는 queries, aggregations, text analyzerstokenizers, ingest processors 등이 포함됩니다.

 

이러한 유형에 대한 Java API Client 클래스는 기본 제공 변형 외에 _custom 변형을 수용합니다. 이를 통해 요청 시 임의의 JSON을 제공하여 플러그인에서 정의한 확장을 사용할 수 있으며, 플러그인에서 생성한 임의의 JSON을 응답으로 받을 수 있습니다.

 

아래의 예시에서는 3D 좌표를 포함하는 문서를 참조 위치에 대한 거리로 그룹화하는 sphere-distance 집계를 추가하는 가상의 플러그인을 사용합니다.

 

사용자 지정 집계를 생성하려면 _custom() 집계 유형을 사용하고 플러그인에 의해 정의된 식별자와 매개변수를 제공해야 합니다. 매개변수는 JSON으로 직렬화할 수 있는 임의의 객체나 값이 될 수 있습니다. 아래의 예시에서는 간단한 맵을 사용합니다.

Map<String, Object> params = new HashMap<>(); // 1
params.put("interval", 10);
params.put("scale", "log");
params.put("origin", new Double[]{145.0, 12.5, 1649.0});

SearchRequest request = SearchRequest.of(r -> r
    .index("stars")
    .aggregations("neighbors", agg -> agg
        ._custom("sphere-distance", params) // 2
    )
);

1. 커스텀 집계를 위한 매개변수

2. 매개변수와 함께 sphere-distance 유형의 사용자 지정 집계 neighbors를 생성합니다.

 

custom 변형의 결과는 JsonData 객체로 표시되는 원시 JSON으로 반환됩니다. 그런 다음 JSON 트리를 탐색하여 데이터를 가져올 수 있습니다. 이것이 항상 편리한 것은 아니기 때문에 해당 JSON 데이터를 나타내는 클래스를 정의하고 원시 JSON에서 역직렬화할 수도 있습니다.

 

JSON 트리 탐색

SearchResponse<Void> response = esClient.search(request, Void.class); // 1

JsonData neighbors = response
    .aggregations().get("neighbors")
    ._custom(); // 2

JsonArray buckets = neighbors.toJson() // 3
    .asJsonObject()
    .getJsonArray("buckets");

for (JsonValue item : buckets) {
    JsonObject bucket = item.asJsonObject();
    double key = bucket.getJsonNumber("key").doubleValue();
    double docCount = bucket.getJsonNumber("doc_count").longValue();
    doSomething(key, docCount);
}

1. 검색 결과가 아닌 집계 결과에만 관심이 있는 경우 Void를 사용하세요.

2. neighbors 집계 결과를 사용자 정의 JSON 결과로 가져옵니다.

3. JSON 트리를 탐색하여 결과 데이터를 추출합니다.

 

사용자 정의 집계 결과를 나타내는 클래스 사용

SearchResponse<Void> response = esClient.search(request, Void.class);

SphereDistanceAggregate neighbors = response
    .aggregations().get("neighbors")
    ._custom()
    .to(SphereDistanceAggregate.class); // 1

for (Bucket bucket : neighbors.buckets()) {
    doSomething(bucket.key(), bucket.docCount());
}

1. 사용자 정의 JSON을 전용 SphereDistanceAggregate 클래스로 역직렬화합니다.

 

여기서 SphereDistanceAggregate는 다음과 같이 정의할 수 있습니다.

public static class SphereDistanceAggregate {
    private final List<Bucket> buckets;
    @JsonCreator
    public SphereDistanceAggregate(
        @JsonProperty("buckets") List<Bucket> buckets
    ) {
        this.buckets = buckets;
    }
    public List<Bucket> buckets() {
        return buckets;
    };
}

public static class Bucket {
    private final double key;
    private final double docCount;
    @JsonCreator
    public Bucket(
        @JsonProperty("key") double key,
        @JsonProperty("doc_count") double docCount) {
        this.key = key;
        this.docCount = docCount;
    }
    public double key() {
        return key;
    }
    public double docCount() {
        return docCount;
    }
}

Object life cycles and thread safeth

Java API Client에는 서로 다른 생명 주기를 가진 다섯 가지 종류의 객체가 있습니다.

Object mapper

무상태이며 스레드 안전하지만 생성 비용이 클 수 있습니다. 일반적으로 애플리케이션 시작 시 생성되며 전송을 생성하는 데 사용되는 싱글톤입니다.

Transport

스레드 안전하며, 기본 HTTP 클라이언트를 통해 네트워크 자원을 보유합니다. 전송 객체는 Elasticsearch 클러스터와 연결되어 있으며, 네트워크 연결과 같은 기본 자원을 해제하기 위해 명시적으로 닫아야 합니다.

Clients

불변이며, 무상태이고 스레드 안전합니다. 이들은 전송을 래핑하고 API 엔드포인트를 메서드로 제공하는 매우 경량 객체입니다.

Builder

변경 가능하며, 스레드 안전하지 않습니다. 빌더는 일시적인 객체로 build()를 호출한 후 재사용해서는 안 됩니다.

Requests & other API objects

불변이며, 스레드 안전합니다. 애플리케이션에서 동일한 요청이나 요청의 일부를 반복적으로 사용하는 경우, 이러한 객체는 미리 준비하여 다양한 전송을 사용하는 여러 클라이언트 간에 여러 호출에 재사용할 수 있습니다.

Creating API objects from JSON data

Elasticsearch 애플리케이션 개발 중 흔히 사용되는 워크플로우 중 하나는 Kibana Developer Console을 사용하여 쿼리, 집계, 인덱스 매핑 및 기타 복잡한 API 호출을 상호작용 방식으로 준비하고 테스트하는 것입니다. 이 과정에서 애플리케이션에서 사용할 수 있는 JSON 스니펫이 생성됩니다.

 

이 JSON 스니펫을 Java 코드로 변환하는 것은 시간이 많이 걸리고 오류가 발생하기 쉬운데, Java API Client의 대부분의 데이터 클래스는 JSON 텍스트에서 로드될 수 있습니다. 객체 빌더는 withJson() 메서드를 통해 원시 JSON으로부터 빌더를 채울 수 있으며, 이를 통해 동적으로 로드된 JSON과 프로그래밍적으로 생성된 객체를 결합할 수 있습니다.

 

내부적으로, withJson() 메서드는 객체의 deserializer를 호출합니다. 따라서 JSON 텍스트의 구조와 값 유형이 대상 데이터 구조에 맞아야 합니다. withJson()을 사용하면 Java API Client의 강력한 형식 보장(strong typing guarantees)을 유지할 수 있습니다.

Examples

Loading an index definition from a resource file

인덱스 정의가 포함된 리소스 파일 some-index.json을 고려합니다.

{
  "mappings": {
    "properties": {
      "field1": { "type": "text" }
    }
  }
}

다음과 같이 해당 정의에서 인덱스를 만들 수 있습니다.

InputStream input = this.getClass()
    .getResourceAsStream("some-index.json"); // 1

CreateIndexRequest req = CreateIndexRequest.of(b -> b
    .index("some-index")
    .withJson(input) // 2
);

boolean created = client.indices().create(req).acknowledged();

1. JSON 리소스 파일에 대한 입력 스트림을 엽니다.

2. 인덱스 생성 요청을 리소스 파일 콘텐츠로 채웁니다.

Ingesting documents from JSON files

마찬가지로 데이터 파일에서 Elasticsearch에 저장될 문서를 읽을 수 있습니다.

FileReader file = new FileReader(new File(dataDir, "document-1.json"));

IndexRequest<JsonData> req; // 1

req = IndexRequest.of(b -> b
    .index("some-index")
    .withJson(file)
);

client.index(req);

일반 유형 매개변수가 있는 데이터 구조에서 withJson()을 호출할 때 이러한 일반 유형은 JsonData로 간주됩니다.

제네릭 타입을 가지는 데이터 구조는 다양한 타입을 가질 수 있기 때문에, withJson()을 통해 JSON을 전달할 때 제네릭 타입이 구체적으로 무엇인지 알 수 없습니다. 그래서 Elasticsearch Java API에서는 이런 경우에 해당 제네릭 타입을 JsonData라는 특수한 타입으로 처리합니다.
JsonData는 Elasticsearch의 모든 JSON 데이터를 추상화하는 클래스입니다. 즉, 이 타입은 어떤 JSON 값이라도 담을 수 있으며, 내부적으로 JSON 데이터를 유지하면서 나중에 필요할 때 특정 데이터 타입으로 변환할 수 있습니다. withJson() 메서드를 호출할 때 제네릭 타입을 JsonData로 처리함으로써, 다양한 JSON 형식을 유연하게 받아들이고 처리할 수 있게 되는 것입니다.
따라서 제네릭 타입을 갖는 데이터 구조에 withJson()을 사용할 때, 구체적인 타입 정보가 없으므로 이 타입들을 JsonData로 간주하여 유연하게 JSON 데이터를 처리합니다.

Creating a search request combining JSON and programmatic construction

withJson()을 setter 메서드에 대한 일반 호출과 결합할 수 있습니다. 아래 예에서는 문자열에서 검색 요청의 쿼리 부분을 로드하고 프로그래밍 방식으로 집계를 추가합니다.

Reader queryJson = new StringReader(
    "{" +
    "  \"query\": {" +
    "    \"range\": {" +
    "      \"@timestamp\": {" +
    "        \"gt\": \"now-1w\"" +
    "      }" +
    "    }" +
    "  }" +
    "}");

SearchRequest aggRequest = SearchRequest.of(b -> b
    .withJson(queryJson) // 1
    .aggregations("max-cpu", a1 -> a1 // 2
        .dateHistogram(h -> h
            .field("@timestamp")
            .calendarInterval(CalendarInterval.Hour)
        )
        .aggregations("max", a2 -> a2
            .max(m -> m.field("host.cpu.usage"))
        )
    )
    .size(0)
);

Map<String, Aggregate> aggs = client
    .search(aggRequest, Void.class) // 3
    .aggregations();

1. JSON 문자열에서 쿼리를 로드합니다.

2. 집계를 추가합니다.

3. 이는 집계이기 때문에 결과 문서에 신경 쓰지 않고 대상 클래스를 Void로 설정합니다. 즉, 결과 문서는 무시됩니다. 크기를 0으로 설정하면 실제로 문서가 반환되지 않습니다.

Creating a search request from multiple JSON snippets

withJson() 메서드는 부분 역직렬화 도구입니다. JSON에서 로드된 속성들은 기존 속성을 설정하거나 교체하지만, JSON 입력에서 찾을 수 없는 다른 속성들은 초기화되지 않습니다. 이를 이용하여 여러 JSON 스니펫을 결합하여 복잡한 검색 요청을 작성할 수 있습니다. 아래 예시에서는 일부 문서를 선택하는 쿼리 정의와, 이 쿼리의 결과에 대해 실행되는 집계 정의를 결합하고 있습니다.

Reader queryJson = new StringReader(
    "{" +
    "  \"query\": {" +
    "    \"range\": {" +
    "      \"@timestamp\": {" +
    "        \"gt\": \"now-1w\"" +
    "      }" +
    "    }" +
    "  }," +
    "  \"size\": 100" + // 1
    "}");

Reader aggregationJson = new StringReader(
    "{" +
    "  \"size\": 0, " + // 2
    "  \"aggregations\": {" +
    "    \"hours\": {" +
    "      \"date_histogram\": {" +
    "        \"field\": \"@timestamp\"," +
    "        \"interval\": \"hour\"" +
    "      }," +
    "      \"aggregations\": {" +
    "        \"max-cpu\": {" +
    "          \"max\": {" +
    "            \"field\": \"host.cpu.usage\"" +
    "          }" +
    "        }" +
    "      }" +
    "    }" +
    "  }" +
    "}");

SearchRequest aggRequest = SearchRequest.of(b -> b
    .withJson(queryJson) // 3
    .withJson(aggregationJson) // 4
    .ignoreUnavailable(true) // 5
);

Map<String, Aggregate> aggs = client
    .search(aggRequest, Void.class)
    .aggregations();

1. 쿼리에 대해 반환된 문서의 최대 수를 100으로 설정합니다.

2. 집계에서 일치하는 문서는 0으로 설정합니다.

3. 요청 쿼리 부분을 로드합니다.

4. 집계 쿼리 부분을 로드합니다. (쿼리의 size를 덮어씁니다.)

5. 프로그래밍적으로 추가적인 요청 속성을 설정할 수 있습니다.

.ignoreUnavailable()
Elasticsearch에서 제공하는 설정 중 하나로, 검색 요청 시 사용되는 인덱스가 일시적으로 사용 불가능한 경우나 존재하지 않을 때, 해당 인덱스를 무시하고 검색 요청을 계속 처리하도록 하는 옵션입니다.

 

JSON 스니펫에 공통된 속성이 있을 때는 순서가 중요하다는 점에 유의하세요. 프로그래밍 방식으로 속성 값을 설정할 때와 마찬가지로, 마지막으로 설정된 속성 값이 이전 값을 덮어씁니다.