이전 포스팅에서 환경설정과 파라미터에 대하여 알아보았습니다.

이번 포스팅에선 Query DSL의 주요 쿼리에 대하여 알아보도록 하겠습니다.

Query DSL의 주요 쿼리

Match All Query

  • match_all 파라미터를 사용하여 모든 문서를 검색하는 쿼리입니다. 가장 단순한 쿼리로 일반적으로 색인에 저장된 문서를 확인할 때 사용됩니다.
    POST movie_search/_search
    {
      "query": {
          "match_all": {}
      }
    }
  • 결과는 다음과 같습니다
    { 
      "took": 68,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": { 
        "total": 63069,
        "max_score": 1,
        "hits": [
         ... 중략
      }
    }

Match Query

  • 텍스트, 숫자, 날짜 등이 포함된 문장을 형태소 분석을 통해 텀으로 분리한 후 이 텀들을 이용해 검색 질의를 수행합니다. 검색어가 분석돼야 할 경우에 사용합니다.
    POST movie_search/_search
    {
      "query": {
          "match": {
              "movieNm": "그대 장미"
          }
      }
    }
  • "그대 장미"는 2개의 텀으로 분리된 후 별도의 operator 필드가 지정되어 있지 않기 때문에 두 개의 텀을 대상으로 OR 연산을 이용해 검색을 수행합니다. 결과는 다음과 같습니다.
    { 
      "took": 48,
      "timed_out": false,
      "_shards": { 
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": { 
        "total": 50,
        "max_score": 11.7566185,
        "hits": [ 
          { 
            "_index": "movie_search",
            "_type": "_doc",
            "_id": "JD3JqmkBjjM-ebDb8AAR",
            "_score": 11.7566185,
            "_source": { 
              "movieCd": "20166602",
              "movieNm": "그대 이름은 장미(가제)",
              "movieNmEn": "",
              "prdtYear": "2016",
              "openDt": "",
              "typeNm": "장편",
              "prdtStatNm": "후반작업",
              "nationAlt": "한국",
              "genreAlt": [
                "코미디"
              ],
              "repNationNm": "한국",
              "repGenreNm": "코미디",
              "directors": [
                { 
                  "peopleNm": "조석현"
                }
              ],
              "companys": [
                {
                  "companyCd": "20124237",
                  "companyNm": "(주)엠씨엠씨"
                }
              ]
            }
          },
      ... 중략
    }

Multi Match Query

  • Match Query와 기본적인 사용 방법은 동일하나 단일 필드가 아닌 여러 개의 필드를 대상으로 검색해야 할때 사용합니다.
    POST movie_search/_search
    {
      "query": {
          "multi_match": {
              "query": "가족",
              "fields": ["movieNm", "movieNmEn"]
          }
      }
    }
  • 결과는 다음과 같습니다.
    { 
      "took": 17,
      "timed_out": false,
      "_shards": { 
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      ... 중략
    }

Term Query

  • 별도의 분석 작업을 수행하지 않고 입력된 텍스트가 존재하는 문서를 찾습니다. keyword 데이터 타입을 사용하는 필드를 검색하려면 Term 쿼리를 이용해야 합니다. 필드에 텀이 정확히 존재하지 않는 경우 검색이 되지 않고, 영문 대소문자가 다를 경우 검색이 되지 않으므로 특히 주의해야 합니다.

    POST movie_search/_search
    {
      "query": {
          "term": {
              "genreAlt": "코미디"
          }
      }
    }
  • 결과는 다음과 같습니다.

    {
      "took": 81,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": 6590,
        "max_score": 2.2890944,
        "hits": [
          { 
            "_index": "movie_search",
            "_type": "_doc",
            "_id": "dDzJqmkBjjM-ebDb8PsR",
            "_score": 2.2890944,
            "_source": {
              "movieCd": "20179163",
              "movieNm": "반도에 살어리랏다",
              "movieNmEn": "I'll Just Live in Bando",
              "prdtYear": "2017",
              "openDt": "20180125",
              "typeNm": "장편",
              "prdtStatNm": "개봉예정",
              "nationAlt": "한국",
              "genreAlt": [ 
                "애니메이션",
                "코미디"
              ],
              "repNationNm": "한국",
              "repGenreNm": "애니메이션",
              "directors": [
                { 
                  "peopleNm": "이용선"
                }
              ],
              "companys": [ 
    
              ]
            }
          },
          ... 중략
    }

Bool Query

  • 관계형데이터베이스처럼 AND, OR로 묶은 여러 조건을 WHERE 절에서 사용하는데 ElasticSearch에서도 Bool Query를 통해 지원합니다. must: =, must not: !=, should: or, filter: in에 대응합니다.
    POST movie_search/_search
    {
      "query": {
          "bool": {
              "must": [
                  {
                      "term": {
                          "regGenreNm": "코미디"
                      }
                  },
                  {
                      "match": {
                          "repNationNm": "한국"
                       }
                  }
              ],
              "must_not": [
                  {
                      "match": {
                          "typeNm": "단편"
                      }
                  }
              ]
          }
      }
    }
  • 결과는 다음과 같습니다.
    { 
      "took": 31,
      "timed_out": false,
      "_shards": { 
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": {
        "total": 459,
        "max_score": 4.009591,
        "hits": [ 
          { 
            "_index": "movie_search",
            "_type": "_doc",
            "_id": "izzJqmkBjjM-ebDb8PsR",
            "_score": 4.009591,
            "_source": { 
              "movieCd": "20177473",
              "movieNm": "비밥바룰라",
              "movieNmEn": "",
              "prdtYear": "2018",
              "openDt": "20180124",
              "typeNm": "장편",
              "prdtStatNm": "개봉",
              "nationAlt": "한국",
              "genreAlt": [ 
                "코미디"
              ],
              "repNationNm": "한국",
              "repGenreNm": "코미디",
              "directors": [
                { 
                  "peopleNm": "이성재"
                }
              ],
              "companys": [ 
                { 
                  "companyCd": "20142433",
                  "companyNm": "영화사김치(주)"
                }
              ]
            }
          },
          ... 중략
    }      

Query String

  • 엘라스틱서치에는 기본적으로 내장된 쿼리 분석기가 있습니다. 주의할 점은 기존 텀 쿼리와 다르게 공백은 연산자로 사용되지 않으며 입력된 텍스트 그대로 형태소 분석기에 전달됩니다.
    POST movie_search/_search
    {
      "query": {
          "query_string": {
              "default_field": "movieNm",
              "query": "(가정)" AND (어린이 날)"
          }
      }
    }

Prefix Query

  • 해당 접두어가 있는 모든 문서를 검색하는데 사용됩니다.

    POST movie_search/_search
    {
      "query": {
          "prefix": {
              "movieNm": "자전차"
          }
      }
    }
  • 결과는 다음과 같습니다.

    { 
      "took": 27,
      "timed_out": false,
      "_shards": { 
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": { 
        "total": 1,
        "max_score": 1,
        "hits": [ 
          { - 
            "_index": "movie_search",
            "_type": "_doc",
            "_id": "GD3JqmkBjjM-ebDb8AQS",
            "_score": 1,
            "_source": { 
              "movieCd": "20176442",
              "movieNm": "자전차왕 엄복동",
              "movieNmEn": "",
              "prdtYear": "2017",
              "openDt": "",
              "typeNm": "장편",
              "prdtStatNm": "후반작업",
              "nationAlt": "한국",
              "genreAlt": [ 
                "드라마"
              ],
              "repNationNm": "한국",
              "repGenreNm": "드라마",
              "directors": [ 
    
              ],
              "companys": [ 
    
              ]
            }
          }
        ]
      }
    }

Exists Query

  • 문서를 색인할 때 필드의 값이 없다면 필드를 생성하지 않거나 필드의 값을 null로 설정할 때가 있습니다. 이러한 데이터를 제외하고 실제 값이 존재하는 문서만 찾고 싶다면 Exists Query를 사용합니다.
    POST movie_search/_search
    {
      "query": {
          "exists": {
              "field": "movieNm"
          }
      }
    }

Wildcard Query

  • * 문자의 길의와 상관 없이 와일드 카드와 일치하는 모든 문서를 찾습니다. ? 지정된 위치의 한 글자가 다른 경우의 문서를 찾습니다.
    POST movie_search/_search
    {
      "query": {
          "wildcard": {
              "typeNm": "곤*"
          }
      }
    }
  • 결과는 다음과 같습니다.
    { 
      "took": 28,
      "timed_out": false,
      "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      },
      "hits": { 
        "total": 28,
        "max_score": 1,
        "hits": [ 
          { 
            "_index": "movie_search",
            "_type": "_doc",
            "_id": "djzJqmkBjjM-ebDb8PsR",
            "_score": 1,
            "_source": { 
              "movieCd": "20180804",
              "movieNm": "곤지암",
              "movieNmEn": "",
              "prdtYear": "2016",
              "openDt": "",
              "typeNm": "장편",
              "prdtStatNm": "개봉예정",
              "nationAlt": "한국",
              "genreAlt": [ 
                "공포",
                "호러",
                "",
                "스릴러"
              ],
              "repNationNm": "한국",
              "repGenreNm": "공포(호러)",
              "directors": [ 
                { 
                  "peopleNm": "정범식"
                }
              ],
              "companys": [ 
                { 
                  "companyCd": "2016841",
                  "companyNm": "(주)하이브미디어코프"
                }
              ]
            }
          },
          ... 중략
    }

Nested Query

  • 분산 시스템에서 SQL에서 지원하는 조인(join)과 유사한 기능을 수행하려면 엄청나게 많은 비용이 소모될 것입니다. 수평적으로 샤드가 얼마나 늘어날지 모르는 상황에서 모든 샤드를 검색해야 할 수도 있기에 때문입니다. 이러한 경우에 대비해 ES에서는 Nested Query를 제공합니다. Nested 쿼리는 Nested 데이터 타입의 필드를 검색할 때 사용합니다. Nested 타입은 문서 내부에 다른 문서가 존재할 때 사용합니다.

부가적인 검색 API

Search Shards API

  • 검색이 수행되는 노드 및 샤드에 대한 정보를 확인할 수 있습니다.

    POST movie_search/_search_shards
  • 결과를 보면 movie_search 인덱스는 1개의 노드에 저장되어 있고, 인덱스는 5개의 샤드로 나누어져 저장된 것을 확인할 수 있습니다.

    { - 
      "nodes": { - 
        "j_-O0PdJSZCu3FOfoy1ihw": { - 
          "name": "javacafe-node1",
          "ephemeral_id": "wvfnAl1fRIaLDTw7eP5FFQ",
          "transport_address": "127.0.0.1:9300",
          "attributes": { - 
            "ml.machine_memory": "8589934592",
            "xpack.installed": "true",
            "ml.max_open_jobs": "20",
            "ml.enabled": "true"
          }
        }
      },
      "indices": { - 
        "movie_search": { - 
    
        }
      },
      "shards": [ - 
        [ - 
          { - 
            "state": "STARTED",
            "primary": true,
            "node": "j_-O0PdJSZCu3FOfoy1ihw",
            "relocating_node": null,
            "shard": 0,
            "index": "movie_search",
            "allocation_id": { - 
              "id": "zo0nojo8Ri6zgmNotWD-MQ"
            }
          }
        ],
        [ - 
          { - 
            "state": "STARTED",
            "primary": true,
            "node": "j_-O0PdJSZCu3FOfoy1ihw",
            "relocating_node": null,
            "shard": 1,
            "index": "movie_search",
            "allocation_id": { - 
              "id": "KIDGgXvSQnCUsfDjSXChmQ"
            }
          }
        ],
        [ - 
          { - 
            "state": "STARTED",
            "primary": true,
            "node": "j_-O0PdJSZCu3FOfoy1ihw",
            "relocating_node": null,
            "shard": 2,
            "index": "movie_search",
            "allocation_id": { - 
              "id": "-wMfyPutQeaWjQ2V64-0cw"
            }
          }
        ],
        [ - 
          { - 
            "state": "STARTED",
            "primary": true,
            "node": "j_-O0PdJSZCu3FOfoy1ihw",
            "relocating_node": null,
            "shard": 3,
            "index": "movie_search",
            "allocation_id": { - 
              "id": "y38SDjTKRrGQnLa79TyCYw"
            }
          }
        ],
        [ - 
          { - 
            "state": "STARTED",
            "primary": true,
            "node": "j_-O0PdJSZCu3FOfoy1ihw",
            "relocating_node": null,
            "shard": 4,
            "index": "movie_search",
            "allocation_id": { - 
              "id": "McuTBAzwT8a-yk658wNY3w"
            }
          }
        ]
      ]
    }

Multi Search API

  • 여러건의 검색 요청을 통합해서 한번에 요청하고 한번에 결과를 종합해서 받을때 사용됩니다.

Count API

  • 문서 본문보다 검색된 문서의 갯수만 필요할 때 사용합니다.
    POST movie_search/_count
    {
      "query": {
          "query_string": {
              "default_field": "prdtYear",
              "query": "2017"
          }
      }
    }
  • 결과는 다음과 같습니다.
    { 
      "count": 2114,
      "_shards": { 
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
      }
    }

Validate API

  • 쿼리가 유효하게 작성 됐는지 검증이 가능합니다.
    {
      "query": {
          "match": {
              "prdtYear": 2017
          }
      }
    }
  • 유효할 경우
    { 
      "_shards": { 
        "total": 1,
        "successful": 1,
        "failed": 0
      },
      "valid": true
    }
  • 유효하지 않을 경우
    { 
      "_shards": { 
        "total": 1,
        "successful": 1,
        "failed": 0
      },
      "valid": false
    }

Explain API

  • 검색 결과를 확인하면 _score를 통해 검색한 키워드와 검색 결과가 얼마나 유사한지 확인할 수 있습니다. 만약, _score 값이 어떻게 계산 됐는지 정보를 알고 싶을 경우 사용합니다.

Profile API

  • 쿼리에 대한 상세한 수행 계획과 각 수행 계획별로 수행된 시간을 돌려주므로 성능을 튜닝학나 디버깅할때 유용하게 활용됩니다.

-> 질의 결과에 대한 스코어 계산 정보를 알려면 Explain을, 질의를 실행하는 과정에서 각 샤드별로 얼마나 시간이 소요되었는지 확인하려면 Profile API를 이용합니다.

 

 

참조

  • 엘라스틱서치 실무가이드 chap4
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며


  이번 포스팅에선 AWS 파일서버인 S3에 대하여 알아보고, S3의 생성, 연결 및 관리 방법에 대하여 알아보도록 하겠습니다.




S3란?

 

  S3란 Simple Storage Service의 약자입니다. 아마존에서 사용하는 파일 서버로 용량에 관계없이 파일을 저장할 수 잇고 웹에서 HTTP 프로토콜을 이용하여 파일에 접근할 수 있습니다. S3를 써야 하는 이유는 성능과 비용에 있습니다. 대용량의 파일 저장을 EC2와 EBS를 통해 구축한다면 상당히 많은 비용이 청구됩니다. (EC2는 RDS와 함께 AWS에서 많은 비용을 차지하는 부분중에 하나이기 때문에 EC2서버와 파일서버를 분리하는 것을 권장합니다) S3는 저장 용량이 무한대이고 파일저장에 최적화 되어 있습니다. 


 따라서, 동적 웹페이지를 EC2에 구축하고, 이미지 관련 정적 파일 등은 S3에 업로드 하여 구축합니다. S3는 흔히 웹하드와 비교하곤 하지만 HTTP를 이용한 파일 업로드/다운로드를 처리하기에 사용하기에 쉽습니다.


* S3 기본 개념


- 객체 (Object)

: S3에 데이터가 저장되는 최소 단위입니다. 객체는 파일과 메타데이터로 구성됩니다.

: 기본적으로 키(Key)가 객체의 이름이며 값(Value)이 객체의 데이터입니다.

: 객체 하나의 크기는 1 바이트부터 5TB까지 지원됩니다.


-  버킷(bucket)

: S3에서 생성할 수 있는 최상위 폴더입니다.

: 버킷은 리전(지역) 별로 생성해야 합니다. 버킷의 이름은 모든 S3 리전 중에서 유일해야 합니다

: 폴더 생성이 가능하고 버킷안에 객체가 저장됩니다.

: 저장 가능한 객체의 개수와 용량은 무제한입니다.

: 접속 제어 및 권한 관리가 가능합니다.


S3 버킷생성




먼저, S3 대쉬보드에서 버킷을 누른후 '버킷 만들기'를 클릭합니다.




 다음으로 버킷이름과 리전을 선택하여 줍니다. 리전은 지역 위치에 따라 속도가 차이가 많이 나므로, 운영할 서버 EC2에 위치한 리전으로 선택 해주는게 좋습니다. 버킷이름은 유니크 하기 때문에 이미 리전에 생성된 이름이 있으면 사용할 수 없습니다.



 생성된 버킷에 들어가면 다음과 디렉토리를 만들고 업로드, 다운로드를 대쉬보드내에서 진행 할 수 있습니다. 하지만, 이렇게 사용하려고 S3를 구성한 것은 아니니 코드(Java)로 제어하는 방법을 알아보도록 하겠습니다. Java 이외에 python, node.js, kotlin, .net 등의 언어로도 지원 가능합니다.



코드로 S3 다루기 feat Java

 

 먼저, S3를 다루는 몇가지 방식이 있습니다. 


1) IAM 계정 추가 방식

- 계정에는 access_key와 secret_key가 부여됩니다.

- s3 full access라는 정책을 계정에 부여합니다.

- s3 client에 access_key, secret_key를 부여한 credential 정보를 부여하고 bucket에 접근하여 업로드, 변경, 다운로드를 수행합니다.

AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

AmazonS3 s3 = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();


2) EC2에 역할 부여

- EC2에 S3에 대한 full access 정책을 할당합니다.

- s3 client를 할당하여 api를 통해 업로드, 변경, 다운로드를 수행합니다.

AmazonS3 s3 = AmazonS3ClientBuilder.standard().withRegion(Regions.DEFAULT_REGION).build();
엑세스 권한이 부여된 1) key 접근 방식은 보안적인 측면에서 여러 문제가 발생할 소지가 있기 때문에, AWS에서는 2번째 방법으로 S3의 파일 데이터를 다루는 것은 추천하고 있습니다!!

간단히 API를 보겠습니다.

- bucket_name : 버킷 이름 (유니크한 이름)
- key_name : 저장될 파일 이름 (해당 경로에 위치될 이름입니다)
- file : 파일

* upload 

try {
s3.putObject(bucket_name, key_name, new File(file_path));
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}

* download

try {
S3Object o = s3.getObject(bucket_name, key_name);
S3ObjectInputStream s3is = o.getObjectContent();
FileOutputStream fos = new FileOutputStream(new File(key_name));
byte[] read_buf = new byte[1024];
int read_len = 0;
while ((read_len = s3is.read(read_buf)) > 0) {
fos.write(read_buf, 0, read_len);
}
s3is.close();
fos.close();
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
} catch (FileNotFoundException e) {
System.err.println(e.getMessage());
} catch (IOException e) {
System.err.println(e.getMessage());
}

* move, copy

try {
s3.copyObject(from_bucket, object_key, to_bucket, object_key);
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}

* delete

try {
s3.deleteObject(bucket_name, object_key);
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}


* 정리
s3 Object API 다루기 (bucket도  API 코드로 다룰수 있지만 혹시 모르니 버킷은 대시보드 내에서 작업 하시는걸 추천드립니다)
1) aws 클라우드 내 작업
- s3에 대한 엑세스 권한 부여

2) Java code 내 작업
- credential 부여 (1번, 2번 방법에 따라 optional)
- amazones3 client 주입
- api 수행


출처


AWS는 문서화가 잘 되어 있기에 공식 문서를 많이 찾아 보는걸 추천드립니다!!


AWS 공식 개발 document

AWS s3 Object Java API

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

초기 세팅


 엘라스틱서치는 일반적으로 대용량 데이터를 처리하기 때문에 클러스터를 구성해서 사용합니다. 따라서, 로컬에서도 클러스터를 구성해서 돌려보기로 하였습니다. 테스트 환경은 아래와 같습니다.



OS

- mac


IDE

- IntelliJ 


Build

- Gradle


Dependency

-implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:6.4.3'


엘라스틱서치

- 클러스터 1개

- node 3개 사용 (port 9200, 9201, 9202)

- monitoring tool cerebro (port 9000)

- cluster-name: javacafe(공통)

- node-name: node1, node2, node3 (개별)




다운로드 링크


엘라스틱서치 6.6.0

https://www.elastic.co/kr/downloads/past-releases/elasticsearch-6-6-0

세레브로 

https://github.com/lmenezes/cerebro/releases



클러스터 구성


 파일을 받았으면 압축을 풀고 elasticsearch-6.6.0을 2개 더 복사하여 클러스터를 만들 준비를 하여 줍니다.


 여기서 주의 사항이 있는데, 인덱스를 만들면 data 파일이 생깁니다. 이렇게 된 경우 yml 파일을 수정하여도 기존 데이터들이 이전 yml 파일을 기준으로 생성되었기 때문에 오류가 생깁니다. 이렇게 된 경우 elasticsearch-6.6.0/data를 밀고 복사하여 줍니다.


 이제 vim으로 해당 파일들을 열어 클러스터를 구성하여 줍니다. 경로는 아래와 같습니다. 주의할 점은 클러스터네임은 공통으로 하여주는 것이고, 노드네임은 다르게 지어주는 것입니다. port는 설정을 따로 하지 않았는데, 일반적으로 마스터노드는 9200번 차순의 노드들은 9201, 9202순으로 매핑되어 집니다.


elasticsearch-6.6.0/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node1

node.master: true

node.data: true

elasticsearch-6.6.0-0/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node2

node.master: false

node.data: true

elasticsearch-6.6.0-1/config/elasticsearch.yml

cluster.name: javacafe-cluster

node.name: javacafe-node3

node.master: false

node.data: true


 클러스터 구성은 끝이 났습니다. 엘라스틱서치를 실행하여 봅시다. elasticsearch-6.6.0/bin에 있는 elasticsearch 파일을 각자 실행시켜 줍니다. 실행방법은 ./elasticsearch 입니다.


 마찬가지로 cerebro (모니터링 툴)도 실행하여 줍니다. 키바나도 있지만 여기선 cerebro를 사용하여 노드들의 상태를 확인할 것 입니다. 실행방법은 ./cerebro 입니다.




실행을 하게 되면 각 노드들이 올라온 것을 볼 수 있습니다.





 마지막으로, localhost:9000번으로 접속해 봅니다. 마스터노드의 9200번으로 접속하면 아래와 같은 화면이 나올것입니다.




커넥트를 하게 되면 클러스터안에 노드 3대가 떠 있는 것을 볼 수 있을것 입니다.



INDEX API


 이제 클러스터까지 구성해 봤으니, Spring Boot로 API를 실행해 보겠습니다. API 구성은 다음과 같습니다.


  • 인덱스 생성
  • 인덱스 닫기
  • 인덱스 오픈
  • 인덱스 삭제


 여기서 의문이 생길 수 있는데, 인덱스-타입이 일반 RDB의 테이블과 같다고 보면 이해가 쉬울 것 입니다. 개념적인 부분은 구글이나 책을 사서 꼭 공부를 해보길 추천합니다.


 코드보다 엘라스틱서치는 rest를 지원하기 때문에 툴을 이용하여 인덱스를 생성하는 것이 편합니다. cerebro에 있는 rest를 이용하여 movie_rest 인덱스를 생성할 수 있습니다.


movie_rest PUT


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
    "settings": {
            "number_of_shards"3,
            "number_of_replicas"2
    },
    "mappings": {
        "_doc": {
            "properties": {
                "movieCd": { "type" : "integer" },
                "movieNm": { "type" : "text" },
                "movieNmEn": { "type" : "text" },
                "prdtYear": { "type" : "integer" },
                "openDt": { "type" : "date" },
                "typeNm": { "type" : "keyword" },
                "prdtStatNm": { "type" : "keyword" },
                "nationAlt": { "type" : "keyword" },
                "genreAlt": { "type" : "keyword" },
                "repNationNm": { "type" : "keyword" },
                "repGenreNm": { "type" : "keyword" }
            }
        }
    }
}
cs



 이제 코드로 작성하여 봅니다. 우선 build.gradle에 dependency를 추가하여 줍니다.

implementation 'org.elasticsearch.client:elasticsearch-rest-high-level-client:6.4.3'


@Autowired
private RestHighLevelClient client;

// Index명
private final String INDEX_NAME = "movie_rest";

// 타입명
private final String TYPE_NAME = "_doc";

@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}


먼저, before/ after로 커넥션을 관리하여 주었습니다.

@Test
public void index_테스트1_생성() throws IOException {

// 매핑정보
XContentBuilder indexBuilder = jsonBuilder()
.startObject()
.startObject(TYPE_NAME)
.startObject("properties")
.startObject("movieCd")
.field("type", "keyword")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNm")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNmEn")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.endObject()
.endObject()
.endObject();

// 매핑 설정
CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);
request.mapping(TYPE_NAME, indexBuilder);

// Alias 설정
String ALIAS_NAME = "moive_auto_alias";
request.alias(new Alias(ALIAS_NAME));

boolean acknowledged = client.indices()
.create(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}

 인덱스 생성 API인데 인덱스 > 타입 > 프로퍼티 순으로 작성후 CREATE를 하였습니다. 테스트를 수행하면 정상작동을 하게 되는데 cerebro를 이용하여 클러스트를 확인해 보겠습니다.



 movie_rest 인덱스가 생성된 것을 볼 수 있습니다. 


추가적으로 실선과 점선으로 생긴 블록들이 생긴 것을 볼 수 있는데 '샤드'와 '레플리카' 라는 것입니다. 이부분이 엘라스틱서치의 중요한 개념인데 엘라스틱서치는 방대한 데이터를 클러스터내에서 분산으로 나누어 요청을 처리합니다. 병렬로 데이터를 처리하기 때문에 빠르게 작업을 하는 것이고 어떠한 이유로 한 노드가 죽게 되어도 '샤드'의 복제본을 다른 노드에서 '레플리카'로 가지고 있기 때문에 모든 데이터를 검색할 수 있습니다.


이 부분은 중요 개념이기에 샤드와 레플리카를 모르시는 분들은 꼭 .. 공부할 것을 권장합니다!!



@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class IndexApiTest {

@Autowired
private RestHighLevelClient client;

// Index명
private final String INDEX_NAME = "movie_rest";

// 타입명
private final String TYPE_NAME = "_doc";

@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}

@Test
public void index_테스트1_생성() throws IOException {

// 매핑정보
XContentBuilder indexBuilder = jsonBuilder()
.startObject()
.startObject(TYPE_NAME)
.startObject("properties")
.startObject("movieCd")
.field("type", "keyword")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNm")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.startObject("movieNmEn")
.field("type", "text")
.field("store", "true")
.field("index_options", "docs")
.endObject()
.endObject()
.endObject()
.endObject();

// 매핑 설정
CreateIndexRequest request = new CreateIndexRequest(INDEX_NAME);
request.mapping(TYPE_NAME, indexBuilder);

// Alias 설정
String ALIAS_NAME = "moive_auto_alias";
request.alias(new Alias(ALIAS_NAME));

boolean acknowledged = client.indices()
.create(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트2_닫기() throws IOException{

CloseIndexRequest requestClose = new CloseIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices().close(requestClose, RequestOptions.DEFAULT).isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트3_오픈() throws IOException{

OpenIndexRequest requestOpen = new OpenIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices().open(requestOpen, RequestOptions.DEFAULT).isAcknowledged();

assertThat(acknowledged, is(true));
}

@Test
public void index_테스트4_삭제() throws IOException {

DeleteIndexRequest request = new DeleteIndexRequest(INDEX_NAME);

boolean acknowledged = client.indices()
.delete(request, RequestOptions.DEFAULT)
.isAcknowledged();

assertThat(acknowledged, is(true));
}
}


추가적으로 닫기, 오픈, 삭제 API가 있는데 코드를 수행해보고 cerebro를 이용해 클러스터의 상태를 확인해 보길 바랍니다.



DOCUMENT API


 이제 인덱스-타입을 생성하였으니 데이터를 쌓아 보겠습니다. 이부분을 RDB와 비교해보면 '인덱스-타입' = '테이블', '다큐먼트' = '행' 이라고 이해하면 됩니다.


@Test
public void index_테스트1_insert() throws IOException {

IndexRequest request = new IndexRequest(INDEX_NAME,TYPE_NAME, ID);

request.source(jsonBuilder()
.startObject()
.field("movieCd", "20173732")
.field("movieNm", "살아남은 아이")
.field("movieNmEn", "Last Child")
.endObject()
);


IndexResponse response = client.index(request, RequestOptions.DEFAULT);
String id = response.getId();
RestStatus status = response.status();

assertThat(id, is("1"));
assertThat(status, is(RestStatus.CREATED));

}


1번 ID에 다음 데이터를 넣어보고 데이터를 확인해 보겠습니다. REST 통신을 지원하니 cerebro에서 실행해 보겠습니다.



인덱스/타입/아이디 HTTP METHOD

movie_rest/_doc/1 GET



 데이터가 정상적으로 들어왔습니다. version값이 보인다 해당 아이디에 crud에서 생성/수정/삭제 를 수행하면 version이 올라가는데 이 부분은 증분색인과 관련이 있습니다.


@RunWith(SpringRunner.class)
@SpringBootTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DocumentApiTest {

@Autowired
private RestHighLevelClient client;
// Index명
private final String INDEX_NAME = "movie_rest";
// 타입명
private final String TYPE_NAME = "_doc";
// 문서 키값
private final String ID = "1";


@Before
public void connection_생성() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("127.0.0.1", 9200, "http")));
}

@After
public void connection_종료() throws IOException {
client.close();
}

@Test
public void index_테스트1_insert() throws IOException {

IndexRequest request = new IndexRequest(INDEX_NAME,TYPE_NAME, ID);

request.source(jsonBuilder()
.startObject()
.field("movieCd", "20173732")
.field("movieNm", "살아남은 아이")
.field("movieNmEn", "Last Child")
.endObject()
);


IndexResponse response = client.index(request, RequestOptions.DEFAULT);
String id = response.getId();
RestStatus status = response.status();

assertThat(id, is("1"));
assertThat(status, is(RestStatus.CREATED));

}

@Test
public void index_테스트2_get() throws IOException {

GetRequest request = new GetRequest( INDEX_NAME, TYPE_NAME,ID);

GetResponse response = client.get(request, RequestOptions.DEFAULT);

Map<String, Object> sourceAsMap = response.getSourceAsMap();
String movieCd = (String) sourceAsMap.get("movieCd");
String movieNm = (String) sourceAsMap.get("movieNm");
String movieNmEn = (String) sourceAsMap.get("movieNmEn");

assertThat(movieCd, is("20173732"));
assertThat(movieNm, is("살아남은 아이"));
assertThat(movieNmEn, is("Last Child"));
}

@Test
public void index_테스트3_update() throws IOException {

XContentBuilder builder = jsonBuilder()
.startObject()
.field("createdAt", new Date())
.field("prdtYear", "2018")
.field("typeNm", "장편")
.endObject();

UpdateRequest request = new UpdateRequest(INDEX_NAME, TYPE_NAME, ID).doc(builder);

UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT);
RestStatus status = updateResponse.status();

assertThat(status, is(RestStatus.OK));
}

@Test
public void index_테스트4_delete() throws IOException {

DeleteRequest request = new DeleteRequest(INDEX_NAME, TYPE_NAME, ID);
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);

RestStatus status = deleteResponse.status();
assertThat(status, is(RestStatus.OK));
}
}

 INDEX,DOCUMENT이외에도 검색, 집계에 관한 API가 있는데 이 부분은 추후에 또 올려보도록 하겠습니다.



---

 오류나 오타의 댓글을 남겨 주면 수정하도록 하겠습니다.


참조


[책] 엘라스틱서치 실무 가이드

[링크]  https://github.com/javacafe-project/elastic-book/tree/master/src/main/java/io/javacafe/client/rest


링크에 있는 API를 테스트 케이스로 만들어 기재하였습니다.

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며


API 문서를 보면, http통신을 할 경우 데이터 타입을 어떻게 주고 받을지

정의하여 줍니다.


그러한 타입의 종류로 xml이나 json을 많이 사용하고 있고,

과거에는 xml을 많이 사용하였다면 최근에는 json 형태를 많이 이용하고 있습니다.



JSON이란?

위에서는 다음과 같이 설명하고 있습니다.


JSON은 속성-값 쌍 또는 "키-값 쌍"으로 이루어진 데이터 오브젝트를 전달하기 위해 인간이 읽을 수 있는 텍스트를 사용하는 개방형 표준 포맷이다. 비동기 브라우저/서버 통신 을 위해, 넓게는 XML을 대체하는 주요 데이터 포맷이다


위의 설명대로, JSON은 key-value로 이루어진 어떠한 Object입니다.


즉, key-value를 기반으로 하는 데이터 포맷입니다.



JSON 사용해보기


사용법을 간단히 익힌다면, API를 만들고 데이터를 주고 받는게 간단해집니다.


우선, json을 먼저 만들어 보겠습니다.



jsonObject의 key값으로 접근하여 value값을 가져올 수 있습니다.


JSON은 데이터를 파싱하는 방법으로 stringify와 parse 함수를 제공하는데요,

JSON을 string으로 변경하여 보겠습니다.


jsonString이라는 객체를 만들어서 jsonObject의 데이터를 String으로 바꾸어 파싱하였습니다.

그러면 위와 같이 데이터 타입이 다르게 찍히는 것을 확인할 수 있습니다.


String으로 변경 되었기 때문에, 이전과 같이 key값으로 접근을 할 수 없게 되는 것이지요.


이번에는 String을 JSON으로 parsing하는 작업을 해보겠습니다.



이번에는 JSON.parse를 이용하여 string을 JSON 데이터 타입으로 파싱하였습니다.

JSON타입이기 때문에 key값으로 접근하여 데이터를 가져오는 것을 확인 할 수 있습니다.




이처럼 stringify와 parse 함수를 적절히 이용한다면


API를 읽고 사용하거나, 만드는 데 활용할 수 있습니다!!

블로그 이미지

사용자 yhmane

댓글을 달아 주세요