이번에 다루어볼 포스팅은 도커로 ES를 띄우기전에 뭔가 커스텀한 이미지를 만들어서 올릴수없을까 하는 생각에 간단히 ES 기본 이미지에 한글 형태소 분석기(Nori) 플러그인이 설치가된 ES docker image를 커스텀하게 만들어보았다.

 

#Dockerfile

FROM docker.elastic.co/elasticsearch/elasticsearch:7.6.2

ENV ES_VOLUME=/usr/share/elasticsearch/data
ENV ES_BIN=/usr/share/elasticsearch/bin

RUN mkdir $ES_VOLUME/dictionary
RUN $ES_BIN/elasticsearch-plugin install --batch analysis-nori

 

간단히 설명하면, 베이스 이미지로 공식 elasticsearch image를 사용하였고, 나중에 사용자 사전이 위치할 도커 볼륨 디렉토리를 잡아주었고, 거기에 사전이 담기는 디렉토리를 생성했다. 그리고 마지막으로 한글 형태소분석기 플러그인을 설치하는 쉘을 실행시켰다. 이렇게 빌드된 elasticsearch image는 한글 형태소분석기가 이미 설치가된 elasticsearch image가 된다. 그래서 굳이 plugin directory를 마운트해서 직접 플러그인을 설치할 필요가 없다. 물론 추후에 필요에 의해서 설치해야한다면 어쩔수 없지만 말이다.

 

여기에 추후 더 커스텀할 내용을 넣으면 될듯하다.

posted by 여성게
:

오늘 다루어볼 내용은 Elasticsearch를 이용한 한글 자동완성 구현이다. 실습을 위한 Elasticsearch는 도커로 세팅을 진행할 것이다. 한글 형태소 분석기가 필요하기 때문에 Elasticsearch docker image를 조금 커스터마이징하여 한글 형태소 분석기(nori)가 설치된 ES 도커 이미지로 도커 컨테이너를 실행시킬 것이다.

 

ES 도커 이미지는 아래 링크를 참조해서 빌드해준다.

 

Elasticsearch - Elasticsearch custom docker image 빌드(엘라스틱서치 커스텀 도커 이미지 생성)

이번에 다루어볼 포스팅은 도커로 ES를 띄우기전에 뭔가 커스텀한 이미지를 만들어서 올릴수없을까 하는 생각에 간단히 ES 기본 이미지에 한글 형태소 분석기(Nori) 플러그인이 설치가된 ES docker image를 커스..

coding-start.tistory.com

docker run
#동의어 사전 사용을 위해 volume mount and data volume mount
> docker run --name elasticsearch -d -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -v /home/deploy/elasticsearch_v/dictionary:/usr/share/elasticsearch/data/dictionary \
  -v /home/deploy/elasticsearch_v/data_v:/usr/share/elasticsearch/data \
  es_image_docker_hub

 

Elasticsearch index setting

토크나이저는 한글 형태소분석기인 노리(nori) 형태소 분석기를 이용했고, 자동완성 구현을 위해 edge_ngram filter를 이용하였다.

 

curl --location --request PUT 'localhost:9200/auto_complete' \
--header 'Content-Type: application/json' \
--data-raw '{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "korean_analyzer": {
            "type": "custom",
            "tokenizer": "korean_tokenizer",
            "filter": [
              "lowercase",
              "korean_posfilter",
              "synonym_filter",
              "edge_ngram_filter_front",
              "edge_ngram_filter_back",
              "trim"
            ]
          }
        },
        "tokenizer": {
          "korean_tokenizer" : {
            "type" : "nori_tokenizer",
            "decompound_mode" : "mixed",
            "user_dictionary":"/usr/share/elasticsearch/data/dictionary/user_dict.txt"
          }
        },
        "filter": {
          "edge_ngram_filter_front": {
            "type": "edgeNGram",
            "min_gram": "1",
            "max_gram": "10",
            "side": "front"
          },
          "edge_ngram_filter_back": {
            "type": "edgeNGram",
            "min_gram": "1",
            "max_gram": "10",
            "side": "back"
          },
          "synonym_filter": {
            "type": "synonym",
            "lenient": true,
            "synonyms_path":"/usr/share/elasticsearch/data/dictionary/synonym.txt"
          },
          "korean_posfilter":{
                "type":"nori_part_of_speech",
                "stoptags":[
                    "E",
                    "IC",
                    "J",
                    "MAG",
                    "MM",
                    "NA",
                    "NR",
                    "SC",
                    "SE",
                    "SF",
                    "SP",
                    "SSC",
                    "SSO",
                    "SY",
                    "UNA",
                    "VA",
                    "VCN",
                    "VCP",
                    "XPN",
                    "XR",
                    "XSA",
                    "XSN",
                    "XSV"
                ]
            }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "auto_complete": {
        "type": "text",
        "analyzer": "korean_analyzer"
      }
    }
  }
}'

 

만약 자동완성을 위한 필드에 색인할때, 생략할 품사들을 지정하기 위해서는 nori_part_of_speech 필터의 stoptags에 넣어주면 된다. 위 설정은 명사류 혹은 동사류만 색인되도록 품사지정을 하였다. 품사(nori_part_of_speech)에 대한 자세한 사항은 아래 링크를 참조하자.

 

 

Elasticsearch - 4.한글 형태소분석기(Nori Analyzer)

엘라스틱서치 혹은 솔라와 같은 검색엔진들은 모두 한글에는 성능을 발휘하기 쉽지 않은 검색엔진이다. 그 이유는 한글은 다른 언어와 달리 조사나 어미의 접미사가 명사,동사 등과 결합하기 ��

coding-start.tistory.com

 

색인 및 한글 자동완성 쿼리
#index document
curl --location --request POST 'http://localhost:9200/auto_complete/_doc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "auto_complete" : "피자 주문할게요"
}'

 

위 요청으로 문서를 색인하고 아래 쿼리를 날려보자.

 

#match query
curl --location --request POST 'http://localhost:9200/auto_complete/_search' \
--header 'Content-Type: application/json' \
--data-raw '{
    "query" : {
        "match" : {
            "auto_complete" : {
                "query" : "피"
            }
        }
    }
}'

 

마지막으로 동의어를 넣기 위해서는 아래와 같이 index를 close 했다가 open해야한다. 아래 요청을 참조하자.

 

#index close
curl --location --request POST 'http://localhost:9200/auto_complete/_close' \
--data-raw ''
 
 
#index open
curl --location --request POST 'http://localhost:9200/auto_complete/_open' \
--data-raw ''

 

여기까지 아주 간단하게 한글 자동 완성을 구현해보았다. 사실 더 최적화할 것도 많고, 자소 분리등을 넣어서 자음만 넣어도 자동완성을 구현할 수도 있지만, 이번 포스팅에서는 그냥 한글 자동완성이 어떤 식으로 구현되는지 간단하게 다루어보았다.

posted by 여성게
:

 

파이프라인 집계(Pipeline Aggregations)는 다른 집계와 달리 쿼리 조건에 부합하는 문서에 대해 집계를 수행하는 것이 아니라, 다른 집계로 생성된 버킷을 참조해서 집계를 수행한다. 집계 또는 중첩된 집계를 통해 생성된 버킷을 사용해 추가적으로 계산을 수행한다고 보면 된다. 파이프라인 집계에는 부모(Parent), 형제(Sibling)라는 두 가지 유형이 있다.

 

파이프라인 집계를 수행할 때는 buckets_path 파라미터를 사용해 참조할 집계의 경로를 지정함으로써 체인 형식으로 집계 간의 연산이 이뤄진다. 파이프라인 집계는 모든 집계가 완료된 후에 생성된 버킷을 사용하기 때문에 하위 집계를 가질 수는 없지만 다른 파이프라인 집계와는 buckets_path를 통해 참조하도록 지정할 수 있다.

 

-형제 집계(Sibling)

형제 집계는 동일 선상의 위치에서 수행되는 새 집계를 의미한다. 즉, 형제 집계를 통해 수행되는 집계는 기존 버킷에 추가되는 형태가 아니라 동일 선상의 위치에서 새 집계가 생성되는 파이프라인 집계다. 형제 집계는 다음과 같은 집계들이 있다.

 

평균 버킷 집계(Avg Bucket Aggregation)
최대 버킷 집계(Max Bucket Aggregation)
최소 버킷 집계(Min Bucket Aggregation)
합계 버킷 집계(Sum Bucket Aggregation)
통계 버킷 집계(Stats Bucket Aggregation)
확장 통계 버킷 집계(Extended Stats Bucket Aggregation)
백분위수 버킷 집계(Percentiles Bucket Aggregation)
이동 평균 집계(Moving Average Aggregation)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{ //중첩 집계 
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                }
            }
        },
        "max_bytes":{ //파이프라인 집계
            "max_bucket":{
                "buckets_path":"histo>bytes_sum" //버킷 참조
            }
        }
    }
}
 
->result
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    }
                }
            ]
        },
        "max_bytes": {
            "value": 8.78559106E8,
            "keys": [
                "2015-05-20T00:00:00.000Z"
            ]
        }
    }
}
cs

 

위는 간단하게 2개의 집계를 중첩하였고, 형제 레벨로 histo 집계 밑의 bytes_sum의 버킷을 참조하여 최대값을 구하는 파이프라인 집계를 작성한 것이다. 결과값으로는 중첩된 집계결과와 마지막에 파이프라인의 집계가 나온다. 현재는 bytes_sum이 단일 메트릭 집계이기 때문에 집계 이름으로만 참조하고 있지만 stats 같은 다중 메트릭 집계일 경우 메트릭명까지 참조해줘야 한다. histo>bytes_sum.avg

 

Additional Sibling Aggregations

파이프라인 집계명 집계 쿼리
최대 버킷 집계 - 최대 값으로 버킷을 식별하고 버킷의 값과 키를 출력하는 형제 파이프 라인 집계입니다. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계 여야합니다.

{

  "max_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

최소 버킷 집계 - 최소값으로 버킷을 식별하고 버킷의 값과 키를 모두 출력하는 형제 파이프 라인 집계이다. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계 여야한다.

{

  "min_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

평균 버킷 집계 - 지정된 메트릭의 평균 값을 계산하는 파이프 라인 집계이다. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계여야한다.

{

  "avg_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

통계 버킷 집계 - 모든 버킷에 대한 다양한 통계를 계산하는 형제 파이프 라인 집계이다. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계 여야한다.

{

  "stats_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

확장 통계 버킷 집계 -

모든 버킷에 대한 다양한 통계를 계산하는 형제 파이프 라인 집계이다. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계 여야한다.

이 집계는 집계와 비교하여 몇 가지 통계 (제곱합, 표준 편차 등)를 제공한다.

{

  "extended_stats_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

백분위수 버킷 집계 - 모든 버킷에서 백분위 수를 계산하는 형제 파이프 라인 집계이다.. 지정된 메트릭은 숫자 여야하고 형제 집계는 다중 버킷 집계 여야한다.

{

  "percentiles_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

이동 평균 버킷 집계 - 이동 평균은 순차 데이터를 부드럽게하는 간단한 방법이다. 이동 평균은 일반적으로 주가 또는 서버 메트릭과 같은 시간 기반 데이터에 적용이다. 평활화는 고주파수 변동 또는 랜덤 노이즈를 제거하는 데 사용될 수 있으므로 계절 성과 같이 저주파수 추세를보다 쉽게 ​​시각화 할 수 있다.

다양한 옵션이 있으므로 자세한 사용법은 레퍼런스를 참고하자.

>https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movavg-aggregation.html

{

  "moving_avg_bucket":{

    "bucket_path":"histo>bytes_sum"

  }

}

 

이동 평균 버킷 집계는 부모에 histogram 혹은 date_histogram 집계가 있어야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "moving_avg_agg":{
                    "moving_avg":{
                        "buckets_path":"bytes_sum"
                    }
                }
            }
        }
    }
}
 
->result
{
    "took": 7,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    },
                    "moving_avg_agg": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    },
                    "moving_avg_agg": {
                        "value": 6.0144803E8
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    },
                    "moving_avg_agg": {
                        "value": 6.229077996666666E8
                    }
                }
            ]
        }
    }
}
cs

 

-부모 집계(Parent)

부모 집계는 집계를 통해 생성된 버킷을 사용해 계산을 수행하고, 그 결과를 기존 집계 결과에 반영한다. 집계의 종류는 아래와 같다.

 

파생 집계(Derivative Aggregation)
누적 집계(Cumulative Sum Aggregation)
버킷 스크립트 집계(Bucket Script Aggregation)
버킷 셀렉터 집계(Bucket Selector Aggregation)
시계열 차분 집계(Serial Differencing Aggregation)

 

아파치 웹 로그 등을 통해 수집된 데이터가 시간이 지남에 따라 변화하는 값의 변경폭 추이를 확인하고 싶은 경우 파생 집계를 활용할 수 있다. 파생 집계는 부모 히스토그램 또는 날짜 히스토그램 집계에서 지정된 메트릭의 파생값을 계산하는 상위 파이프라인 집계다. 이는 부모 히스토그램 집계 측정 항목에 대해 작동하고, 히스토그램 집계에 의한 각 버킷의 집계 값을 비교해서 차이를 계산한다. 반드시 지정된 메트릭은 숫자여야 하고, 상위에 해당하는 집계의 min_doc_count가 0보다 큰 값으로 설정되는 경우 일부 간격이 결과에서 생략될 수 있기에 min_doc_count 값을 0으로 설정해야 한다.

 

파생 집계의 경우에는 이처럼 선행되는 데이터가 존재하지 않으면 집계를 수행할 수 없는데, 실제 데이터를 다루다 보면 종종 노이즈가 포함되기도 하고, 필요한 필드에 값이 존재하지 않을 수 있다. 이러한 부분을 갭(Gap)이라고 할 수 있는데, 쉽게 말해 데이터가 존재하지 않는 부분을 의미한다.

 

갭(gap)이 발생되는 이유는 아래와 같다.

 

  1. 어느 하나의 버킷 안으로 포함되는 문서들에 요청된 필드가 포함되지 않은 경우
  2. 하나 이상의 버킷에 대한 쿼리와 일치하는 문서가 존재하지 않는 경우
  3. 다른 종속된 버킷에 값이 누락되어 계산된 메트릭이 값을 생성할 수 없는 경우

이러한 경우에는 파이프라인 집계에 원하는 동작을 알리는 메커니즘이 필요하다. 이 역할을하는 것이 갭 정책(gap_policy)이다. 모든 파이프라인 집계에서는 gap_policy 파라미터를 허용한다.

 

<갭정책>

skip 누락된 데이터를 버킷이 존재하지 않는 것으로 간주한다. 버킷을 건너뛰고 다음으로 사용 가능한 값을 사용해 계산을 계속해서 수행한다.
insert_zeros 누락된 값을 0으로 대체하며 파이프라인 집계 계산은 정상적으로 진행된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "sum_deriv":{
                    "derivative":{
                        "buckets_path":"bytes_sum"
                    }
                }
            }
        }
    }
}
 
->result
{
    "took": 5,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    },
                    "sum_deriv": {
                        "value": 3.74376256E8
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    },
                    "sum_deriv": {
                        "value": -1.22808819E8
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    },
                    "sum_deriv": {
                        "value": 2.12731767E8
                    }
                }
            ]
        }
    }
}
cs

 

결과를 보면 파생 집계는 각 버킷 간의 차이를 값으로 보여준다. 첫번째 버킷은 이전 데이터가 존재하지 않으므로 파생 집계 결과가 포함되지 않는다.

 

Addtional Parent Aggregations

파이프라인 집계명 쿼리
파생 집계 - 상위 히스토그램 (또는 date_histogram) 집계에서 지정된 메트릭의 미분을 계산하는 상위 파이프 라인 집계이다. 지정된 메트릭은 숫자 여야하며 둘러싸는 막대 그래프는 ( 집계의 기본값 ) 으로 min_doc_count이 0으로 설정되어 있어야한다.

{

  "derivative":{

    "buckets_path":"bytes_sum"

  }

}

누적 집계 - 위 히스토그램 (또는 date_histogram) 집계에서 지정된 지표의 누적 합계를 계산하는 상위 파이프 라인 집계입니다. 지정된 메트릭은 숫자 여야하며 둘러싸는 막대 그래프는 ( 집계의 기본값 ) 으로 min_doc_count이 0으로 설정되어 있어야합니다 .

{

  "cumulative_sum":{

    "buckets_path":"bytes_sum"

  }

}

버킷 스크립트 집계 - 부모 다중 버킷 집계에서 지정된 메트릭에 대해 버킷 당 계산을 수행 할 수있는 스크립트를 실행하는 부모 파이프 라인 집계입니다. 지정된 메트릭은 숫자 여야하며 스크립트는 숫자 값을 반환해야합니다.

{

  "bucket_script":{

    "buckets_path":{

      "my_var1":"bytes_sum",

      "my_var2":"total_count"

    },

    "script":"params.my_var1/params.my_var2

  }

}

버킷 셀렉터 집계 - 현재 버킷을 상위 멀티 버킷 집계에 유지할지 여부를 결정하는 스크립트를 실행하는 상위 파이프 라인 집계입니다. 지정된 메트릭은 숫자 여야하며 스크립트는 부울 값을 반환해야합니다. 스크립트 언어 인 경우 expression숫자 반환 값이 허용됩니다. 이 경우 0.0은 그대로 평가되고 false 다른 모든 값은 true로 평가됩니다.

{

  "bucket_selector":{

    "buckets_path":{

      "my_var1":"bytes_sum",

      "my_var2":"total_count"

    },

    "script":"params.my_var1 > params.my_var2

  }

}

시계열 차분 집계 - 시계열의 값을 다른 시차 또는 기간에 차감하는 기술입니다. 예를 들어, 데이터 포인트 f (x) = f (x t )-f (x t-n ), 여기서 n은 사용되는 기간입니다.

{

  "serial_diff":{

    "buckets_path":"bytes_sum",

    "lag":"7"

  }

}

 

자세한 설명은 공식 레퍼런스를 참고하시길 바랍니다.

 

누적 집계

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "sum_deriv":{
                      "cumulative_sum":{
                        "buckets_path":"bytes_sum"
                      }
                }
            }
        }
    }
}
 
->result
{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    },
                    "sum_deriv": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    },
                    "sum_deriv": {
                        "value": 1.20289606E9
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    },
                    "sum_deriv": {
                        "value": 1.868723399E9
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    },
                    "sum_deriv": {
                        "value": 2.747282505E9
                    }
                }
            ]
        }
    }
}
cs

 

집계의 값을 계속 해서 누적하는 집계이다.

 

버킷 스크립트 집계

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "sum_deriv":{
                      "bucket_script":{
                        "buckets_path":{
                          "my_var1":"bytes_sum",
                          "my_var2":"bytes_sum"
                        },
                        "script":"params.my_var1/params.my_var2"
                      }
                }
            }
        }
    }
}
 
->result
{
    "took": 106,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    },
                    "sum_deriv": {
                        "value": 1.0
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    },
                    "sum_deriv": {
                        "value": 1.0
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    },
                    "sum_deriv": {
                        "value": 1.0
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    },
                    "sum_deriv": {
                        "value": 1.0
                    }
                }
            ]
        }
    }
}
cs

 

스크립트를 활용해 버킷의 결과 값을 스크립트로 조작가능하다.

 

버킷 셀렉터

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "sum_deriv":{
                      "bucket_selector":{
                        "buckets_path":{
                          "my_var1":"bytes_sum",
                          "my_var2":"bytes_sum"
                        },
                        "script":"params.my_var1 < params.my_var2"
                      }
                }
            }
        }
    }
}
 
->result
{
    "took": 7,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": []
        }
    }
}
cs

 

불린을 결과값으로 하는 script를 작성해 결과에 노출시킬 버킷을 선택한다.

 

시계열 차분 집계

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{
    "aggs":{
        "histo":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day"
            },
            "aggs":{
                "bytes_sum":{
                    "sum":{
                        "field":"bytes"
                    }
                },
                "thirtieth_difference":{
                      "serial_diff":{
                        "buckets_path":"bytes_sum",
                        "lag":2
                      }
                }
            }
        }
    }
}
 
->result
{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "histo": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17T00:00:00.000Z",
                    "key": 1431820800000,
                    "doc_count": 1632,
                    "bytes_sum": {
                        "value": 4.14259902E8
                    }
                },
                {
                    "key_as_string": "2015-05-18T00:00:00.000Z",
                    "key": 1431907200000,
                    "doc_count": 2893,
                    "bytes_sum": {
                        "value": 7.88636158E8
                    }
                },
                {
                    "key_as_string": "2015-05-19T00:00:00.000Z",
                    "key": 1431993600000,
                    "doc_count": 2896,
                    "bytes_sum": {
                        "value": 6.65827339E8
                    },
                    "thirtieth_difference": {
                        "value": 2.51567437E8
                    }
                },
                {
                    "key_as_string": "2015-05-20T00:00:00.000Z",
                    "key": 1432080000000,
                    "doc_count": 2578,
                    "bytes_sum": {
                        "value": 8.78559106E8
                    },
                    "thirtieth_difference": {
                        "value": 8.9922948E7
                    }
                }
            ]
        }
    }
}
cs

 

lag 값을 2로 주어 2번째 전 버킷과 현재 버킷의 차분을 계산하여 결과값에 포함시킨다.

 

여기까지 간단히 파이프라인 집계에 대해 다루어보았다. 사실 집계의 모든 것을 다 다루지는 못했다. 부족한 것은 공식레퍼런스와 서적을 더 참고해야겠다.

 

이번 포스팅말고 메트릭, 버킷 집계는 아래 링크를 참조하자.

2019/09/19 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,메트릭(Metric Aggregations) 집계) -1

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,메트릭(Metric Aggregations) 집계) -1

이번에 다루어볼 내용은 엘라스틱서치 Aggregation API이다. 해당 기능은 SQL과 비교하면 Group by의 기능과 아주 유사하다. 즉, 문서 데이터를 그룹화해서 각종 통계 지표 만들어 낼 수 있다. 엘라스틱서치의 집..

coding-start.tistory.com

2019/09/20 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,버킷(Bucket Aggregations) 집계) -2

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,버킷(Bucket Aggregations) 집계) -2

이번 포스팅은 엘라스틱서치 Aggregation(집계) API 두번째 글이다. 이번 글에서는 집계중 버킷집계(Bucket)에 대해 알아볼 것이다. 우선 버킷 집계는 메트릭 집계와는 다르게 메트릭을 계산하지 않고 버킷을 생..

coding-start.tistory.com

 

posted by 여성게
:

이번 포스팅은 엘라스틱서치 Aggregation(집계) API 두번째 글이다. 이번 글에서는 집계중 버킷집계(Bucket)에 대해 알아볼 것이다. 우선 버킷 집계는 메트릭 집계와는 다르게 메트릭을 계산하지 않고 버킷을 생성한다. 생성되는 버킷은 쿼리와 함께 수행되어 쿼리 결과에 따른 컨텍스트 내에서 집계가 이뤄진다. 이렇게 집계된 버킷은 또 다시 하위에서 집계를 한번 더 수행해서 집계된 결과에 대해 중첩된 집계 수행이 가능하다.

 

버킷이 생성되는 것은 집계 결과 집합을 메모리에 저장한다는 것이기 때문에 너무 많은 중첩 집계는 메모리 사용량을 점점 높히기에 성능에 악영향을 줄 수 있다. 이러한 문제때문에 엘라스틱서치는 설정으로 최대 버킷수를 조정할 수 있다. 

 

> search.max_buckets

 

버킷의 크기를 -1 혹은 10000 이상의 값을 지정할 경우 엘라스틱서치에서 경고메시지를 보낸다. 이 말은 여러가지 이유로 안정적인 집계 분석을 위해 버킷의 크기, 집계의 중첩양 등을 충분히 고려한 후에 집계 수행을 해야한다.

 

-범위 집계(Range Aggregations)

범위 집계는 사용자가 지정한 범위 내에서 집계를 수행하는 다중 버킷 집계이다. 집계가 수행되면 쿼리의 결과가 범위에 해당하는 지 체크하고, 범위에 해당되는 문서들에 대해서만 집계를 수행한다. from과 to 속성을 지정하고, to에 지정한 값을 결과에서 제외된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
    "aggs":{
        "bytes_range":{
            "range":{
                "field":"bytes",
                "ranges":[{"from":1000,"to":2000}]
            }    
        }
    }
}
 
->result
{
    "took": 51,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "bytes_range": {
            "buckets": [
                {
                    "key": "1000.0-2000.0",
                    "from": 1000.0,
                    "to": 2000.0,
                    "doc_count": 754
                }
            ]
        }
    }
}
cs

 

결과값에 대해 간단히 설명하면 "key"는 집계할 범위를 뜻하고, from은 시작,to는 끝,doc_count는 범위 내의 문서수를 의미한다. 또한 집계 쿼리에서 "ranges" 필드가 배열인 것으로 보아 여러개의 범위 지정이 가능한 것을 알 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    "aggs":{
        "bytes_range":{
            "range":{
                "field":"bytes",
                "ranges":[
                    {"from":1000,"to":2000},
                    {"from":2000,"to":4000}
                ]
            }    
        }
    }
}
 
->result
{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "bytes_range": {
            "buckets": [
                {
                    "key": "1000.0-2000.0",
                    "from": 1000.0,
                    "to": 2000.0,
                    "doc_count": 754
                },
                {
                    "key": "2000.0-4000.0",
                    "from": 2000.0,
                    "to": 4000.0,
                    "doc_count": 1004
                }
            ]
        }
    }
}
cs

 

그리고 키값을 더 의미있는 값으로 지정해 줄 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    "aggs":{
        "bytes_range":{
            "range":{
                "field":"bytes",
                "ranges":[
                    {"key":"small","from":1000,"to":2000},
                    {"key":"medium","from":2000,"to":4000}
                ]
            }    
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "bytes_range": {
            "buckets": [
                {
                    "key": "small",
                    "from": 1000.0,
                    "to": 2000.0,
                    "doc_count": 754
                },
                {
                    "key": "medium",
                    "from": 2000.0,
                    "to": 4000.0,
                    "doc_count": 1004
                }
            ]
        }
    }
}
cs

 

-날짜 범위 집계

날짜 값을 범위로 집계를 수행한다. 날짜 포맷은 엘라스틱서치가 내부적으로 이해할 수 있는 날짜 타입이 와야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
    "aggs":{
        "request_count_date":{
            "date_range":{
                "field":"timestamp",
                "ranges":[
                    {"from":"2015-05-04T05:16:00.000Z","to":"2015-05-18T05:16:00.000Z"}
                ]
            }    
        }
    }
}
 
->result
{
    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "request_count_date": {
            "buckets": [
                {
                    "key": "2015-05-04T05:16:00.000Z-2015-05-18T05:16:00.000Z",
                    "from": 1.43071656E12, //시작날짜의 밀리초값
                    "from_as_string": "2015-05-04T05:16:00.000Z",
                    "to": 1.43192616E12, //끝날짜의 밀리초값
                    "to_as_string": "2015-05-18T05:16:00.000Z",
                    "doc_count": 2345 //날짜 범위에 해당되는 문서수
                }
            ]
        }
    }
}
cs

 

-히스토그램 집계(Histogram)

지정한 범위 간격으로 집계를 낸다. 만약 10000으로 지정하였다면, 0~10000(미포함), 10000~20000 의 간격으로 집계를 낸다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
    "aggs":{
        "bytes_histogram":{
            "histogram":{
                "field":"bytes", //집계필드
                "interval":10000, //집계 간격
                "min_doc_count":1 //최소 1개 이상되어야 결과에 포함
            }    
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "bytes_histogram": {
            "buckets": [
                {
                    "key": 0.0,
                    "doc_count": 4196
                },
                {
                    "key": 10000.0,
                    "doc_count": 1930
                },
                {
                    "key": 20000.0,
                    "doc_count": 539
                },
                
                ...             
   
                
                {
                    "key": 5.43E7,
                    "doc_count": 24
                },
                {
                    "key": 6.525E7,
                    "doc_count": 2
                },
                {
                    "key": 6.919E7,
                    "doc_count": 2
                }
            ]
        }
    }
}
cs

 

대시보드에서 시각화할때도 아주 좋은 역할을 할듯하다.

 

-날짜 히스토그램 집계

날짜 히스토그램 집계는 분, 시간, 월, 연도를 구간으로 집계를 수행할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
    "aggs":{
        "daily_request_count":{
            "date_histogram":{
                "field":"timestamp", //집계 필드
                "interval":"day", //집계 간격
                "format":"yyyy-MM-dd" //출력되는 날짜 포맷 변경
            }    
        }
    }
}
 
->result
{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "daily_request_count": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17",
                    "key": 1431820800000,
                    "doc_count": 1632
                },
                {
                    "key_as_string": "2015-05-18",
                    "key": 1431907200000,
                    "doc_count": 2893
                },
                {
                    "key_as_string": "2015-05-19",
                    "key": 1431993600000,
                    "doc_count": 2896
                },
                {
                    "key_as_string": "2015-05-20",
                    "key": 1432080000000,
                    "doc_count": 2578
                }
            ]
        }
    }
}
cs

 

key_as_string은 집계한 기준 날짜인데, UTC가 기본이며 "yyyy-MM-dd'T'HH:mm: ss.SSS 형식을 사용한다. 하지만 "format" 필드로 형식 포맷 변경이 가능하다. key는 집계 기준 날짜에 대한 밀리초이다.

 

구간 지정을 위해서 interval 속성을 사용하는데, 여기에 year, quarter, month, week, day, hour, minute, second 표현식을 사용할 수 있고, 더 세밀한 설정을 위해 30m(30분 간격), 1.5h(1시간 30분 간격) 같은 값도 사용가능하다. 

 

지금까지 사용한 예제에서 날짜는 모두 UTC 기준으로 기록됬다. 우리나라 사용자가 사용하기 위해서는 9시간을 더해서 계산해야 현재 시간이 되기 때문에 번거로울 수 있다. 하지만 엘라스틱서치는 타임존을 지원하기 때문에 한국 시간으로 변환된 결과를 받을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
{
    "aggs":{
        "daily_request_count":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day",
                "format":"yyyy-MM-dd-HH:mm:ss",
                "time_zone":"+09:00"
            }    
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "daily_request_count": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17-00:00:00",
                    "key": 1431788400000,
                    "doc_count": 538
                },
                {
                    "key_as_string": "2015-05-18-00:00:00",
                    "key": 1431874800000,
                    "doc_count": 2898
                },
                {
                    "key_as_string": "2015-05-19-00:00:00",
                    "key": 1431961200000,
                    "doc_count": 2902
                },
                {
                    "key_as_string": "2015-05-20-00:00:00",
                    "key": 1432047600000,
                    "doc_count": 2862
                },
                {
                    "key_as_string": "2015-05-21-00:00:00",
                    "key": 1432134000000,
                    "doc_count": 799
                }
            ]
        }
    }
}
cs

 

타임존과는 다르게 offset을 사용해 집계 기준이 되는 날짜 값의 조정이 가능하다. 위에서 데일리로 집계했을 때, 00시 기준이었는데, 3시를 기준으로 하고 싶다면 아래와 같이 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
    "aggs":{
        "daily_request_count":{
            "date_histogram":{
                "field":"timestamp",
                "interval":"day",
                "format":"yyyy-MM-dd-HH:mm:ss",
                "offset":"+3h"
            }    
        }
    }
}
 
->result
{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "daily_request_count": {
            "buckets": [
                {
                    "key_as_string": "2015-05-17-03:00:00",
                    "key": 1431831600000,
                    "doc_count": 1991
                },
                {
                    "key_as_string": "2015-05-18-03:00:00",
                    "key": 1431918000000,
                    "doc_count": 2898
                },
                {
                    "key_as_string": "2015-05-19-03:00:00",
                    "key": 1432004400000,
                    "doc_count": 2895
                },
                {
                    "key_as_string": "2015-05-20-03:00:00",
                    "key": 1432090800000,
                    "doc_count": 2215
                }
            ]
        }
    }
}
cs

 

-텀즈 집계(terms)

텀즈 집계는 버킷이 동적으로 생성되는 다중 버킷 집계이다. 집계 시 지정한 필드에 대해 빈도수가 높은 텀의 순위로 결과가 반환된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
{
    "aggs":{
        "request_count_country":{
            "terms":{
                "field":"geoip.country_name.keyword"
            }    
        }
    }
}
 
->result
{
    "took": 8,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0.0,
        "hits": []
    },
    "aggregations": {
        "request_count_country": {
            "doc_count_error_upper_bound": 48,
            "sum_other_doc_count": 2334,
            "buckets": [
                {
                    "key": "United States",
                    "doc_count": 3974
                },
                {
                    "key": "France",
                    "doc_count": 855
                },
                {
                    "key": "Germany",
                    "doc_count": 510
                },
                {
                    "key": "Sweden",
                    "doc_count": 440
                },
                {
                    "key": "India",
                    "doc_count": 428
                },
                {
                    "key": "China",
                    "doc_count": 416
                },
                {
                    "key": "United Kingdom",
                    "doc_count": 276
                },
                {
                    "key": "Spain",
                    "doc_count": 227
                },
                {
                    "key": "Canada",
                    "doc_count": 224
                },
                {
                    "key": "Russia",
                    "doc_count": 214
                }
            ]
        }
    }
}
cs

 

집계 필드는 "geoip.country_name" 필드인데, 해당 필드는 text와 keyword 타입 두개를 가지는 필드이며, 집계 필드로 "*.keyword"로 지정하였다. 이유는 text 데이터 타입의 경우 형태소 분석이 들어가기에 집계할때는 형태소 분석이 없는 keyword 데이터 타입을 사용해야만 한다. 물론 text 타입이 안되는 건 아니지만, 성능은.. 최악이 될것이다.

 

결과 값에 대해 설명하자면 "doc_count_error_upper_bound"는 문서 수에 대한 오류 상한선이다. 오류 상한선이 있는 이유는 각 샤드별로 계산되는 집계의 성능을 고려해 근사치를 계산하기에 문서 수가 정확하지 않아 최대 오류 상한선을 보여준다. "sum_other_doc_count"는 결과에 포함되지 않은 모든 문서수를 뜻한다.(size를 늘려 결과에 더 많은 집계 데이터를 포함시키면 된다. terms 안의 field와 같은 레벨로 size 옵션을 주면 된다.) key는 집계 필드 값이고, doc_count는 같은 필드 값의 문서수이다.

 

여기서 "doc_count_error_upper_bound" 값에 대해 조금 더 자세히 다루어보면, 내부 집계 처리 플로우는 각 샤드에서 집계를 한후에 모든 결과를 병합해서 집계 결과를 최종으로 반환한다. 하지만 아래와 같은 상황이 있다고 해보자.

  샤드 A 샤드 B 샤드 C
1 Product A(25) Product A(30) Product A(45)
2 Product B(18) Product B(25) Product C(44)
3 Product C(25)    

 

청크의 분포가 위와 같다라고 가정하고 집계 시 사이즈를 2로 지정하면 아래와 같은 결과를 반환할 것이다.

 

1 Product A(100)
2 Product B(43)
3 Product C(44)

 

결과는 나왔지만, Product C의 값에 오차가 생겼다. 즉, 쿼리 작성시 적절히 size값을 정해서 오차를 줄이거나 혹은 전부 포함시켜야한다. 하지만 역시나 사이즈를 키우면 키울 수록 집계 비용은 올라갈 것이다. 즉 위에서는 doc_count_error_upper_bound 값이 25가 될것이다.

 

집계와 샤드 크기

텀즈 집계가 수행될 때 각 샤드에게 최상위 버킷을 제공하도록 요청한 후에 모든 샤드로부터 결과를 받을 때까지 기다린다. 결과를 기다리다가 모든 샤드로부터 결과를 받으면 설정된 size에 맞춰 하나로 병합한 후 결과를 반환한다.

각 샤드는 size에 해당되는 갯수로 집계 결과를 반환하지 않는다. 각 샤드에서는 정확성을 위해 size의 크기가 아닌 샤드 크기를 이용한 경험적인 방법(샤드 크기*1.5+10)을 사용해 내부적으로 집계를 수행하는데, 텀즈 집계 결과로 받을 텀의 개수를 정확하게 파악할 수 있는 경우에는 shard_size 속성을 사용해 각 샤드에서 집계할 크기를 직접 지정해 불필요한 연산을 줄이면서 정확도를 높힐 수 있다.

앞서 설명한 바와 같이 shard_size가 기본값 -1로 되어있다면 엘라스틱서치가 샤드 크기를 기준으로 자동으로 추정한다. 만약 shard_size를 직접 설정할 경우에는 size보다 작은 값은 설정할 수 없다.

 

여기까지 간단히 버킷집계를 다루어보았고, 다음 포스팅에 이어 파이프라인 집계부터 다루어볼 것이다.

 

2019/09/19 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,메트릭(Metric Aggregations) 집계) -1

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,메트릭(Metric Aggregations) 집계) -1

이번에 다루어볼 내용은 엘라스틱서치 Aggregation API이다. 해당 기능은 SQL과 비교하면 Group by의 기능과 아주 유사하다. 즉, 문서 데이터를 그룹화해서 각종 통계 지표 만들어 낼 수 있다. 엘라스틱서치의 집..

coding-start.tistory.com

2019/09/20 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,파이프라인(Pipeline Aggregations) 집계) -3

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,파이프라인(Pipeline Aggregations) 집계) -3

파이프라인 집계(Pipeline Aggregations)는 다른 집계와 달리 쿼리 조건에 부합하는 문서에 대해 집계를 수행하는 것이 아니라, 다른 집계로 생성된 버킷을 참조해서 집계를 수행한다. 집계 또는 중첩된 집계를..

coding-start.tistory.com

 

posted by 여성게
:

이번에 다루어볼 내용은 엘라스틱서치 Aggregation API이다. 해당 기능은 SQL과 비교하면 Group by의 기능과 아주 유사하다. 즉, 문서 데이터를 그룹화해서 각종 통계 지표 만들어 낼 수 있다.

 

엘라스틱서치의 집계(Aggregation)

통계 분석을 위한 프로그램은 아주 많다. 하지만 실시간에 가깝게 어떠한 대용량의 데이터를 처리하여 분석 결과를 내놓은 프로그램은 많지 않다. 즉, RDBMS이나 하둡등의 대용량 데이터를 적재하고 배치등을 돌려 분석을 내는 것이 대부분이다. 하지만 엘라스틱서치는 많은 양의 데이터를 조각내어(샤딩)내어 관리하며 그 덕분에 다른 분석 프로그램보다 거의 실시간에 가까운 통계 결과를 만들어낼 수 있다.

 

하지만 집계기능은 일반 검색 기능보다 훨씬 더 많은 리소스를 소모한다. 성능 문제를 어느정도 효율적으로 해결하기 위해서는 캐시를 적절히 이용해야 하는 것이다. 물론 우리가 직접적으로 캐시를 조작하는 API를 사용하거나 하지는 않지만 어느정도 설정으로 조정가능하다. 그렇다면 엘라스틱서치에서 사용하는 캐시의 종류가 뭐가 있는지 간단히 알아보자.

 

캐시 종류 설명에 앞서 우선 엘라스틱서치의 캐시를 이용하면 질의의 결과를 캐시에 두고 다음에 동일한 요청이 오면 다시 한번 요청을 처리하는 것이 아닌 캐시에 있는 결과값을 그대로 돌려준다. 보통 캐시의 크기는 일반적으로 힙 메모리의 1%로 정도를 할당하며, 캐시에 없는 질의의 경우 성능 향상에 별다른 도움이 되지 못한다. 만약 엘라스틱서치가 사용하는 캐시 크기를 키우고 싶다면 아래와 같은 설정이 가능하다.

 

~/elasticsearch.yml

indices.requests.cache.size: n%

 

여기서 퍼센트(%)는 엘라스틱서치가 사용하는 힙메모리 중 몇 퍼센트를 나타내는 것이다. 다음은 엘라스틱서치가 사용하는 캐시 종류이다.

 

캐시명 설명
Node query Cache

노드의 모든 샤드가 공유하는 LRU(Least-Recently-Used)캐시다. 캐시 용량이 가득차면 사용량이 가장 적은 데이터를 삭제하고 새로운 결과값을 캐싱한다.

쿼리 캐싱 사용여부는 elasticsearch.yml 파일에 아래 옵션을 추가한다. 기본값은 true이다.

index.queries.cache.enabled: true

Shard request Cache 샤드는 데이터를 분산 저장하기 위한 단위로서, 사실 그 자체가 온전한 기능을 가지는 인덱스라고 볼 수 있다. 그래서 우리는 조회 기능을 특정 샤드에 요청해서 그 샤드에 있는 결과값만 얻어올 수 있는 이유가 그렇다. Shard request Cache는 바로 이 샤드에서 수행된 쿼리의 결과를 캐싱한다. 샤드의 내용이 변경되면 캐시도 삭제하기 때문에 문서 수정이 빈번한 인덱스에서는 오히려 성능 저하가 있을 수 있다.
Field data Cache 엘라스틱서치가 필드에서 집계 연산을 수행할 때는 모든 필드 값을 메모리에 로드한다. 이러한 이유로 엘라스틱서치에서 계산되는 집계 쿼리는 성능적인 측면에서 비용이 상당하다. Field data Cache는 집계 계산동안 필드의 값을 메모리에 보관한다.

 

Aggregation API

 

집계 쿼리 구조

 

GET>http://localhost:9200/indexName/_search?size=0

1
2
3
4
5
6
7
8
9
10
"aggregations":{
    "<aggregation_name>":{
        "<aggregation_type>":{
            "<aggregation_body>"
        }
        [,"meta":{[<meta_data_body>]}]?
        [,"aggregations":{[<sub_aggregation>]+}]?
    }
    ,[,"<aggregation_name_2>:{...}"]*
}
cs

 

집계쿼리는 위와 같은 구조를 갖는다. 각각의 키값에 대한 설명은 직접 예제 쿼리를 통해 다루어볼 것이다. 엘라스틱서치의 집계 기능이 강력한 이유중 하나는 위의 쿼리에서 보듯 여러 집계를 중첩하여 더 고도화된 데이터를 반환할 수 있다는 점이다. 물론 중첩이 될수록 성능은 떨어지지만 더 다양한 데이터를 집계할 수 있다. 또한 URL 요청을 보면 맨뒤에 size=0이 보일 것이다. 해당 쿼리스트링을 보내지 않으면 집계 스코프 대상(query)의 결과도 노출되니 집계 결과만 보고 싶다면 size=0으로 지정해주어야 한다.

 

집계 Scope

집계 API의 전체 요청 JSON구조를 보면 아래와 같다.

 

GET> http://localhost:9200/apache-web-log/_search?size=0

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "query":{
        "match_all":{}
    },
    "aggs":{
        "region_count":{
            "terms":{
                "field":"geoip.region_name.keyword",
                "size":20
            }
        }
    }
}
cs

 

위에 query라는 필드가 하나더 존재하는데, 해당 쿼리를 통해 나온 결과값을 이용하여 집계를 내겠다라는 집계 대상이 되는 Scope를 query를 이용하여 지정한다. 참고로 aggregations는 aggs로 줄여서 필드명을 작성할 수 있다. 그렇다면 아래와 같은 쿼리는 어떠한 결과를 낼까?

 

GET> http://localhost:9200/apache-web-log/_search?size=0

1
2
3
4
5
6
7
8
9
10
11
{
    "size":0,
    "aggs":{
        "region_count":{
            "terms":{
                "field":"geoip.region_name.keyword",
                "size":3
            }
        }
    }
}
cs

 

쿼리가 생략되면 내부적으로 match_all 쿼리를 수행한다. 또한 이러한 경우도 있다. 한번의 집계 쿼리를 통해 사용자가 지정한 질의에 해당하는 문서들 집계를 수행하고 전체 문서에 대해서도 집계를 수행해야 하는 경우는 아래와 같이 글로벌 버킷을 사용하면 된다.

 

GET> http://localhost:9200/apache-web-log/_search?size=0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
    "query":{
        "match":{
            "geoip.region_name":"California"
        }
    },
    "aggs":{
        "region_count":{
            "terms":{
                "field":"geoip.region_name.keyword",
                "size":3
            }
        },
        "global_aggs":{
            "global":{},
            "aggs":{
                "all_doc_aggs":{
                    "terms":{
                        "field":"geoip.region_name.keyword",
                        "size":3
                    }
                }
            }
        }
    }
}
cs

 

우선 region_name이 California인 질의의 결과를 이용하여 region_count라는 집계를 수행하고 이것 이외로 global_aggs 글로벌 버킷의 all_doc_aggs 집계를 전체 문서를 대상으로 한번더 수행한다. 결과는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 756,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "global_aggs": {
            "doc_count": 10001,
            "all_doc_aggs": {
                "doc_count_error_upper_bound": 77,
                "sum_other_doc_count": 5729,
                "buckets": [
                    {
                        "key": "California",
                        "doc_count": 756
                    },
                    {
                        "key": "Texas",
                        "doc_count": 588
                    },
                    {
                        "key": "Virginia",
                        "doc_count": 424
                    }
                ]
            }
        },
        "region_count": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
                {
                    "key": "California",
                    "doc_count": 756
                }
            ]
        }
    }
}
cs

 

그렇다면 집계의 종류에는 무엇이 있을까?

 

집계 종류 설명
버킷 집계 쿼리 결과로 도출된 문서 집합에 대해 특정 기준으로 나눈 다음 나눠진 문서들에 대한 산술 연산을 수행한다. 이때 나눠진 문서들의 모음들이 각 버컷에 해당된다.
메트릭 집계 쿼리 결과로 도출된 문서 집합에서 필드의 값을 더하거나 평균을 내는 등의 산술 연산을 수행한다.
파이프라인 집계 다른 집계 또는 관련 메트릭 연산의 결과를 집계한다.
행렬 집계 버킷 대상이 되는 문서의 여러 필드에서 추출한 값으로 행렬 연산을 수행한다. 이를 토대로 다양한 통계정보를 제공한다.

 

이제 위의 집계 종류에 대하여 하나하나 간단히 다루어보자.

 

메트릭 집계

메트릭 집계(Metrics Aggregations)를 사용하면 특정 필드에 대해 합이나 평균을 계산하거나 다른 집계와 중첩해서 결과에 대해 특정 필드의 _score 값에 따라 정렬을 수행하거나 지리 정보를 통해 범위 계산을 하는 등의 다양한 집계를 수행할 수 있다. 이름에서도 알 수 있듯이 정수 또는 실수와 같이 숫자 연산을 할 수 있는 값들에 대한 집계를 수행한다.

 

메트릭 집계는 또한 단일 숫자 메트릭 집계와 다중 숫자 메트릭 집계로 나뉘는데, 단일 숫자 메트릭 집계는 집계를 수행한 결과값이 하나라는 의미로 sum과 avg 등이 속한다. 다중 숫자 메트릭 집계는 집계를 수행한 결과값이 여러개가 될 수 있고, stats나 geo_bounds가 이에 속한다.

 

-합산집계(sum)

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

 

apache 로그에 유입되는 데이터의 바이트 총합을 구하는 집계 쿼리이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
    "aggs":{
        "total_bytes":{
            "sum":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 12,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "total_bytes": {
            "value": 2747282505
        }
    }
}
cs

 

만약 전체 데이터가 아닌 쿼리를 날려 매치되는 문서를 집계하기 위해서 특정 지역에서 유입된 apache 로그를 검색해 그 결과로 bytes 수를 총합하는 쿼리는 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "total_bytes":{
            "sum":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "total_bytes": {
            "value": 428964
        }
    }
}
cs

 

논외의 이야기이지만, 스코어 점수가 필요없는 어떠한 검색에 constant_score 쿼리를 사용하면 성능상 이슈가 있다. 자주 사용되는 필터 쿼리는 엘라스틱 서치에서 캐시하므로 성능에 이점이 있을 수 있다. 만약 위의 쿼리에서 바이트를 KB나 MB,GB 단위로 보고 싶다면 어떻게 하면 좋을까? 사실 집계 쿼리에 데이터 크기 단위를 조정하는 기능은 없다. 하지만 script를 이용하면 집계되는 데이터를 원하는 단위로 변환이 가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "total_bytes":{
            "sum":{
                "script":{
                    "lang":"painless",
                    "source":"doc.bytes.value"
                }
            }
        }
    }
}
cs

 

해당 쿼리는 위의 쿼리와 동일한 결과를 내놓는다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "total_bytes":{
            "sum":{
                "script":{
                    "lang":"painless",
                    "source":"doc.bytes.value / params.divice_value",
                    "params":{
                        "divice_value":1000
                    }
                }
            }
        }
    }
}
 
->result
{
    "took": 30,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "total_bytes": {
            "value": 422
        }
    }
}
cs

 

이렇게 스크립트를 이용하면 결과값을 일부 후처리할 수 있다. 하지만 결과가 조금이상하다. 428964/1000 인데 422가 됬다. 분명 428이 되야하는데 말이다. 그 이유는 모든 합산 값에 대한 나누기가 아니라 각 문서의 개별적인 값을 1000으로 나눈 후에 더했기 때문이다. 즉, 1000보다 작은수는 모두 0이 되어 합산이 되었다. 이 문제를 해결하기 위해서는 정수가 아닌 실수로 값을 계산해야한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "total_bytes":{
            "sum":{
                "script":{
                    "lang":"painless",
                    "source":"doc.bytes.value / (double)params.divice_value",
                    "params":{
                        "divice_value":1000
                    }
                }
            }
        }
    }
}
 
->result
{
    "took": 18,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "total_bytes": {
            "value": 428.96399999999994
        }
    }
}
cs

 

-평균 집계(avg)

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "avg_bytes":{
            "avg":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "total_bytes": {
            "value": 20426.85714285714
        }
    }
}
cs

 

-최소값 집계(min)

최소값 집계는 단일 숫자 메트릭 집계에 해당한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "min_bytes":{
            "min":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "min_bytes": {
            "value": 1015
        }
    }
}
cs

 

최대값 집계는 aggregation_type을 max로 바꾸어주면 된다.

 

-개수집계(count)

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "count_bytes":{
            "value_count":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "count_bytes": {
            "value": 21
        }
    }
}
cs

 

-통계집계(Stats)

통계집계는 결과값이 여러 개인 다중 숫자 메트릭 집계에 해당한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "stats_bytes":{
            "stats":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "stats_bytes": {
            "count": 21,
            "min": 1015,
            "max": 53270,
            "avg": 20426.85714285714,
            "sum": 428964
        }
    }
}
cs

 

count,min,max,avg,sum 등 한번에 모든 집계 결과를 받을 수 있다.

 

-확장 통계 집계(extended Stats)

확장 통계 집계는 결과값이 여러 개인 다중 숫자 메트릭 집계에 해당한다. 앞의 통계 집계를 확장해서 표준편차 같은 통계값이 추가된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "count_bytes":{
            "extended_stats":{
                "field":"bytes"
            }
        }
    }
}
 
->result
{
    "took": 4,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "count_bytes": {
            "count": 21,
            "min": 1015,
            "max": 53270,
            "avg": 20426.85714285714,
            "sum": 428964,
            "sum_of_squares": 18371748404,
            "variance": 457588669.3605442,
            "std_deviation": 21391.32229107271,
            "std_deviation_bounds": {
                "upper": 63209.501725002556,
                "lower": -22355.787439288277
            }
        }
    }
}
cs

 

-카디널리티 집계(Cardinality)

카디널리티 집계는 단일 숫자 메트릭 집계에 해당한다. 개수 집합과 유사하게 횟수를 계산하는데, 중복된 값은 제외한 고유한 값에 대한 집계를 수행한다. 하지만 모든 문서에 대해 중복된 값을 집계하는 것은 성능에 큰 영향을 줄 수 있기에 근사치를 통해 집계한다. 근사치를 구하기 위해 HyperLogLog++ 알고리즘 기반으로 동작한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "geoip.country_name":"United"    
                    }
                },
                {
                    "match":{
                        "geoip.country_name":"States"
                    }
                }
            ]
        }
    },
    "aggs":{
        "us_city_names":{
            "cardinality":{
                "field":"geoip.city_name.keyword"
            }
        }
    }
}
 
->result
{
    "took": 25,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 3974,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "us_city_names": {
            "value": 206
        }
    }
}
cs

 

-백분위 수 집계(Percentiles)

역시나 근사치이고 TDigest 알고리즘을 이용한다. 카디날리티 집계와 마찬가지로 문서들의 집합 크기각 작을 수록 정확도는 높아지고, 문서의 집합이 클수록 오차범위가 늘어난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "bytes_percentiles":{
            "percentiles":{
                "field":"bytes"
            }    
        }
    }
}
 
->result
{
    "took": 14,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "bytes_percentiles": {
            "values": {
                "1.0": 1015,
                "5.0": 1015,
                "25.0": 3638,
                "50.0": 6146,
                "75.0": 50662.75,
                "95.0": 53270,
                "99.0": 53270
            }
        }
    }
}
cs

 

아래와 같이 백분율을 명시할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "bytes_percentiles":{
            "percentiles":{
                "field":"bytes",
                "percents":[0,10,20,30,40,50,60,70,80,90,100]
            }    
        }
    }
}
 
->result
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "bytes_percentiles": {
            "values": {
                "0.0": 1015,
                "10.0": 1015,
                "20.0": 3638,
                "30.0": 4629.2,
                "40.0": 4877,
                "50.0": 6146,
                "60.0": 17147,
                "70.0": 37258.399999999994,
                "80.0": 52315,
                "90.0": 52697,
                "100.0": 53270
            }
        }
    }
}
cs

 

-백분위 수 랭크 집계

위의 랭크 집계와는 달리 특정 값을 주고 어느 백분위 범위에 속하는지를 결과값으로 돌려준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geoip.city_name":"Paris"
                }
            }
        }
    },
    "aggs":{
        "bytes_percentiles_rank":{
            "percentile_ranks":{
                "field":"bytes",
                "values":[4000,6900]
            }    
        }
    }
}
 
->result
{
    "took": 5,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 21,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "bytes_percentiles_rank": {
            "values": {
                "4000.0": 26.592105768861217,
                "6900.0": 53.03370689244701
            }
        }
    }
}
cs

 

-지형 경계 집계

지형 좌표를 포함하고 있는 필드에 대해 해당 지역 경계 상자를 계산하는 메트릭 집계다. 해당 집계를 사용하기 위해서는 계산하려는 필드의 타입이 geo_point여야 한다.

 

 

필드 매핑타입이다.

 

해당 필드에 들어간 값의 예제이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
    "aggs":{
        "viewport":{
            "geo_bounds":{
                "field":"geoip.location",
                "wrap_longitude":true
            }    
        }
    }
}
 
->result
{
    "took": 14,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "viewport": {
            "bounds": {
                "top_left": {
                    "lat": 69.34059997089207,
                    "lon": -159.76670005358756
                },
                "bottom_right": {
                    "lat": -45.88390002027154,
                    "lon": 176.91669998690486
                }
            }
        }
    }
}
cs

 

추후 키바나에서 이러한 지형집계로 시각화를 할 수 있다.

 

 

-지형 중심 집계

지형 경계 집계 범위에서 정가운데의 위치를 반환한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
    "aggs":{
        "centroid":{
            "geo_centroid":{
                "field":"geoip.location"
            }    
        }
    }
}
 
->result
{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 10001,
        "max_score": 0,
        "hits": []
    },
    "aggregations": {
        "centroid": {
            "location": {
                "lat": 38.715619301146354,
                "lon": -22.189867686554656
            },
            "count": 9993
        }
    }
}
cs

 

여기까지 메트릭 집계에 대해 간단히 다루어봤다. 글이 길어져 다음 포스팅에 이어서 집계 API를 다루어보도록 한다.

 

2019/09/20 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,버킷(Bucket Aggregations) 집계) -2

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,버킷(Bucket Aggregations) 집계) -2

이번 포스팅은 엘라스틱서치 Aggregation(집계) API 두번째 글이다. 이번 글에서는 집계중 버킷집계(Bucket)에 대해 알아볼 것이다. 우선 버킷 집계는 메트릭 집계와는 다르게 메트릭을 계산하지 않고 버킷을 생..

coding-start.tistory.com

2019/09/20 - [Search-Engine/Elasticsearch&Solr] - Elasticsearch - Aggregation API(엘라스틱서치 집계,파이프라인(Pipeline Aggregations) 집계) -3

 

Elasticsearch - Aggregation API(엘라스틱서치 집계,파이프라인(Pipeline Aggregations) 집계) -3

파이프라인 집계(Pipeline Aggregations)는 다른 집계와 달리 쿼리 조건에 부합하는 문서에 대해 집계를 수행하는 것이 아니라, 다른 집계로 생성된 버킷을 참조해서 집계를 수행한다. 집계 또는 중첩된 집계를..

coding-start.tistory.com

 

posted by 여성게
:

 

오늘 간단히 다루어볼 내용은 엘라스틱서치의 REST 자바 클라이언트인 Rest High Level Client를 이용하여 Index Template을 생성해보는 예제이다. 바로 예제로 들어간다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void indexTemplate() throws IOException {
        
        String typeName = "_doc";
        
        if(!existTemplate()) {
            
            try(RestHighLevelClient client = createConnection();){
                PutIndexTemplateRequest templateRequest = new PutIndexTemplateRequest("log-template");
                
                templateRequest.patterns(Arrays.asList("logstash-*"));
                
                XContentBuilder mapping = XContentFactory.jsonBuilder()
                                                         .startObject()
                                                             .startObject(typeName)
                                                                 .startObject("properties")
                                                                     .startObject("date")
                                                                           .field("type","date")
                                                                      .endObject()
                                                                      .startObject("fieldName")
                                                                          .field("type","keyword")
                                                                      .endObject()
                                                                 .endObject()
                                                             .endObject()
                                                         .endObject();
                
                templateRequest.mapping("_doc", mapping);
                
                AcknowledgedResponse templateResponse = client.indices().putTemplate(templateRequest, RequestOptions.DEFAULT);
                
                if(!templateResponse.isAcknowledged()) throw new ElasticsearchException("Create Index Template Failed !");
                
            }
        }
        
    }
cs

 

해당 인덱스 템플릿으로 생성될 수 있는 인덱스 패턴은 배열로 여러개 지정가능하다. 현재 설정은 단순 mapping만 설정하였지만, settings 정보까지 인덱스 템플릿 설정으로 넣어줄 수 있다. 만약 로그스태시나 비트 프레임워크를 엘라스틱과 연동하여 일자별 로그를 수집하는 기능을 구현한다면 미리 인덱스 템플릿으로 생성될 인덱스의 정의를 잡아주는 것이 좋을 것이다.

 

이제 해당 인덱스 템플릿을 구성한 이후에 logstash-*로 시작하는 인덱스가 생성될때 위와 같은 mapping 설정대로 필드가 생성될 것이다.

posted by 여성게
:

 

오늘 포스팅할 내용은 ELK Stack에서 중요한 보조 수단 중 하나인 Filebeat(파일비트)에 대해 다루어볼 것이다. 우선 Filebeat를 이용하는 사례를 간단하게 하나 들어보자면, 운영중인 애플리케이션에서 File을 통한 로그데이터를 계속 해서 쌓고 있다면 이러한 로그데이터를 단순 파일로 가지고 있는 것이 유용할까? 물론 모니터링하는 시스템이 존재 할 수 있다. 하지만 이러한 모니터링 시스템이 아닌 로그데이터를 계속해서 축적하여 통계를 내고 싶고, 데이터의 증가,하강 추이를 시각화하여 보고 싶을 수도 있다. 이렇게 특정 로그파일을 주기적으로 스캔하여 쌓이고 있는 데이터를 긁어오는 역할을 하는 것이 파일비트이다. 물론 록그스태시만 이용하여 파일에 쌓이는 행데이터를 가져올 수 있다. 하지만 이러한 로그스태시를 엔드서버에 설치하지 못하는 상황이 있을 수 있기에 가벼운 파일비트를 이용하곤 한다.

 

 

파일비트 아키텍쳐

파일비트는 Prospectors, Harvesters, Spooler라는 주요 구성 요소를 가지고 있다. Prospector는 로그를 읽을 파일 목록을 구분하는 역할을 담당한다. 여러 파일 경로를 설정하면 로그를 읽을 파일을 식별하고 각 파일에서 로그를 읽기 시작한다. 이때 파일 컨텐츠, 즉 이벤트 데이터(로그)를 읽는 역할은 Harvester가 담당한다. 파일을 행 단위로 읽고 출력으로 보낸다. 하나의 Harvester가 개별로 파일을 담당하며 파일을 열고 닫는다. 읽어올 파일 수가 여러개가 되면 그에 따라 Harvester도 여러개가 되는 것이다. 이벤트가 발생하여 읽어온 로그데이터는 Spooler에게 보낸다. 그리고 Spooler는 이벤트를 집계하고 설정할 출력으로 전달한다.

 

위의 그림처럼 파일비트는 다수의 Prospector, Harvester로 이루어진다. 파일비트가 지원하는 Input 타입은 log와 stdin이 있다. log는 Prospectors에 정의된 파일을 읽어 데이터를 수집하며, stdin은 표준 입력에서 데이터를 읽는다. 그럼 여기서 의문이 드는 것이 있다. 물리적인 파일을 읽는데 어디까지 읽었고, 출력은 어디까지 보냈고 이런 정보를 어디서 유지하고 있는 것일까? 이러한 정보는 Harvester가 offset으로 디스크에 주기적으로 기록하며 이는 레지스트리 파일에서 관리한다. 엘라스틱서치,카프카,레디스 같은 출력 부분 미들웨어 시스템에 문제가 발생하면 파일비트는 마지막으로 보낸 행을 기록하고, 문제가 해결될때까지 계속 데이터를 수집하고 있는다. 이러한 관리 덕분에 파일비트를 내렸다 다시 올려도 데이터의 위치를 기억한 상태에서 기동되게 된다. 또한 Harvester 같은 경우 출력에게 데이터를 보낸 후에 출력 부분에서 데이터를 잘 받았다는 응답을 기다리는데 해당 응답을 받지 않은 경우 다시 데이터를 보내게됨으로 반드시 한번은 데이터 손실없이 보내게 된다.

 

파일비트 사용법(filebeat-6.6.2 버전기준으로 작성됨)

여기서 파일비트 설치법은 다루지 않는다. 예제에서 다루어볼 것은 Input(file-log type) output(es,logstash) 이다. 간단한 예제이므로 주석에 설명을 달아놓았고 별도의 설명은 하지 않는다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
filebeat.yml
#Filebeat prospectors
filebeat.prospectors:
- input_type: log
  paths:
    - /Users/yun-yeoseong/rnb-chatbot-rivescript/logs/*.log
  #파일비트로 읽어오지 않을 패턴을 지정
  #exclude_lines: ["^INFO"]
  #파일비트로 읽어올 패턴을 지정
  #include_liens: ["^ERR","^WARN"]
 
  #파일비트가 파일을 output으로 내보낼때 밑의 tags도 추가해서 보낸다.(field -> tags)
  #보통 수집기 별로 혹은 애플리케이션 별로 tags 필드의 값을 다르게 주어
  #키바나와 로그스태시에서 이벤트를 필터링하는데 사용한다.
  tags: ["app_logs"]
 
  #output으로 보낼때 해당 필드를 추가해서 보낸다.(field -> fields.env)
  fields:
    env: dev
 
  #여러라인의 로그를 어떻게 처리할지 패턴을 정의한다
  #RE2 구문을 기반으로 정규식을 짠다(https://godoc.org/regexp/syntax)
  #공백으로 시작하는 연속행을 발견한다.
  multiline.pattern: '^[[:space:]]'
  #정규식패턴 무효화 여부
  multiline.negate: false
  #공백으로 시작하는 구문을 공백으로 시작하지 않는 구문 어디로 합칠것인가(after,before)
  multiline.match: after
 
  #파일을 몇 초마다 스캔할 것인가(default 10s)
  scan_frequency: 5s
  #엘라스틱서치가 아웃풋이라면 document type을 무엇으로 할지
  #document_type: doc
 
#Outputs
#Elasticsearch output
output.elasticsearch:
  #enabled로 출력을 활성/비활성 할 수 있다.
  enabled: true
  #ES클러스터 노드 리스트를 지정할 수 있다. 각 노드의 데이터 분배는
  #라운드로빈 방식을 이용한다.
  hosts: ["localhost:9200","localhost:9300"]
  #인증이 필요할 경우 username/password 지정이 가능
  username: "elasticsearch"
  password: "password"
  #인제스트노드 파이프라인에 전달해 색인 이전에 데이터 전처리가 가능
  #pipeline: "pipeline-name"
 
#logstash output, 로그스태시를 지정하려면 로그스태시에서
#비트 이벤트를 수신할 수 있는 비트 입력 플러그인을 설정해야한다.
#output.logstash:
 # enabled: true
  #출력 노드를 여러개 지정할 수 있다. 기본적으로 임의의 노드를 선택해 데이터를 보내고,
  #데이터를 보낸 호스트에서 응답을 하지 않으면 다른 노드들중 하나로 다시 데이터를 보낸다.
  #hosts: ["localhost:5044","localhost:5045"]
  #로그스태시 호스트들에서 이벤트데이터를 균등히 보내고 싶다면 로드벨런싱 설정을 넣어준다.
  #loadbalance: true
#전역옵션
#파일 정보를 유지하는 데 사용하는 레지스트리 파일 위치를 지정한다.
#마지막으로 읽은 오프셋과 읽은 행이 설정 시 지정한 출력 지점의 응답 여부 등
#filebeat.registry_file: /Users/yun-yeoseong/elasticsearch-file/filebeat/registry
#파일비트 종료 시 데이터 송신이 끝마칠 때까지 대기하는 시간
#파일비트는 기본적으로 종료시 이벤트 송신 처리를 기다리지 않고 종료하기 때문에
#이벤트 송신이 잘되었는지 수신을 종료전에 기다리도록 시간설정을 할 수 있다.
filebeat.shutdown_timeout: 10s
 
#일반옵션
#네트워크 데이터를 발송하는 수집기의 이름 기본적으로 필드에는 호스트 이름을 지정한다.
name: "app_log_harvester"
#동시에 실행할 수 있는 최대 CPU 개수를 설정. 기본값은 시스템에서 사용 가능한 논리적인 CPU 개수
#max_procs: 2
 
cs

 

./filebeat --c ./filebeat_exam.yml -> 해당 명령어로 실행시켜준다.

 

데이터는 spring boot 프로젝트를 실행시켰을 때, 올라가는 로그를 수집한 결과이다. kibana Discovery tab을 이용하여 데이터가 어떻게 들어왔는지 결과를 확인할 것이다.

 

Result

 

 

일부 예제에 들어간 설정과 다른 데이터가 들어간 것도 있다. 데이터는 고려하지 말고 어떤 필드들이 생겼고 어떤 데이터가 설정과 비교하여 들어왔는지 확인하자. 일반적으로 파일비트는 인덱스를 버전정보+날짜정보를 이용하여 일자별로 인덱스 생성을 진행한다.(매일 생긴다는 것은? 그만큼 인덱스가 많아진다는 것이기 때문에 백업 스케쥴을 잘 잡아줘야한다.)그리고 @timestamp를 찍어 데이터 색인 시간을 볼 수 있고, 모든 데이터는 message 필드에 저장된다. 또한 설정 부분에 tags, fields.env 설정이 들어간 것이 있을 것이다. 만약 성격이 다른 애플리케이션이 있고 각각 따로 통계를 내고 싶다면 해당 필드들을 이용하여 필터를 걸수도 있고 또한 Logstash를 이용하여 데이터 처리를 할때 해당 필드들을 이용하여 필터를 걸어 사용할 수도 있을 것이다.(Input의 파일 경로(- input_type)를 여러개 지정할 수 있으므로 각 패스마다 해당 필드의 값을 다르게 준다)

 

여기까지 간단히 파일비트가 뭔지 알아봤고 사용법도 알아보았다. 사실 운영환경에서는 이렇게 간단히 사용할 수 없는 환경이 대부분일 것이다. 레퍼런스를 활용하여 더 깊게 공부할 필요성이 있다.

posted by 여성게
:

개발환경 또는 테스트를 진행하기 위해서는 엘라스틱서치의 단일 노드로도 충분하다.  그래서 엘라스틱서치 노드는 기본적으로 싱글 노드에서 모든 역할을 수행할 수 있게 설정하는 것이 가능하다. 하지만 실제 운영환경에서는 대부분 다수의 노드를 클러스터링하여 구성하기 때문에 각각 목적에 맞는 노드를 적절히 설정해 운영하는 것이 유리하다.

 

엘라스틱서치 노드의 종류

elasticsearch.yml 파일에는 노드 관련 속성이 제공된다. 이 속성들을 적절히 조합해서 특정 모드로 설정하는 것이 가능하다.

 

  • node.master : 마스터 기능 활성화 여부
  • node.data : 데이터 기능 활성화 여부
  • node.ingest : Ingest 기능 활성화 여부
  • search.remote.connect : 외부 클러스터 접속 가능 여부

위의 설정들을 조합하여 아래와 같은 노드 모드로 운영가능하다.

 

  • Single Node mode
  • Master Node mode
  • Data Node mode
  • Ingest Node mode
  • Coordination Node mode

 

Single Node mode

모든 기능을 수행하는 모드다. 기본 설정으로 지정돼 있기 때문에 elasticsearch.yml 파일에 아무런 설정을 하지 않는다면 기본적으로 싱글모드로 동작한다.

 

node.master: true / node.data: true / node.ingest: true / search.remote.connect: true

 

검색 클러스터의 규모가 작을 때는 노드별로 별도의 Role을 분담하여 운영하기 보다는 모든 노드가 싱글 모드로 수행하게 하는 것이 좋다. 일반적으로 3대 이하의 소규모 클러스터를 구축한다면 모든 엘라스틱서치 노드를 싱글모드로 동작시키는 것이 좋다.

 

 

Master Node mode

클러스터의 제어를 담당하는 모드이다. 

 

node.master: true / node.data: false / node.ingest: false / search.remote.connect: false

 

마스터 모드는 기본적으로 인덱스 생성/변경/삭제 등의 역할을 담당한다. 그리고 분산코디네이터 역할을 담당하여 클러스터를 구성하는 노드의 상태를 주기적으로 점검하여 장애를 대비한다. 즉, 마스터 모드는 클러스터 전체를 관장하는 마스터 역할을 수행하게 된다. 이처럼 중요한 역할을 하는 마스터 노드는 클러스터에 다수 존재하는 것이 좋다. 그래야 장애가 발생할 경우에도 후보 마스터 노드가 역할을 위임받아 안정적으로 클러스터 운영 유지가 되기 때문이다.

 

 

Data Node mode

클러스터의 데이터를 보관하고 데이터의 CRUD, 검색, 집계 등 데이터 관련 작업을 담당하는 모드이다.

 

node.master: false / node.data: true / node.ingest: false / search.remote.connect: false

 

노드가 데이터 모드로 동작하면 내부에 색인된 데이터가 저장된다. 이말은 즉, 마스터 노드와는 달리 대용량의 저장소를 필수적으로 갖춰야한다.(물론 대용량 서비스 운영환경이라면) 또한 CRUD 작업과 검색, 집계와 같은 리소스를 제법 잡아먹는 역할도 수행하기 때문에 디스크만이 아닌 전체적인 스펙을 갖춘 서버로 운영하는 것이 좋다.

 

 

Ingest Node mode

다양한 형태의 데이터를 색인할 때 데이터의 전처리를 담당하는 모드다.

 

node.master: false / node.data: false / node.ingest: true / search.remote.connect: false

 

엘라스틱서치에서 데이터를 색인하려면 인덱스라는(RDB Schema) 틀을 생성해야한다. 비정형 데이터를 다루는 저장소로 볼 수 있지만 일정한 형태의 인덱스를 생성해주어야한다. 그리고 해당 인덱스에는 여러 포맷의 데이터 타입 필드가 존재한다. 만약 데이터를 색인할때 간단한 포맷 변경이나 유효성 검증 같은 전처리가 필요할 때 해당 모드를 이용할 수 있다.

 

Coordination Node mode

사용자 요청을 받아 처리하는 코디네이터 모드이다.

 

node.master: false / node.data: false / node.ingest: false / search.remote.connect: false

 

엘라스틱서치의 모든 노드는 기본적으로 코디네이션 모드 노드이다. 이 말은 즉, 모든 노드가 사용자의 요청을 받아 처리할 수 있다는 뜻이다. 하지만 이렇게 별도의 코디네이션 노드가 필요한 이유가 있을까? 싱글 모드로 구성된 클러스터에 사용자가 검색 요청을 보낸다면 검색요청을 받은 노드는 클러스터에 존재하는 모든 데이터 노드에게(싱글 모드는 모든 노드가 대상) 검색을 요청한다. 왜냐하면 클러스터에 존재하는 모든 데이터 노드에 샤드로 데이터가 분산되어 있기 때문이다. 그리고 각 샤드는 자신이 가지고 있는 데이터 내에서 검색을 수행하고 자신에게 요청을 보낸 노드에서 결과값을 전송한다. 그리고 모든 데이터를 취합하여 사용자에게 전달한다. 모든 데이터가 취합될때까지 요청을 다른 노드에게 보낸 코디네이션 노드역할(싱글모드에서는 코디네이션 이외의 모든일을 하는 노드가 된다.)을 하는 노드는 아무 일도 못하고 기다리고 있어야한다. 또한 데이터를 취합하는 일도 많은 양의 메모리가 필요한 작업이다. 이 상황에서 코디네이션 노드를 따로 구축하지 않았다면 이렇게 결과를 취합하는 과정에 마스터 노드로서의 역할이나 데이터 노드로서의 역할을 할 수 없게 되고 최악의 경우에는 노드에 장애가 발생할 수 있다. 이렇게 다른 노드들에게 요청을 분산하고 결과값을 취합하는 코디네이션 노드를 별도로 구축한다면 안정적인 클러스터 운영이 가능해진다.

 

 

대용량 클러스터 환경에서 전용 마스터 노드 구축이 필요한 이유

예를 들어보자. 만약 모든 노드를 싱글모드로 클러스터링을 구축한 환경에서 무거운 쿼리가 주 마스터 역할을 하는 싱글 노드에 요청되어 데이터 노드의 부하로 인해 시스템에 순간적으로 행(hang, freezing 시스템이 아무런 일도 하지 못하는 상황)이 걸리거나 노드가 다운되는 경우가 발생할 수 있다. 그렇다면 주 마스터 역할로써도 정상적으로 동작하지 못할 것이다. 이 순간 시스템 장애가 발생하면 쉽게 복구할 수 있는 상황도 복구할 수 없게되는 상황이 발생한다.

 

이러한 경우 다른 싱글 노드 중 하나가 마스터 역할로 전환되어 처리되지 않을 까라는 생각을 당연히 하게 되지만 모든 상황에서 그렇지는 않다. 주 마스터 노드가 hang 상태에 빠져있지만 시스템적으로 정상적으로 프로세스로 떠 있다 판단 될 수 있어 다른 후보 마스터에게 역할이 위임되지 않을 가능성이 있기 때문이다.

 

이렇게 마스터 노드와 데이터 노드의 분리는 대용량 클러스터 환경에서 필수이게 되는 것이다.

 

 

대용량 클러스터 환경에서의 검색엔진에서 코디네이션 노드 구축이 필요한 이유

엄청난 데이터량을 가지고 있는 클러스터를 가지고 있다고 생각해보자. 만약 이러한 클러스터에서 복잡한 집계 쿼리를 날린다고 가정하면 안그래도 리소스를 많이 잡아먹는 집계쿼리인데 데이터마저 크다면 엄청난 부하를 주게 될 것이다. 이런 상황에서 데이터 노드 모드와 코디네이션 노드 모드를 분리하여 클러스터 환경을 구성한다면 장애가 발생할 가능성이 조금은 낮아질 것이다. 왜냐 검색은 데이터노드가 담당하고 이러한 요청을 보내는 역할과 결과의 병합을 코디네이션 노드가 담당하기에 리소스 사용의 부담을 서로 나누어 갖기 때문이다.

 

 

클러스터 Split Brain 문제 방지

클러스터에는 마스터 노드 모드로 설정된 노드가 최소한 하나 이상 존재해야 장애가 발생하였을 때, 즉시 복구가 가능해진다. 다수의 마스터 노드가 존재할 경우 모든 마스터 노드들은 투표를 통해 하나의 마스터 노드만 마스터 노드로서 동작하고 나머지는 후보 마스터 노드로서 대기한다. 클러스터를 운영하는 중에 마스터 노드에 장애가 발생할 경우 대기 중인 후보 마스터 노드 중에서 투표를 통해 최종적으로 하나의 후보 마스터 노드가 주 마스터 노드로 승격하게 된다. 이후 장애가 발생한 주 마스터노드는 다시 후보 마스터 노드로 하락하게 된다. 이런식으로 마스터 노드의 부재없이 안정적인 클러스터 운영이 가능한 것이다.

 

그렇다면 전용으로 구축되는 마스터 노드는 몇개가 적당할까?

 

이런 상황을 생각해보자. 만약 주 마스터 노드에 장애가 발생하였고 후보 마스터 노드 3개 중 투표를 통해 하나의 마스터 노드를 선출하는 도중에 네트워크 환경에 단절이 발생했다 생각하자. 그렇다면 후보노드들은 모두 나 자신밖에 마스터노드 후보가 없다고 생각하고 자기 자신을 마스터 노드로 승격시킬 것이고, 각 노드가 동일하게 행동하여 하나 이상의 마스터 노드가 생겨버릴 수 있다.(Split Brain 문제) 이렇다면 클러스터는 엉망진창으로 꼬일 것이고 서비스 불능 상태가 될 수 있다. Split Brain 문제는 비단 엘라스틱서치만의 문제는 아니고 클러스터 환경에서 운영되는 애플리케이션 전반적인 문제이다. 엘라스틱서치는 이 상황을 하나의 설정만으로 해결방법을 제시한다.

 

elasticsearch.yml -> discovery.zen.minimum_master_nodes

 

이 속성은 기본 값으로 1을 가진다. 이 뜻은 마스터 노드 선출 투표를 진행할 때, 후보 마스터 노드의 최소한의 갯수를 뜻하는 것이다. 클러스터에 존재하는 마스터 노드의 개수가 1개이거나 2개일 경우는 해당 설정은 1로 설정하고 마스터 노드의 수가 3개 이상일 경우에는 다음 공식에 대입해서 적절한 값을 찾아 설정한다.

 

(마스터 후보 노드 수 / 2) + 1

 

  • 마스터 노드가 3개일 경우 : 3/2+1 = 2
  • 마스터 노드가 4개일 경우 : 4/2+1 = 3
  • 마스터 노드가 5개일 경우 : 5/2+1 = 3
  • 마스터 노드가 6개일 경우 : 6/2+1 = 4

싱글모드노드로 구성하는 간단한 클러스터링

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<node1>
cluster.name: clusterName
node.name: node1
network.host: 0.0.0.0
http.port: 9200
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9500"]
discovery.zen.minimum_master_nodes: 1
transport.tcp.port: 9300
#########싱글 노드로 동작 여부#########
node.master: true
node.data: true
node.ingest: true
search.remote.connect: true
 
 
<node2>
cluster.name: clusterName
node.name: node1
network.host: 0.0.0.0
http.port: 9400
discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300"]
discovery.zen.minimum_master_nodes: 1
transport.tcp.port: 5300
#########싱글 노드로 동작 여부#########
node.master: true
node.data: true
node.ingest: true
search.remote.connect: true
cs

 

두개의 엘라스틱서치 디렉토리를 준비한 후에 각각의 설정 파일을 위와 같이 바꾸어준다. 우선 중요한 것은 cluster.name을 동일하게 마추어줘야한다. 그리고 포트는 적절히 할당해준다. 필자는 동일한 서버환경에서 두개의 노드를 설치한 것이라 포트가 다르지만 서로 다른 환경이라면 동일하게 포트를 맞춰놔도 무방하다. 그리고 중요한 것은 discovery 설정이다. 디스커버리 설정으로 서로다른 노드를 discovery할 수 있게 해주는 설정인 것이다. 그리고 모든 노드가 싱글모드로 동작시키게 하기 위해 싱글 노드 설정으로 세팅해주었다.

 

설정파일을 모두 변경하였으면 각각 엘라스틱서치를 실행 시킨 후에 아래의 요청을 보내 클러스터링이 잘 걸렸나 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET http://localhost:9200/_cluster/health
 
result ->
{
    "cluster_name": "clusterName",
    "status": "green",
    "timed_out": false,
    "number_of_nodes": 2,
    "number_of_data_nodes": 2,
    "active_primary_shards": 0,
    "active_shards": 0,
    "relocating_shards": 0,
    "initializing_shards": 0,
    "unassigned_shards": 0,
    "delayed_unassigned_shards": 0,
    "number_of_pending_tasks": 0,
    "number_of_in_flight_fetch": 0,
    "task_max_waiting_in_queue_millis": 0,
    "active_shards_percent_as_number": 100
}
cs

 

필자는 클러스터 설정이 모두 정상적으로 적용되어 Active한 노드가 2개인것을 볼 수 있다. 물론 완벽한 클러스터링을 위해서는 설정해야 할것이 많을 수도 있다. 그리고 지금은 개발모드로 작성되어 부트스트랩과정을 거치지 않아서 쉽게 구성되었을 것이다. 하지만 추후에 운영환경모드로 실행을 하면 14가지정도의 부트스트랩 과정을 검사하기 때문에 맞춰주어야 하는 설정들이 많이 있다. 더 자세한 클러스터링 환경설정은 추후에 다루어볼 것이다.

posted by 여성게
: