본문 바로가기
프로그래밍/ElasticSearch

[elasticsearch + Spring] elasticsearch를 Java Spring에서 사용해보자 - Document CRUD 만들기

by 차곡차곡 사바라다 2021. 3. 23.

안녕하세요. 오늘은 이전 시간에 이어서 elasticsearch를 java Spring에서 사용하는 방법에 대해서 코드로 알아보도는 시간을 가져보도록 하겠습니다. 오늘 알아볼 내용은 Java Spring을 이용하여 ElasticSaerch와 기본적인 Document의 CRUD 작업을 하는 방법입니다.

Document는 ElasticSearch에 저장되는 실제 데이터입니다. RDB로 치자면 Table의 row에 해당한다고 볼 수 있습니다.

주의사항

제가 사용하는 ElasticSearch 버전은 7.9.3 버전입니다. Java High Level REST Client SDK 같은 경우 버전에 종속적이기 때문에 만약 버전을 꼭 맞추시고 사용하시기를 권장드립니다. 대표적인 예로 6 버전에는 Type 이라는 Index와 다른 값이 존재하였습니다만 7 버전으로 올라오면서 없어졌습니다. 이 외에도 미처파악하지 못한 부분에서 에러가 발생할 수 있기때문에 ElasticSearch 버전과 SDK 버전을 꼭 맞춰주시기 바랍니다.

환경 구축

환경을 구축하는 방법에 대해서는 이전 포스터를 참고해주시기바랍니다.

인덱스 이름은 game이며 RestHighLevelClient를 사용하여 통신합니다.

Document 생성

Document를 생성해보도록 하겠습니다.

Document를 생성할 때는 아래 http 인터페이스를 사용합니다. 해당 프로토콜로 사용하면 랜덤 문자열 ID를 가지는 document가 하나 생성됩니다.

POST /{index}/_doc

{Json Body}

위 프로토콜을 직접 사용하면 아래와 같습니다. 이렇게 Rest API를 사용하면 HTTP status 코드 201(Created)과 함께 응답메시지를 리턴 받습니다. 응답내용의 _id값을 보시면 랜덤 문자열이 있는것을 알 수 있습니다.

POST http://localhost:9200/game/_doc

{
    "title" : "테스트 게임",
    "content" : "테스트 게임입니다."
}
{
    "_index": "game",
    "_type": "_doc",
    "_id": "NeaAXngBmJX07A_LyBd0",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 181,
    "_primary_term": 1
}

아이디를 명시적으로 지정하여 호출할 수도 있습니다. 이렇게 호출한다면 _id의 값은 내가 지정한 값이 됩니다. 만약 _id값이 겹친다면 기존 정보가 수정되는 것을 유의하시기 바랍니다. 이때는 아래처럼 rest API를 사용해주면 됩니다.

POST /{index}/_doc/{id}

위 프로토콜을 사용하면 아래와 같이 요청 및 응답을 해주시면됩니다. 응답값의 _id 값이 우리가 입력한 test 라는 값이라는 것을 알 수 있었습니다.

POST http://localhost:9200/game/_doc/test

{
    "title" : "테스트 게임",
    "content" : "테스트 게임입니다."
}
{
    "_index": "game",
    "_type": "_doc",
    "_id": "test",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 182,
    "_primary_term": 1
}

SDK를 이용하여 Java로 코딩한다면 아래와 같이 이용할 수 있습니다. 여기서 client는 RestHighLevelClient입니다. document 생성의 Rest API를 모르더라도 간단하고 쉽게 사용할 수 있습니다.

public void createDocument(
    @NotNull String index,
    @NotNull String id,
    @NotNull String jsonBody) throws IOException {
IndexRequest request = new IndexRequest(index)
    .id(id)
    .source(jsonBody, XContentType.JSON);

client.index(request, RequestOptions.DEFAULT);
}

Document 읽기

그렇다면 이번에는 ElasticSearch의 document를 읽어보도록 하겠습니다. Rest API는 아래와 같습니다. JsonBody는 필요하지 않습니다.

GET /{index}/_doc/{document_id}

Rest API를 직접한번 사용해보겠습니다. 동작은 아래와 같습니다. 응답값의 _source를 보시면 Document가 정상적으로 생성되었다라는 사실을 알 수 있습니다.

http://localhost:9200/game/_doc/test
{
    "_index": "game",
    "_type": "_doc",
    "_id": "test",
    "_version": 1,
    "_seq_no": 182,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "title": "테스트 게임",
        "content": "테스트 게임입니다."
    }
}

Java로 구현하면 아래와 같습니다. 응답값으로 GetResponse 객체가 반환됩니다. 해당 객체의 getSourceAsString 메서드를 이용하면 _source의 값을 String으로 얻어낼 수 있습니다.

public GetResponse getDocument(String index, String id) throws IOException {
    GetRequest request = new GetRequest(index, id);
    return client.get(request, RequestOptions.DEFAULT);
}

Document 부분 수정

document를 업데이트하기 위해서는 아래와 같은 Rest API를 사용하여야합니다. Update를 하기 위해서는 해당 ID와 업데이트하고자 하는 필드가 json으로 주어져야합니다. 부분 업데이트를 진행하기 위한 Rest API는 아래와 같습니다.

POST /{index}/_doc/{document_id}/_update

{Json Body}
{
    "doc" : {
        // 부분 업데이트 fields : values
    }
}

직접 사용해보도록 하겠습니다.

POST http://localhost:9200/game/_doc/test/_update

{
    "doc" : {
      "content" : "테스트당"
    }
}

아래는 다시 test ID를 GET을 했을 때 결과입니다. 업데이트한 content만 변경된것을 확인할 수 있습니다. 또한 _version 피드의 숫자값이 올라간 것도 알 수 있었습니다.

{
    "_index": "game",
    "_type": "_doc",
    "_id": "test",
    "_version": 4,
    "_seq_no": 185,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "title": "테스트 게임",
        "content": "테스트당"
    }
}

java로 코딩하면 아래와 같습니다. String, Object의 Map을 받아 doc으로 세팅해줍니다. bodyMap은 변경하고자 하는 key, value를 세팅해줍니다. 그리고 client의 update 메서드를 이용하면 업데이트를 할 수 있습니다.

public void updateDocument(
      @NotNull String index,
      @NotNull String id,
      @NotNull Map<String, Object> bodyMap) throws IOException {
    UpdateRequest request = new UpdateRequest(index, id)
        .doc(bodyMap);

    client.update(request, RequestOptions.DEFAULT);
}

delete Document

마지막으로 삭제에 대해서 알아보도록 하겠습니다. 삭제는 DELETE 메서드를 이용합니다. Body는 없이 사용합니다.

DELETE /{index}/_doc/{document_id}

실제로 사용하면 아래처럼 사용할 수 있으며 응답값을 보시면 _version이 올라갔으며, result로 deleted된 것을 알 수 있었습니다.

DELETE http://localhost:9200/game/_doc/test
{
    "_index": "game",
    "_type": "_doc",
    "_id": "test",
    "_version": 5,
    "result": "deleted",
    "_shards": {
        "total": 1,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 186,
    "_primary_term": 1
}

java 코드로 나타내면 위의 타 메서드들과 동일한 구조를 가지게됨을 알 수 있었습니다.

public void deleteDocument(String index, String id) throws IOException {
  DeleteRequest request = new DeleteRequest(index, id);
  client.delete(request, RequestOptions.DEFAULT);
}

마무리

오늘은 이렇게하여 ElasticSearch의 document CRUD rest API 들과 java로 추상화된 SDK를 이용하여 구현해보는 시간을 가져보았습니다.

감사합니다.

참조

elastic_moving-from-types-to-typeless-apis-in-elasticsearch-7-0

elastic_java-rest-high

댓글2

  • 고추냉이 2021.04.20 09:35

    안녕하세요! 작성해주신 글 잘 읽었습니다. 여쭤보고 싶은 점이 있는데, 혹시 해당 client의 connection은 어느 시점에 닫아야 하는지 알 수 있을까요? 작성해주신 코드를 예로 들자면 deleteDocument의 맨 마지막 줄에 작성해야 할까요?
    Elastic search 공식문서에서는 커넥션을 다 쓰면 꼭 정리하라고 적혀있는데 spring의 문서에서는 또 어느 시점에 정리해야 하는지 적혀 있지 않아서요ㅠㅠ 감사합니다!
    답글

    • 안녕하세요. 답글 감사드립니다.
      RestHighLevelClient는 내부에서 통신에 apache의 CloseableHttpAsyncClient를 사용하는데요. 내부코드에서 사용할 때 자동적으로 close 해주는 코드는 저도 찾지 못했습니다.

      restTemplate의 경우는 doExecute 메서드 코드를 보시면 close를 내부에서 처리해 주기 때문에 close를 해줄 필요가 없습니다.

      두 경우를 비교해보았을 때 RestHighLevelClient를 하는 경우 close를 명시적으로 해주어 내부에서 사용되는 CloseableHttpAsyncClient의 resource를 clear어 해주는것이 맞는것으로 보입니다.

      RestHighLevelClient를 close하는것이 RestHighLevelClient 인스턴스를 release하는것은 아닙니다.

      (혹시 틀린 부분이 있다면 알려주시면 좀 더 확인해보도록 하겠습니다. :) )