인덱스 생성

ElasticSearch는 REST를 지원하기 때문에 여러가지 방법을 이용하여 인덱스를 다룰 수 있습니다.
크게 다루는 방법은 3가지입니다

  • CURL
  • kibana
  • 언어의 라이브러르 이용

여기서는 kibana 쿼리를 이용하여 인덱스를 생성해 보도록 하겠습니다

PUT test_index
{
  "mappings": {
   "_doc": {
      "properties": {
        "testId": {
          "type": "long"
        },
        "testName": {
          "type": "keyword"
        }
      }
    }
  },
  "settings": {
    "index": {
      "refresh_interval": "1s",
      "number_of_shards": "1",
      "number_of_replicas": "1"
    }
  }
}

인덱스의 정보를 위와 같이 정의하였습니다. 사용될 문서의 정보와 샤드 레플리카 정보들로 간단히 설정하였습니다.


Document 생성

마찬가지로 인덱스와 같이 여러 방식을 이용할 수 있습니다.
여기서는 kibana 쿼리를 이용하였습니다

PUT test_index/_doc/1
{
  "testId" : 1,
  "testName" : "yunho"
}

참조

https://esbook.kimjmin.net/04-data/4.2-crud

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

엘라스틱서치를 클러스터로 운영하게 되면 다음과 같이 몇가지 문제를 맞이 할 수 있습니다

  1. 디스크 문제
  2. 메모리 문제
  3. 샤드 할당 문제
  4. 기타 등등

클러스터의 경우 마스터노드와 데이터노드 코디네이터 노드를 여러대 운영하기 때문에 노드의 추가와 제거가 가능합니다
다만, 간단한 작업의 경우 서버 재시작보다는 rolling restart 방법을 고려해 보는게 좋습니다.


문제상황 (예시)

  • 여기선, 단순히 heap 메모리 증설의 경우를 들어 설명하겠습니다.
  • 데이터 노드의 heap 메모리 부족
  • 먼저, 물리 메모리가 확보 됐는지 확인하고, 가능하다면 heap 메모리를 늘려주자

해결방안

메모리 증설은 해당 노드만 종료후 재실행시켜주면 되기에
엘라스틱서치 전체 재시작보다는 rolling-restart 방식을 취하는 것이 좋습니다

쿼리는 kibana 기준으로 작성 되어 있습니다

  • Step1 heap 메모리 재할당
    # path /elasticsearch/data/config/jvm.options
    Xms4g
    Xmx4g

샤드는 프라이머리, 레플리카로 구성됩니다.
노드가 종료되면 샤드 재배치라는 고비용 작업이 이루어지게
rolling-restart 작업을 할 시에는 샤드 재할당 기능을 종료시켜 놓습니다

  • Step2 샤드 할당 비활성

    # kibana 쿼리 / ES 6.4.2 # 노드를 중단했을때 샤드들이 재배치 되지 않도록 설정 
    PUT _cluster/settings { 
      "persistent": {     
          "cluster.routing.allocation.enable": "none" 
      } 
    }
  • Step3 세그먼트 동기화

    # kibana 쿼리 / ES 6.4.2 # primary - replica 간의 세그먼트 저장 상태를 동기화 
    POST _flush/synced
  • Step4 ES 재실행
    재실행시 elasticsearch의 권한을 잘 확인해보는 것이 좋습니다!!

  • Step5 샤드 할당 활성

    # kibana 쿼리 / ES 6.4.2 # unassigned 된 샤드 재배치 
    PUT _cluster/settings 
    { 
      "persistent": { 
          "cluster.routing.allocation.enable": null 
      } 
    }
  • step6 헬스 체크

    GET _cat/health

참조

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

이 글을 읽고 있다면 아마 높은 확률로 ES 클러스터의 디스크 문제로 발생하지 않았을까 합니다 ^^
색인 배치를 만들게 되면 디스크 full 문제가 발생할 수 있는데요, ES는 이러한 경우 읽기 모드로 전환되게 합니다.
읽기 모드에 들어가게 되면 당연히 인덱스의 변경이나 삭제가 불가능한 문제가 발생합니다

해결 방법

  1. 우선적으로 사용하지 않는 인덱스를 삭제하여 줍니다
  2. 인덱스의 읽기 모드를 false로 변경하여 줍니다 (kibana 쿼리)
    PUT _all/_settings 
    {
     "index": { 
         "blocks": { 
             "read_only_allow_delete": "false" 
         } 
     } 
    }

추가적으로 disk 문제의 경우 주기적으로 발생할 수 있는 문제라 생각되면 디스크 증설도 고려해보는게 좋다고 생각합니다.
그럴 경우 클러스터의 데이터 노드를 추가할 것을 권합니다

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

엘라스틱서치를 운영하게 되면 운영 인덱스, 백업용 인덱스들을 여러개 만들게 된다.

다만, 예기치 않은 오류가 발생하여 수동으로 alias를 직접 설정해야 할 경우도 생긴다.

되도록이면 예외처리 코드와 케이스 검사를 잘 해두어 직접 다루는 경우가 없도록 하자 

  • 인덱스 ALIAS 설정 예제 (curl)
curl -X POST "http://www.localhost.com:9200/_aliases" -H 'Content-Type: application/json' -d '{
    "actions": [
        {
            "add" : {
                "index" : "koo-product-202103291800",
                "alias" : "koo-product"
            }
        }
    ]
}'

결과 : {"acknowledged":true}%

 

  • kibana 인덱스 ALIAS 설정 예제
POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "my-data-stream",
        "alias": "my-alias"
      }
    }
  ]
}
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

서비스를 운영하다 보면 데이터 필드의 값을 더하거나 평균을 내는 등 검색 쿼리로 반환된 데이터를 집계하는 경우가 많다. 검색 쿼리의 결과 집계는 다음과 같이 기존 검색 쿼리에 집계 구문을 추가하는 방식으로 수행할 수 있다.

{
    "query": { ... 생략 ...},
    "aggs": { ... 생략 ... }
}

엘라스틱서치는 집계 시 문서를 평가한 후 기준에 만족하는 문서들을 하나로 그룹화한다.
그룹화한 집합을 토대로 집계를 수행하고, 집계가 끝나면 버킷 목록에 속한 문서의 집합이 출력 된다.


집계 종류

  • 메트릭 집계
  • 버킷집계
  • 파이프라인 집계
  • 행렬 집계

메트릭 집계

메트릭 집계는 이름에서도 알 수 있듯이 정수 또는 실수와 같이 숫자 연산을 할 수 있는 값들에 대한 집계를 수행한다. 일반적으로 필드 데이터를 사용해 집계가 이뤄지지만 스크립트를 사용해 조금 더 유연하게 집계를 수행할 수도 있다.

요청값 구조

GET /apache-web-log/_search?size=0  ---1)
{
    "aggs": { ---2)
        "<집계 이름>": { ---3)
            "<집계 타입">: { ---4)
                "field": "<필드명>" ---5)
            }
        }
    }
}
  • 1) size : 집계된 문서들의 데이터는 불필요하므로 size 값을 0 으로 지정해 반환받지 않는다.
  • 2) aggs: 집계를 수행한다. aggregation 또는 aggs를 사용
  • 3) 집계 이름 : 집계에 대한 이름이다. 하위 쿼리 또는 여러 쿼리를 함께 사용할 떄 구별하는 용도로 사용한다.
  • 4) 집계 타입 : 합계, 평규느 시계열 등의 집계 타입을 명시한다.
  • 5) field : 집계의 대상이 되는 필드를 명시한다.

응답값 구조

{
    "took": 1, 
    "timed_out": false,
    "_shards": {
        "total": 5,
        "succeccful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 200,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "<집계이름>": {
            "<집계결과>"
        }
    }
}

합산 집계

합산 집계는 단일 숫자 메트릭 집계에 해당한다. 다음과 같이 집계 쿼리를 수행한다.

GET /apache-web-log/_search?size=0
{
    "aggs": {
        "total_bytes": {
            "sum": {
                "field": "bytes"
            }
        }
    }
}

cerebro를 이용한 화면을 보자. 총 얼마만큼의 데이터 (Bytes)가 유입 됐는지 확인할 수 있다.

조금더 확장해서 사용해보자. 이번엔 filter를 추가하여 'geoip.city_name'의 필드값이 'Paris'인 것을 집계해보자

{
    "query": {
      "constant_score": {
          "filter": {
              "match": {"geoip.city_name": "Paris"}
          }
      }  
    },
    "aggs": {
        "total_bytes": {
            "sum": {
                "field": "bytes"
            }
        }
    }
}

cerebro를 이용해 결과 화면을 봐보자. 필터가 적용된 bytes를 확인할 수 있다.


평균 집계

평균 집계는 단일 숫자 메트릭 집계에 해당한다. 마찬가지로 간단한 예제를 수행해보자

{
    "aggs": {
        "avg_bytes": {
            "avg": {
                "field": "bytes"
            }
        }
    }
}

세레브로로 결과를 확인해보자. 평균값을 확인할 수 있다.

합산 집계와 마찬가지로 평균집계에도 필터를 적용하여 보자.

{
    "query": {
      "constant_score": {
          "filter": {
              "match": {"geoip.city_name": "Paris"}
          }
      }  
    },
    "aggs": {
        "avg_bytes": {
            "avg": {
                "field": "bytes"
            }
        }
    }
}


* 최솟값, 최댓값 집계*

최솟값 집계와 최댓값 집계는 모두 단일 숫자 메트릭 집계에 해당한다.

{
    "query": {
      "constant_score": {
          "filter": {
              "match": {"geoip.city_name": "Paris"}
          }
      }  
    },
    "aggs": {
        "min_bytes": {
            "min": {
                "field": "bytes"
            }
        }
    }
}

cerebro를 확인해보자. 집계이름과, 집계 타입, 결과만 확인해보자. (min, max)



개수 집계

개수집계는 단일 숫자 메트릭 집계에 해당한다.

{
    "query": {
      "constant_score": {
          "filter": {
              "match": {"geoip.city_name": "Paris"}
          }
      }  
    },
    "aggs": {
        "bytes_count": {
            "value_count": {
                "field": "bytes"
            }
        }
    }
}

위 쿼리는 파리 지역에서 일어난 사용자 요청 횟수를 집계 하였다. cerebro를 이용해 확인해보자.


통계 집계

통계 집계는 다중 숫자 메트릭 집계에 해당한다. 통계 집계를 이용하면 앞서 살펴본 합, 평균, 최대, 최소, 개수를 한번의 쿼리로 집계할 수 있다.

{
    "query": {
      "constant_score": {
          "filter": {
              "match": {"geoip.city_name": "Paris"}
          }
      }  
    },
    "aggs": {
        "bytes_stats": {
            "stats": {
                "field": "bytes"
            }
        }
    }
}

결과는 다음과 같다.

통계 이외에도, 확장 통계를 이용하여 더 다양한 집계를 할 수 있다. 표준편차와 같은 통계값이 추가 된다. 집계 타입은 (stats-> extend_stats)으로 바꿔주면 된다.


이번 포스팅에서는 집계에 대하여 간략히 알아보았습니다.
메트릭 집계는 정수 또는 실수와 같은 숫자 연산을 할 수 있는 값들에 대한 집계를 수행하는데, 조금 더 응용하면 유용하게 쓰일 수 있습니다.
다음 포스팅에서는 버킷집계, 파이프라인 집계 등에 대하여 알아보도록 하겠습니다.


참조

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

사용자 yhmane

댓글을 달아 주세요

환경설정 참고
환경설정은 검색 API를 다루었을 때 하였습니다. 집계용 스냅샷만 추가해주면 됩니다.

스냅샷 설정

  • 집계 스냅샷 생성
    curl -XPUT 'http://localhost:9200/_snapshot/apache-web-log' -H 'Content-Type:application/json' -d' {
      "type": "fs",
      "settings": {
          "location": "/Users/hwang-yunho/Documents/es/elastic-book-snapshot-master/book_backup/agg_example",
          "compress": true 
      } 
    }'
  • 결과
    {
      "acknowledged":true
    }
  • 집계 스냅샷 복구
    curl -XPOST 'http://localhost:9200/_snapshot/apache-web-log/default/_restore'
  • 결과
    {
      "acknowledged":true
    }


apache-web-log에 문서 10,001건이 생성된 것을 확인할 수 있습니다.


참조

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

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

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

이번 포스팅에선 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

댓글을 달아 주세요

이전 포스팅 에선 검색을 하기 위한 es6.4.3 환경 설정을 진행하였습니다.
이번 포스팅에선 검색 API에 대해서 알아보도록 하겠습니다.

검색

검색 질의 표현방식

  • URI검색
  • Request Body 검색
  • Query DSL 검색

URI 검색

URI 방식은 HTTP GET 요청을 활용하는 방식으로 파라미터를 'Key=Value' 형태로 전달하는 방식입니다.

  • URI에 검색할 칼럼과 검색어를 직접 지정하여 사용합니다.
  • 파라미터로 표현할 수 있는 표현의 한계로 복잡한 질의를 작성하는 것은 불가능합니다.
    GET movie_search/_search?q=prdtYear:2018

kibana나 postman 등을 이용해도 좋지만 개인적으론 cerebro가 조금 더 편한거 같네용ㅎㅎ

자주 사용하는 파라미터

  • q : 검색을 수행할 쿼리 문자열 조건을 지정
  • df : 쿼리에 검색을 수행할 필드가 지정되지 않았을 경우 기본값으로 검색할 필드를 지정
  • analyzer : 쿼리 문자열을 형태로 분석할 때 사용할 형태소 분석기를 지정
  • analyze_wildcard : 접두어/와일드카드(*) 검색 활성화 여부를 지정
  • default_operator : 두개 이상의 검색 조건이 쿼리 문자열에 포함된 경우 검색 조건 연산자를 설정
  • _source : 검색 결과에 문서 본문 포함 여부를 지정
  • sort : 검색 결과의 정렬 기준 필드를 지정
  • from : 검색을 시작할 문서의 위치를 설정
  • size : 반환할 검색 결과 개수를 설정

Request Body 검색

Request Body 방식은 본문에 JSON 형태로 검색 조건을 기록해서 검색을 요청합니다.

POST movie_search/_search
{
    "query": {
        "query_string": {
            "default_field": "movieNmEn",
            "query": "Family"
        }
    }
}




Query DSL

JSON 구조를 기반으로 하여 제공합니다. 여러 개의 질의를 조합하거나 질의 결과에 대해 다시 검색을 수행하는 등 강력한 검색이 가능해집니다.

Query DSL 구조

  • request
    {
    "size": -- 1) return 받는 결과의 갯수
    "from": -- 2) 몇 번째 문서부터 가져올지 지정. 기본값은 0
    "timeout": -- 3) 검색을 요청해서 결과를 받는 데까지 걸리는 시간. 기본값은 무한대
    "_source": {} -- 4) 검색시 필요한 필드만 출력하고 싶을 때 사용
    "query": {} -- 5) 검색 조건문이 들어가야 하는 공간
    "aggs": {} -- 6) 통계 및 집계 데이터를 사용할 때 사용하는 공간
    "sort": {} -- 7) 문서 결과를 어떻게 출력할지에 대한 조건을 사용하는 공간
    }
  • response
    {
    "took": -- 1) 쿼리를 실행한 시간
    "timed_out": -- 2) 쿼리 시간이 초과할 경우
    "_shards": {
      "total": -- 3) 쿼리를 요청한 전체 샤드의 갯수
      "successful": -- 4) 검색 요청에 성공적으로 응답한 샤드의 갯수
      "failed": -- 5) 검색 요청에 실패한 샤드의 갯수
    },
    "hits": {
      "total": -- 6) 검색어에 매칭된 문서의 전체 갯수
      "max_score": -- 7) 일치하는 문서의 스코어 값 중 가장 높은 값
      "hits": [] -- 8) 각 문서 정보의 스코어 값
    }
    }

Query DSL 쿼리와 필터

쿼리 컨텍스트

  • 문서와 쿼리가 얼마나 유사한지 스코어로 계산
  • 질의가 요청될 때마다 엘라스틱서치에서 내부의 루씬을 이용해 계산을 수행
  • 일반적으로 전문 검색에 많이 사용
  • 캐싱되지 않고 디스크 연산을 수행하기 때문에 상대적으로 느림
    POST movie_search/_search
    {
    "query": {
      "match": {
        "movieNm": "기묘한 가족"
      }
    }
    }

    필터 컨텍스트

  • 쿼리의 조건과 문서가 일치하는지(YES/NO)를 구분
  • 별도의 스코어를 계산하지 않고 단순 매칭 여부를 검사
  • 자주 사용되는 필터의 결과는 엘라스틱서치가 내부적으로 캐싱
  • 기본적으로 메모리 연산을 수행하기 때문에 상대적으로 빠름
    POST movie_search/_search
    {
    "query": {
      "bool": {
        "must": [
          {
            "match_all": {}
          }
        ],
        "filter": {
          "term": {
            "repGenreNm": "다큐멘터리"
          }
        }
      }
    }
    }

Query DSL의 주요 파라미터

Multi Index 검색

  • 기본적으로 모든 검색 요청은 Multi Index 및 Multi Type 검색이 가능
  • 검색 요청시 ","를 이용해 다수 인덱스 명을 입력
    POST movie_search,movie_auto/_search
    {
    "query": {
      "term": {
        "repGenreNm": "다큐멘터리"
      }
    }
    }
  • 또한 '*' 와일드 카드를 이용할 수도 있습니다.
    POST /log-2019-*/_search

쿼리 결과 페이징

  • from 과 size 파라미터를 이용합니다.
  • 엘라스틱서치는 관계형 데이터베이스와 다르게 페이징된 해당 문서만 선택적으로 가져오는 것이 아니라 모든 데이터를 읽어 된다.
  • 설정된 페이지를 제공하기 위해서는 전체를 읽어서 사이즈만큼 필터링 하는 구조이기 때문에 페이지 번호가 높아질수록 쿼리 비용도 덩달아 높아진다.
    PUT movie_search/_search // 1페이지
    {
      "from": 0,
        "size": 5,
      "query": {
          "term": {
                "repNationNm": "한국"
          }
        }
    }
    PUT movie_search/_search // 2페이지
    {
        "from": 5,
        "size": 5,
      "query": {
          "term": {
                "repNationNm": "한국"
          }
        }
    }

쿼리 결과 정렬

  • sort 파라미터
  • asc, desc가 존재
  • 엘라스틱서치가 계산에 유도한 스코어 값이 아니라 필드의 이름이나 가격 날짜 등을 기준으로 정렬할 때 사용
    POST movie_search/_search
    {
        "query": {
          "term": {
                "repNationNm": "한국"
          }
        },
        "sort": {
          "prdtYear": {
                "order": "asc"
          }
        }
    }

_source 필드 필터링

  • 항상 모든 필드를 볼 필요는 없으며, 특정 필드를 제거하거나 네트워크 사용량을 줄여 응답 속도를 빠르게 할때 사용

범위 검색

  • 숫자나 날짜 등 데이터가 지정한 값이 아닌 범위를 기준으로 질의해야 할 때 사용
  • lt(<), gt(>), lte(<=), gte(>=)
    POST movie_search/_search
    {
        "query": {
          "range": {
                "prdtYear": {
                    "gte": "2016",
                    "lte": "2017"
              }
          }
        }
    }

operator 설정

  • 기본적으로 term과 term 사이에는 or 연산이 적용된다
  • and를 사용할 경우 넣어주면 된다
    POST movie_search/_search
    {
        "query": {
          "match": {
                "movieNm": {
                    "query": "자전차왕 엄복동",
                    "operator": "and"
              }
          }
        }
    }

minimum_should_match 설정

  • or 연산을 수행할 경우 옵션
  • 검색 결과가 너무 많아 질 경우, term의 갯수가 몇개 이상 매칭될 때만 검색 결과로 나오게 설정
    POST movie_search/_search
    {
        "query": {
          "match": {
                "movieNm": {
                    "query": "자전차왕 엄복동",
                    "minimum_should_match": 2
              }
          }
        }
    }

fuzziness 설정

  • 유사한 값을 찾는 경우 사용한다.
  • 오차 범위가 n글자 이하인 검색 결과까지 포함해서 출력 된다.
  • Fly High를 예로 본다
    POST movie_search/_search
    {
        "query": {
          "match": {
                "movieNm": {
                    "query": "Fli High",
                    "fuzziness": 1
              }
          }
        }
    }

boost 설정

  • 필드에 가중치를 부여하여, 검색 결과를 좀 더 상위로 올리고 싶을 때 사용한다
    POST movie_search/_search
    {
        "query": {
          "multi_match": {
                "query": "Fly",
                "fields": ["movieNm3", "movieNmEn"]
          }
        }
    }

참조

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

사용자 yhmane

댓글을 달아 주세요

데이터 검색

엘라스틱서치는 인덱스에 저장된 문서를 검색할 수 있도록 다양한 검색 기능을 제공합니다. 문서(Document)는 색인시 설정한 분석기에 의해 분석 과정을 거쳐 토큰으로 분리되는데, 이러한 분석기는 색인 시점에 사용할 수도 있지만 검색 시점에 사용하는 것도 가능합니다. 검색의 대상이 되는 필드는 분석이 되는 Text 타입 유형이 될수 있고 분석 되지 않는 Keyword 유형이 될수도 있습니다.

검색 API

문장은 색인 시점에 텀으로 분해됩니다. 검색시에는 이 텀을 일치시켜야 검색이 가능합니다. 엘라스틱서치는 색인 시점에 Analyzer를 통해 분석된 텀을 Term, 출현빈도, 문서번호와 같이 역색인 구조로 만들어 내부에 저장합니다. 검색 시점에는 Keyword 타입과 같은 분석이 불가능한 데이터와 Text 타입과 같은 분석이 가능한 데이터를 구분해서 분석합니다. 이를 통해 검색 시점에도 Term을 얻을 수 있으며, 해당 Term으로 역색인 구조를 이용해 문서를 찾고 이를 통해 스코어를 계산해 결과로 제공합니다. 이러한 동작 방식을 이용하고 이에 맞춰 목적에 맞게 검색 쿼리를 사용해야 합니다.

  • 검색 질의 방식
    • URI 검색
    • Request Body 검색
    • Query DSL 검색

환경 세팅

  • OS
    • mac BigSur (여기선 local에서 했지만 개인적으로 비추, linux가 가능하다면 linux로!!)
  • java
    • openjdk11
      ➭ java -version
      openjdk version "11.0.10" 2021-01-19
      OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.10+9)
      OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.10+9, mixed mode)
  • elasticsearch
    • 6.4.3 release
      # ======================== Elasticsearch Configuration =========================
      #
      # NOTE: Elasticsearch comes with reasonable defaults for most settings.
      #       Before you set out to tweak and tune the configuration, make sure you
      #       understand what are you trying to accomplish and the consequences.
      #
      # The primary way of configuring a node is via this file. This template lists
      # the most important settings you may want to configure for a production cluster.
      #
      # Please consult the documentation for further information on configuration options:
      # https://www.elastic.co/guide/en/elasticsearch/reference/index.html
      #
      # ---------------------------------- Cluster -----------------------------------
      #
      # Use a descriptive name for your cluster:
      #
      cluster.name: javacafe-cluster
      #
      # ------------------------------------ Node ------------------------------------
      #
      # Use a descriptive name for the node:
      #
      node.name: javacafe-node1
      node.data: true
      node.master: true
      #
      # Add custom attributes to the node:
      #
      #node.attr.rack: r1
      #
      # ----------------------------------- Paths ------------------------------------
      #
      # Path to directory where to store the data (separate multiple locations by comma):
      #
      #path.data: /path/to/data
      #
      # Path to log files:
      #
      #path.logs: /path/to/logs
      #
      path.repo: ["/Users/hwang-yunho/Documents/es/elastic-book-snapshot-master/book_backup/search_example", "/Users/hwang-yunho/Documents/es/elastic-book-snapshot-master/book_backup/agg_example"]
      # ----------------------------------- Memory -----------------------------------
      #
      # Lock the memory on startup:
      #
      #bootstrap.memory_lock: true
      #
      # Make sure that the heap size is set to about half the memory available
      # on the system and that the owner of the process is allowed to use this
      # limit.
      #
      # Elasticsearch performs poorly when the system is swapping the memory.
      #
      # ---------------------------------- Network -----------------------------------
      #
      # Set the bind address to a specific IP (IPv4 or IPv6):
      #
      #network.host: 192.168.0.1
      #
      # Set a custom port for HTTP:
      #
      #http.port: 9200
      #
      # For more information, consult the network module documentation.
      #
      # --------------------------------- Discovery ----------------------------------
      #
      # Pass an initial list of hosts to perform discovery when new node is started:
      # The default list of hosts is ["127.0.0.1", "[::1]"]
      #
      #discovery.zen.ping.unicast.hosts: ["host1", "host2"]
      #
      # Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1):
      #
      #discovery.zen.minimum_master_nodes:
      #
      # For more information, consult the zen discovery module documentation.
      #
      # ---------------------------------- Gateway -----------------------------------
      #
      # Block initial recovery after a full cluster restart until N nodes are started:
      #
      #gateway.recover_after_nodes: 3
      #
      # For more information, consult the gateway module documentation.
      #
      # ---------------------------------- Various -----------------------------------
      #
      # Require explicit names when deleting indices:
      #
      #action.destructive_requires_name: true
  • cerebro
  • search snapshot

Snapshot 설정

  • 검색 snapshot 생성

    curl -XPUT 'http://localhost:9200/_snapshot/javacafe' -H 'Content-Type:application/json' -d ' {
      "type": "fs",
      "settings": {
          "location": "/Users/hwang-yunho/Documents/es/elastic-book-snapshot-master/book_backup/search_example",
          "compress": true
      }
    }
    결과
    {"acknowledged":true}
  • 생성된 논리적인 javacafe 스냅숏 확인

    curl -XGET 'http://localhost:9200/_snapshot/javacafe/_all'
    결과
    {
    	"snapshots": [
        	{
    		    "snapshot": "movie-search",
                "uuid": "Kz5k4fusS7KBZy55wLeZ0Q",
    		    "version_id": 6040399,
    		    "version": "6.4.3",
    		    "indices": [
                	"movie_search"
    		    ],
        		"include_global_state": false,
    		    "state": "SUCCESS",
    		    "start_time": "2019-03-23T16:01:04.910Z",
    		    "start_time_in_millis": 1553356864910,
    		    "end_time": "2019-03-23T16:01:05.342Z",
    		    "end_time_in_millis": 1553356865342,
    		    "duration_in_millis": 432,
    		    "failures": [],
    		    "shards": {
                	"total": 5,
                    "failed": 0,
                    "successful": 5
    			}
    		}
    	]
    }
  • movie-search 스냅숏 복구

    curl -XPOST 'http://localhost:9200/_snapshot/javacafe/movie-search/_restore'
    결과
    {"accepted":true}


movie_search에 63,069 문서가 생성된 것을 확인할 수 있습니다.

일단 세팅까지만 .. 뒤에 이어서 마무리 하도록 하겠습니다.


참조

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

사용자 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

댓글을 달아 주세요