오늘 다루어볼 내용은 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 여성게
:

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

 

엘라스틱서치 노드의 종류

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 여성게
:

엘라스틱서치는 JVM 위에서 동작하는 자바 애플리케이션이다. 그렇기 때문에 엘라스틱서치는 JVM 튜닝옵션들을 제공한다. 하지만 수년간 엘라스틱서치의 경험으로 최적화된 JVM옵션을 거의 적용하고 있기 때문에 변경할 필요는 없다고 한다. 하지만 Heap Memory 사이즈 같은 경우는 실 운영환경에서는 기본으로 제공하는 1기가보다는 높혀서 사용할 필요성이 있다.

 

$ELASTIC_PATH/config/jvm.options 파일에 들어가면 Xms,Xmx 옵션으로 최소,최대 JVM 힙 메모리 사이즈 조정이 가능하며 기타 다른 JVM옵션 변경이 가능하다. 다시 한번 강조하자면 왠만하면 다른 옵션들은 디폴트 값으로 가져가 사용하는 것이 좋다.

 

그리고 보통 JVM에서 Xms 크기의 메모리를 사용하다가 메모리가 더 필요하면 Xmx 크기만큼 메모리를 늘려서 사용하게 된다. 하지만 이렇게 Xmx크기의 메모리를 사용하려면 그 순간 갑자기 성능이 나빠질 수 있다. 그렇기 때문에 왠만하면 Xms,Xmx의 크기를 같게 주는 것이 여러모로 유리하다. 그리고 힙사이즈를 너무 작게하면 OOM(Out Of Memory) 오류가 날 수 있고 그렇다고 힙사이즈를 너무 크게하면 FullGC 발생시 STW가(Stop The World) 발생해 애플리케이션이 프리징되는 시간이 길어지기 때문에 사용자에게 애플리케이션이 멈춰보이는 현상을 줄 수 있기에 무작정 큰 메모리 사이즈를 할당하는 것도 좋지 않다.(보통 엘라스틱서치의 힙사이즈는 데몬 서버당 32기가 이하로 설정하길 권장한다.)

 

 

운영체제가 사용할 메모리 공간을 확보

엘라스틱서치 샤드는 내부적으로 루씬을 가지고 있으며 루씬은 세그먼트 생성 및 관리를 위해 커널 시스템 캐시를 많이 사용한다. 하지만 이렇게 시스템 캐시는 운영체제가 가지고 있는 메모리 공간으로 커널 내부에 존재하게 된다. 즉, 운영체제가 사용할 메모리를 대략 전체 스펙에 50%정도를 남겨놔야 좋다.

 

자바 8 기반에서는 힙 크기를 32기가 이상 사용하지 않는 것이 좋다

예) 128기가의 물리 머신에서 64기가를 운영체제에게 나머지 64기가를 엘라스틱서치가 할당받는 다면 밑에 스펙중 무엇을 선택할것인가?

  • 1)64기가 운영체제, 64기가 엘라스틱서치 노드1개
  • 2)64기가 운영체제,32기가 엘라스틱서치 노드2개

위의 두가지중 엘라스틱서치에서 안내하는 권장사항을 따른다면 2번 스펙을 따를 것이다. 이 말은 엘라스틱서치 노드 데몬서버 하나당 힙메모리를 32기가이상 잡지 않는것이다. 엘라스틱서치에서 이러한 가이드를 제공하는 이유는 핫스팟(Hot-Spot) JVM의 Object Pointer 정책때문이다. 즉, 엘라스틱서치 뿐만 아니라 모든 자바 기반 애플리케이션에도 동일하게 32기가 이상 잡지 않는 것을 권장한다. Object Pointer는 간단히 객체의 메모리 번지를 표현하는 주소값이다. 그리고 32비트,64비트 JVM은 기본적으로 모두 32비트 주솟값을 가지고 동작한다. 이유는 기본적으로 JVM은 32비트 Object Pointer를 사용하고 있기 때문이다. 여기서 너무 자세한 내용을 설명하는 것은 주제와 맞지 않을 것같아서 간단히 이야기하면 64비트 주솟값을 사용하면 주솟값을 위해 낭비되는 메모리값이 너무 많아 진다. 그렇기 때문에 JVM은 기존 Object Pointer를 개선한 Compressed Ordinary Object Pointer를 사용하는데 이 포인터가 기본적으로 32비트 Object Pointer한다. 이렇게 64비트 환경의 서버에서 32비트의 주소값을 사용하여 메모리 낭비를 줄이며 효율적으로 사용되는데, 만약 JVM 힙메모리 옵션이 32기가 이상 넘어가게되면 COOP에서 일반적인 64비트 주소값을 사용하는 OOP로 바뀌도록 되어 있다. 이렇게 64비트 주솟값 OOP 사용하게 되면 주솟값을 위하여 낭비되는 메모리의 값이 동일하게 증가하기 때문에 효율성이 떨어지게 되는 것이다.

 

 

상황에 따른 엘라스틱서치 힙크기 설정하기

 

  • 1)적절한 성능의 서버 : 32기가 힙메모리를 할당하여 엘라스틱서치 노드를 사용한다.
  • 2)고성능 서버 : 적절히 엘라스틱서치 노드를 나누어서 32기가씩 할당하여 사용한다.
  • 3)전문(Full Text) 검색을 주목적으로 엘라스틱서치를 사용하는 경우 : 엘라스틱서치 힙에 32기가를 할당하고 나머지를 운영체제에 남겨둬서 루씬이 시스템 캐시를 통해 메모리를 최대한 사용할 수 있게 한다. 전문 검색의 경우 메모리 연산보다는 루씬의 역색인 구조를 이용하기 때문에 시스템 캐시를 많이 이용한다.
  • 4)Not Analyzed 필드의 정렬/집계 작업이 많이 수행되는 경우 : 분석되지 않은 필드들의 정렬/집계는 루씬의 DocValues(루씬 캐시,기본적으로 not analyzed한 필드들은 기본적으로 DocValues가 생성됨)를 사용하기 때문에 힙 공간을 거의 사용하지 않고 시스템캐시를 이용하기 때문에 루씬에게 메모리를 많이 할당 될 수 있게 한다.
  • 5)전문(Full Text) 필드에서 정렬/집계 작업을 많이 수행하는 경우 : 전문(analyzed fleld)같은 경우는 루씬의 DocValues를 이용하지 않기 때문에 fielddata라는 힙 기반의 캐시를 이용하기 때문에 전문 필드 정렬/집계가 많은 경우 32기가로 엘라스틱서치 노드를 나누어서 여러개 생성하는 방식이 효율적이다.

 

posted by 여성게
:

 

엘라스틱서치의 구성요소

엘라스틱서치는 기본적으로 클러스터라는 단위로 데이터를 제공한다. 클러스터는 하나 이상의 물리적인 노드로 이루어져 있으며 각 노드는 모두 데이터 색인 및 검색 기능을 제공하는 일종의 물리적인 서버와 같다. 내부에는 루씬 라이브러리를 사용하고 있으며 루씬은 엘라스틱서치의 근간을 이루는 핵심 모듈이다.

 

1)클러스터

클러스터는 데이터를 실제로 가지고 있는 노드의 모음이다. 엘라스틱서치에서는 관련된 모든 노드들을 논리적으로 묶어서 클러스터라고 부른다. 또한 노드들은 같은 클러스터 내부의 데이터만 서로 공유가 가능하다. 같은 클러스터를 구성하는 노드들을 같은 클러스터 이름으로 설정해야한다. 엘라스틱서치는 설정된 클러스터 이름을 이용해 같은 클러스터의 구성원으로 인식된다. 같은 클러스터 내부의 노드는 평소 데이터 색인이나 검색작업을 함께 수행하게 되고 장애가 발생했을 때 데이터 복구를 위한 다양한 작업도 서로 협력해서 함께 진행한다.

 

Cross Cluster Search

일반적으로 검색 시 하나의 클러스터 데이터만 검색하는 것이 원칙이긴 하지만 최초 설계 시 전혀 관련성 없어 보이던 데이터들을 시간이 지나서 데이터가 점점 많이 쌓일 수록 데이터 연관성이 생길 수도 있다. 엘라스틱서치에서는 이처럼 다양한 필요에 따라 다수의 클러스터를 한 번에 검색할 수 있는 Cross Cluster Search라는 기능을 제공한다.

 

 

Cross-cluster search | Elasticsearch Reference [7.1] | Elastic

The cross-cluster search feature allows any node to act as a federated client across multiple clusters. A cross-cluster search node won’t join the remote cluster, instead it connects to a remote cluster in a light fashion in order to execute federated sear

www.elastic.co

 

2)노드

물리적으로 실행된 런타임 상태의 엘라스틱서치를 노드라고 부른다. 노드는 위에서 설명한 클러스터를 이루는 구성원의 일부이며 실제 데이터를 물리적으로 가지고 있는 단일 서버이기도 하다. 실행 시 노드는 클러스터에 의해 UUID가 할당되고 클러스터 내에서는 할당된 UUID로 서로를 식별한다. 기본 값으로 부여되는 UUID를 원하지 않는다면 직접 이름을 설정할 수도 있다.(하지만 이름은 유일해야한다.) 노드는 내부에 다수의 인덱스를 가지고 있으며, 각 인덱스는 다수의 문서를 가지고 있다. 색인 작업을 통해 엘라스틱서치로 전송한 데이터는 인덱스라는 논리적인 자료구조 속에 문서라는 단위로 저장된다. 같은 클러스터 내부에서 존재하는 모든 노드는 서로 다른 노드와 수시로 정보를 주고 받는다. 기본적으로 모든 노드는 마스터 노드와 데이터 노드의 역할을 동시에 수행할 수 있도록 설정되어있지만 실제 대용량의 운영환경에서는 각각 용도에 맞는 노드를 적절히 분리하여 클러스터링하는 것이 좋다.

 

2)-1 노드의 형태

  • 마스터 노드(Master Node) : node.master 설정이 true로 설정된 노드다. 클러스터의 제어를 담당한다.
  • 데이터 노드(Data Node) : node.data 설정이 true로 설정된 노드다. 데이터를 보유하고 CRUD, 검색, 집계 등 데이터 관련 작업을 담당한다.
  • 인제스트 노드(Ingest Node) : node.ingestrk true로 설정된 노드다. 색인 전 전처리 작업을 담당한다.
  • 코디네이팅 노드(Coordinating Node) : 검색이나 집계 시 분산 처리만을 목적으로 설정된 노드다. 대량의 데이터를 처리할 경우에 효율적으로 사용할 수 있는 노드이다.

3)인덱스

엘라스틱서치 인덱스는 유사한 특성을 가지고 있는 문서를 모아둔 문서들의 모임이다. 클러스터 내부에 생성되는 모든 인덱스는 클러스터 내에서 유일한 인덱스명을 가져야한다. 또한 인덱스명은 모두 소문자로 설정해야 한다. 또한 과거 버전과는 다르게 현재 버전들은 하나의 인덱스에 하나의 타입만 생성해야 한다.

 

4)문서

문서는 검색 대상이 되는 실제 물리적인 데이터를 뜻한다. 문서는 인덱스를 생성할 수 있는 기본적인 정보 단위이고 엘라스틱서치에서는 JSON형식으로 문서를 표현한다.

 

5)샤드

인덱스에는 매우 많은 양의 문서가 저장될 수 있다. 일반적으로 하나의 하드웨어에서 제공되는 리소스 이상의 데이터를 저장할 수 없지만 물리적인 한계를 뛰어넘기 위해 샤드라는 개념을 도입했다. 이를 이용하면 데이터를 분산 저장하는 가능하다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 5개의 샤드로 데이터가 분산되도록 생성되고 설정에 의해 샤드의 개수를 원하는 만큼 변경할 수도 있다. 하나의 샤드는 인덱스의 부분 집합이다. 하지만 해당 부분집합으로만으로도 독립적인 검색 서비스가 가능하다. 실제로 인덱스에 질의를 요청하면 인덱스가 가지고 있는 모든 샤드로 검색요청을 보내고 각 샤드의 결과를 취합하여 하나의 결과로 제공한다.

 

6)레플리카

샤드의 복제본을 레플리카라고 한다. 엘라스틱서치에서는 인덱스를 생성할 때 기본적으로 1개의 레플리카를 생성한다. 장애 복구만이 아니고 검색에도 활용되기 때문에 이를 이용하면 읽기 분산에 유리해진다. 엘라스틱서치는 노드의 장애시 페일오버 메커니즘을 레플리카를 이용하여 제공하고 있다. 인덱스가 생성될 때 샤드 개수와 레플리카 개수를 자유롭게 설정할 수 있다. 하지만 인덱스가 생성된  이후에는 샤드 개수를 변경하는 것이 불가능하다. 만약 샤드의 개수를 늘리고 싶다면 샤드개수를 늘린 인덱스를 새로 만들고 기존 인덱스에서 새로 생성한 인덱스로 ReIndex하는 방법 밖에 없다. 데이터가 아주 크다면 Reindex 시간이 아주 길기 때문에 애초에 최적의 샤드의 개수를 정하는 것이 중요하다. 이에 반해 레플리카 개수는 인덱스를 생성한 후에도 자유롭게 변경하는 것이 가능하다.

 

6)-1 엘라스틱서치의 고가용성

엘라스틱서치에서는 샤드나 노드에 장애가 발생할 경우 즉각적인 복구가 가능하기 때문에 안정적인 클러스터 운영이 가능하다. 페일오버 메커니즘을 레플리카를 이용하기 때문에 원본 샤드가 존재하지 않는 노드에 레플리카 샤드를 생성한다. 또한 검색 시 샤드와 레플리카에서 병렬로 실행될 수 있기 때문에 검색 성능이 좋아지는 결과도 있다.

 

7)세그먼트

문서들은 빠른 검색에 유리하도록 설계된 특별한 자료구조로 저장된다. 루씬에 데이터가 색인되면 데이터는 토큰 단위로 분리되고 특수한 형태의 세그먼트라는 단위로 저장이 된다. 이러한 세그먼트는 읽기에 최적화된 역색인이라는 형태로 변환되어 물리적인 디스크에 저장된다. 검색엔진의 특성상 쓰기 연산보다 읽기 연산의 비중이 비교적 높기때문에 읽기에 최적화된 역색인이라는 구조로 저장하는 것이다.

 

 

엘라스틱서치와 RDB비교

엘라스틱서치 RDB
인덱스 데이터베이스
샤드 파티션
타입 테이블
문서
필드
매핑 스키마
Query DSL SQL

 

엘라스틱서치는 루씬 라이브러리를 샤드 내부에 가지고 있으며, 이 루씬 라이브러리가 핵심 모듈이라고 설명했다. 루씬은 검색 라이브러리이고 이 라이브러리에서 중요한 클래스가 바로 IndexWriter & IndexSearcher이다. 간단히 전자는 데이터를 색인하는 역할이고 후자는 검색하는 역할이다. 이 두개를 가지고 색인과 검색 역할을 제공하는 루씬 인스턴스를 루씬 인덱스라고 하는데, 사실 하나의 엘라스틱서치 샤드는 하나의 루씬 인덱스라고 설명할 수 있다. 즉, 샤드가 독립적으로 검색을 제공하는 이유이기도 하다. 이말은 각 샤드마다 데이터를 위한 물리적인 파일을 가지게 될 것이다. 이것을 간단히 표로 표현하면

 

 

엘라스틱서치 인덱스 구조

Elasticsearch Index
Elasticsearch shard Elasticsearch shard Elasticsearch shard Elasticsearch shard
Lucene Index Lucene Index Lucene Index Lucene Index
segment segment segment segment segment segment segment segment

 

루씬 인덱스는 독립적으로 자기가 가지고 있는 세그먼트 내에서만 검색이 가능하다. 하지만 엘라스틱서치는 이러한 루씬 인덱스를 가진 샤드들을 하나의 엘라스틱서치 인덱스로 묶여 있으므로, 다수의 루씬 인덱스에서 동시에 검색이 가능한 것처럼 기능을 제공하게 되는 것이다.

 

 

색인 작업 시 세그먼트의 동작 방식

하나의 루씬 인덱스는 내부적으로 다수의 세그먼트로 구성돼 있다. 읽기 성능이 중요한 검색엔진에서는 하나의 세그먼트로 검색 요청을 처리하는 것보다 다수의 세그먼트를 생성해서 나눠서 처리하는 것이 훨씬 효율적일 것이다. 루씬은 검색 요청을 받으면 다수의 작은 세그먼트 조각들이 각각 검색 결과 조각을 만들어 내고 이를 통합하여 하나의 결과로 합쳐서 응답하도록 설계돼 있다. 이러한 검색 방식을 세그먼트 단위 검색이라고 한다. 세그먼트는 역색인 구조를 지닌 파일 자체를 의미하는데 세그먼트 내부에는 실제로 색인된 데이터가 역색인 구조로 저장돼있다.

루씬은 세그먼트들을 관리하기 위한 용도로 커밋 포인트라는 자료구조를 제공한다. 커밋 포인트는 여러 세그먼트의 목록 정보를 가지고 있으며, 검색 요청 시 이를 적극적으로 활용한다. 최초 색인 작업 요청이 루씬에 들어오면 IndexWriter에 의해 색인 작업이 이루어지고 결과물로 하나의 세그먼트가 생성된다. 그 후 색인 작업이 추가로 요청될 때마다 새로운 세그먼트가 추가로 생성되고 커밋 포인트에 기록된다. 즉, 색인 작업이 일어날 때마다 세그먼트 개수는 점점 늘어난다. 하지만 너무 많은 세그먼트가 생성되면 읽기 성능이 저하될 수 있기 때문에 루씬은 백그라운드에서  주기적으로 세그먼트 파일을 병합하는 작업을 수행하고 이를 통해 모든 세그먼트들을 물리적으로 하나의 파일로 병합한다.

즉, 일정시간이 지나고 추가 색인 작업이 없는 상태라면 최종적으로 하나의 커다란 세그먼트만 남는다. 

색인 작업이 계속 될때마다 세그먼트는 계속 추가적으로 생성된다. 이말은 즉, 세그먼트는 불변성을 가지게 되고 병합과정 이외에는 절대 수정되지 않는다.

 

루씬의 색인 동작과정

  • 최초 색인 요청 - 1) IndexWriter가 세그먼트를 생성 -> 2)IndexSearch가 생성된 세그먼트를 읽어 검색결과 제공
  • 추가 색인 요청 - 1) IndexWriter가 세그먼트를 추가 생성 -> 2)세그먼트가 추가 생성되는 동안 기존 세그먼트만 읽어 검색 결과 제공 -> 3)세그먼트 생성이 완료되면 생성된 모든 세그먼트를 읽어 검색 결과 제공
  • 주기적으로 세그먼트 병합 작업이 일어날 경우 - 1)IndexWriter가 병합 대상이 되는 세그먼트들을 복제 -> 2)IndexWriter가 복제한 세그먼트들을 하나의 세그먼트로 병합 -> 3) 병합 과정 중에는 기존 원본 세그먼트로 검색 결과 제공 -> 4) 병합작업 완료시 원본 세그먼트와 병합 세그먼트를 교체하고 원본 세그먼트 삭제 

 

여기까지 내용을 끊고 다음 포스팅에서 이어서 설명합니다.

posted by 여성게
: