파이프라인 집계(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 여성게
:

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

 

엘라스틱서치 노드의 종류

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

 

이번에 포스팅할 내용은 More Like This 입니다. More Like This 쿼리란 주어진 질의문과 가장 유사한 문서를 찾는 알고리즘입니다. 그리고 해당 알고리즘을 사용하지 위해서는 쿼리를 날릴 필드가 인덱싱되어야하며 분석 속도를 높이기 위하여 term_vector 속성을 지정해주는 것이 좋습니다. 필자를 참고로 간단한 Q&A를 위한 챗봇을 만들기 위하여 해당 알고리즘을 이용하였습니다. 물론 문맥을 이해하는 것이 아니라 유사도를 판단하는 것이지만 나름 성능이 나오는 알고리즘입니다. 모든 예제는 이전에 이용하였던 High Level Rest Client를 이용하였습니다.

 

Index

한글형태소 분석기가 포함된 인덱스를 생성하는 코드입니다. 참고로 질의의 대상이 되는 "question"필드의 term_vector 속성을 "yes"로 지정하였습니다. 생성 후에 몇개 예제로 문서를 색인해주세요.

 

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@RequestMapping("/create")
    public Object createIndex() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            //index name
            String indexName = "sample-korean-index";
            //type name
            String typeName = "_doc";
            
            //settings
            XContentBuilder settingsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("number_of_shards",5)
                        .field("number_of_replicas",1)
                        
                        .startObject("analysis")
                            .startObject("tokenizer")
                                .startObject("sample-nori-tokenizer")
                                    .field("type","nori_tokenizer")
                                    .field("decompound_mode","mixed")
                                    .field("user_dictionary","user_dictionary.txt")
                                .endObject()
                            .endObject()
                            
                            .startObject("analyzer")
                                .startObject("sample-nori-analyzer")
                                    .field("type","custom")
                                    .field("tokenizer","sample-nori-tokenizer")
                                    .array("filter",new String[]{
                                        "sample-nori-posfilter",
                                        "nori_readingform",
                                        "sample-synonym-filter",
                                        "sample-stop-filter"
                                        }
                                    )
                                .endObject()
                            .endObject()
                            
                            .startObject("filter")
                                .startObject("sample-nori-posfilter")
                                    .field("type","nori_part_of_speech")
                                    .array("stoptaags",new String[] {
                                            "E","IC","J","MAG","MM","NA","NR","SC",
                                            "SE","SF","SH","SL","SN","SP","SSC","SSO",
                                            "SY","UNA","UNKNOWN","VA","VCN","VCP","VSV",
                                            "VV","VX","XPN","XR","XSA","XSN","XSV"
                                        }
                                    )
                                .endObject()
                                
                                .startObject("sample-synonym-filter")
                                    .field("type","synonym")
                                    .field("synonyms_path","synonymsFilter.txt")
                                .endObject()
                                
                                .startObject("sample-stop-filter")
                                    .field("type","stop")
                                    .field("stopwords_path","stopFilter.txt")
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject();
            
            //mapping info
            XContentBuilder indexBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(typeName)
                    .startObject("properties")
                        .startObject("question")
                            .field("type","text")
                            .field("analyzer","sample-nori-analyzer")
                            .field("term_vector","yes")
                        .endObject()
                        .startObject("answer")
                            .field("type","keyword")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();
            
            //인덱스생성 요청 객체
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //세팅 정보
            request.settings(settingsBuilder);
            //매핑 정보
            request.mapping(typeName, indexBuilder);
            
            //별칭설정
            String aliasName = "chatbotInstance";
            request.alias(new Alias(aliasName));
            
            //인덱스생성
            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
            
        }catch (Exception e) {
            e.printStackTrace();
            return "인덱스 생성에 실패하였습니다. - catch";
        }
        
        
        return acknowledged == true ? "인덱스가 생성되었습니다.":"인덱스생성에 실패하였습니다.";
    }
cs

 

More Like This Query

 

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
@RequestMapping("/mlt")
    public Object mlt() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.moreLikeThisQuery
                (new String[] {"question"},new String[]{"사용자 발화 입니다. "},null)
                .minTermFreq(2)
                .minDocFreq(1)
                );
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
    private class Answer{
        private String question;
        private String answer;
        private String score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public String getScore() {
            return score;
        }
        public void setScore(String score) {
            this.score = score;
        }
    }
cs

 

파라미터 설명

 

Parameters

 

Document Input Parameters

  - like

    작성된 문서 또는 text를 바탕으로 문서를 검색 합니다.

  - unlike

    작성된 문서 또는 text에서 제외 시킬 term을 작성 합니다.

  - fields

    문서에서 analyzed text 를 가져올 필드를 지정 합니다.

    이 필드를 대상으로 질의 수행이 이루어 집니다.

  - like_text

    like 와 더불어 문서를 검색 할떄 추가적으로 사용할 text를 작성 합니다.

  - ids or docs

    @deprecated

 

Term Selection Parameters

  - max_query_terms

    작성된 문서 또는 text에서 추출하여 사용할 최대 query term size 입니다. (default 25)

  - min_term_freq

    작성된 문서 또는 text의 최소 TF 값으로 이 값보다 작을 경우 작성된 문서와 text는 무시 됩니다. (default 2)

  - min_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최소 크기로 해당 크기 보다 작은 term의 결과는 무시 됩니다. (default 5)

  - max_doc_freq

    입력된 개별 term들에 대해서 각각 matching 된 문서의 최대 크기로 해당 크기 보다 큰 term의 결과는 무시 됩니다. (default unbounded 0)

  - min_word_length

    입력된 개별 term들의 최소 길이로 정의한 값보다 작은 term은 무시 됩니다. (default 0)

  - max_word_length

    입력된 개별 term들의 최대 길이로 정의한 값보다 큰 term은 무시 됩니다. (default unbounded 0)

  - stop_words

    불용어 목록을 등록 합니다.

  - analyzer

    입력한 문서와 text에 대한 analyzer 를 지정 합니다. 지정 하지 않을 경우 first field 의 analyzer 를 사용하게 됩니다.

 

Query Formation Parameters

  - minimum_should_match

    작성된 문서 또는 text에서 추출된 term matching 에 대한 minimum_should_match 정보를 구성 합니다. (default 30%)

  - boost_terms

    tems boost value 를 지정 합니다.

  - include

    검색 결과로 입력 문서를 포함 할지 말지를 결정 합니다. (default false)

  - boost

    전체 질의에 대한 boost value 를 지정 합니다. (default 1.0)

 

posted by 여성게
:

자바 언어를 위해 제공되는 클라이언트에는 두 가지 종류가 있다. 내부적으로 HTTP REST API를 사용해 통신하는 방식과 네티(Netty)모듈을 이용해 네이티브 클라이언트를 통해 통신하는 방식이다.

 

REST Client Transport Client(Netty)
  • Java High Level REST Client라고 불린다.
  • HTTP방식을 이용해 엘라스틱서치와 통신한다.
  • 내부적으로는 HttpClient 모듈을 사용한다.
  • HTTPS 사용이 가능하다.
  • Java Client라고도 불린다.
  • 초기부터 제공되던 클라이언트 방식으로, 상대적으로 빠른 속도를 보장한다.
  • 소켓을 이용해 엘라스틱서치와 통신한다.
  • 내부적으로는 Netty모듈을 사용한다.

 

초기버전의 엘라스틱서치에서는 소켓을 이용하는 Transport 클라이언트만 제공됬다. 일종의 엘라스틱서치 노드와 비슷한 방식으로 동작하기 때문에 속도는 빠르지만 엘라스틱서치가 버전업될 때마다 제공되는 기능이나 API의 명세에 따라 클래스나 메서드가 바뀌는 문제점이 있었다. 이러한 문제점을 해결하기 위해서 새로운 모듈인 REST 클라이언트가 도입됐다.

 

아직까지 Transport방식이 많이 사용되기는 하지만 엘라스틱서치 7.0부터는 해당 방식이 deprecated됬으므로, REST 클라이언트 방식을 사용하는 것이 나을 듯 싶지만, 최근 버전에서는 어떻게 될지 모르겠지만, REST 클라이언트에 지원이 안되는 Transport방식의 기능도 있으므로 용도에 따라 적절히 혼합하여 사용해야할 듯하다. 하지만 이번 포스팅에서는 High-Level Rest Client만 다룰 예정이다.

 

 

Elasticsearch connection

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                    new HttpHost("host",9200,"http")
                    ,new HttpHost("host2",9200,"http")
                )
        );
        
        /*
         * 비지니스로직
         */
        
        /*
         * 사용후 꼭 close
         */
        client.close();
cs

 

High-Level Rest Client는 위와 같이 RestHighLevelClient 객체로 엘라스틱서치 노드와 연결한다. 그리고 매개변수로 빌더패턴 객체가 들어간다. 엘라스틱서치는 위와같이 여러개의 노드 연결이 가능하다. 중요한것은 반드시 사용후 close해주어야 한다는 것이다. 혹은 아래와 같이 자바1.7에 나온 기능인 try 괄호 안에 넣어주어서 auto close하는 방법을 사용해도 될듯하다.(try-resource-with) 따로 close를 호출해도 않아도 되니, 해당 기능은 Closeable을 구현하고 있는 클래스라면 습관적으로 써주는 것이 좋을 듯하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
        try(RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                    new HttpHost("host",9200,"http")
                    ,new HttpHost("host2",9200,"http")
                )
        );){
            /*
             * 비지니스로직
             */
        }
cs

 

우선 앞에서 대충 이야기는 했지만 현재 최신버전은 모르지만 최소한 6버전대에선 transport에서 지원하는 모든 기능을 high-level rest client에서 모두 지원하지는 않는다. 하지만 사용에 크게 무리없는 대부분의 기능은 지원하니 오늘은 그러한 기능 위주로 다루어볼 것이다.

그리고 편의상 모든 요청은 GET 방식으로 만들었다. 실 서비스 목적으로 만드려면 용도에 맞는 METHOD 방식으로 만드는 것이 좋을 듯싶다.

 

 

인덱스 생성 API

직접 postman tool이나 curl로 요청했었던 인덱스 생성 API를 클라이언트를 통하여 생성해볼 것이다. 생성할 인덱스는 커스텀 한글 형태소분석기 설정을 포함하며 매핑정보를 가진 인덱스 생성 API이다. 우선 이전에 설정하였던 것을 JSON으로 표현해본 것이다.

 

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
PUT -> http://localhost:9200/sample-index
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform",
                        "synonym_filtering",
                        "stop_filtering"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E","IC","J","MAG","MM","NA","NR","SC",
                        "SE","SF","SH","SL","SN","SP","SSC","SSO",
                        "SY","UNA","UNKNOWN","VA","VCN","VCP","VSV",
                        "VV","VX","XPN","XR","XSA","XSN","XSV"
                    ]
                },
                "synonym_filtering":{
                    "type":"synonym"
                    ,"synonyms_path":"synonymsFilter.txt"
                },
                "stop_filtering":{
                    "type":"stop"
                    ,"stopwords_path":"stopFilter.txt"
                }
            }
        }
    },"mappings":{
        "_doc":{
            "properties":{
                "answer":{
                    "type":"keyword"
                }
                ,"question":{
                    "type":"text"
                    ,"analyzer":"nori_analyzer"
                }
            }
        }
    }
}
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
 
    @RequestMapping("/create")
    public Object createIndex() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            //index name
            String indexName = "sample-korean-index";
            //type name
            String typeName = "_doc";
            
            //settings
            XContentBuilder settingsBuilder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("number_of_shards",5)
                        .field("number_of_replicas",1)
                        
                        .startObject("analysis")
                            .startObject("tokenizer")
                                .startObject("sample-nori-tokenizer")
                                    .field("type","nori_tokenizer")
                                    .field("decompound_mode","mixed")
                                    .field("user_dictionary","user_dictionary.txt")
                                .endObject()
                            .endObject()
                            
                            .startObject("analyzer")
                                .startObject("sample-nori-analyzer")
                                    .field("type","custom")
                                    .field("tokenizer","sample-nori-tokenizer")
                                    .array("filter",new String[]{
                                        "sample-nori-posfilter",
                                        "nori_readingform",
                                        "sample-synonym-filter",
                                        "sample-stop-filter"
                                        }
                                    )
                                .endObject()
                            .endObject()
                            
                            .startObject("filter")
                                .startObject("sample-nori-posfilter")
                                    .field("type","nori_part_of_speech")
                                    .array("stoptaags",new String[] {
                                            "E","IC","J","MAG","MM","NA","NR","SC",
                                            "SE","SF","SH","SL","SN","SP","SSC","SSO",
                                            "SY","UNA","UNKNOWN","VA","VCN","VCP","VSV",
                                            "VV","VX","XPN","XR","XSA","XSN","XSV"
                                        }
                                    )
                                .endObject()
                                
                                .startObject("sample-synonym-filter")
                                    .field("type","synonym")
                                    .field("synonyms_path","synonymsFilter.txt")
                                .endObject()
                                
                                .startObject("sample-stop-filter")
                                    .field("type","stop")
                                    .field("stopwords_path","stopFilter.txt")
                                .endObject()
                            .endObject()
                        .endObject()
                    .endObject();
            
            //mapping info
            XContentBuilder indexBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject(typeName)
                    .startObject("properties")
                        .startObject("question")
                            .field("type","text")
                            .field("analyzer","sample-nori-analyzer")
                        .endObject()
                        .startObject("answer")
                            .field("type","keyword")
                        .endObject()
                    .endObject()
                .endObject()
            .endObject();
            
            //인덱스생성 요청 객체
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //세팅 정보
            request.settings(settingsBuilder);
            //매핑 정보
            request.mapping(typeName, indexBuilder);
            
            //별칭설정
            String aliasName = "chatbotInstance";
            request.alias(new Alias(aliasName));
            
            //인덱스생성
            CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
            
        }catch (Exception e) {
            e.printStackTrace();
            return "인덱스 생성에 실패하였습니다. - catch";
        }
        
        
        return acknowledged == true ? "인덱스가 생성되었습니다.":"인덱스생성에 실패하였습니다.";
    }
}
 
cs

 

이미 생성된 인덱스명이 존재해서 각종 이름만 변경된 것을 제외하면 동일한 설정값을 갖는다. 그리고 마지막에 인덱스 별칭을 주기 위하여 alias를 추가하였다. XContentBuilder가 우리가 JSON으로 표현할 설정 정보를 세팅하는 객체라고 보면 된다. 빌더패턴을 마치 JSON과 비슷한 느낌으로 사용하고 있으므로 설정에 어려운 부분은 없다고 생각이 든다. 마지막으로 CreateIndexRequest 객체를 이용하여 인덱스 생성 정보를 담은 요청객체를 만들고 해당 객체에 settings정보와 mapping정보 그리고 alias설정등을 넣어주고 마지막으로 CreateIndexResponse 객체로 요청의 결과를 받아온다. 처음에도 강조하였지만 중요한 것은 RestHighLevelClient을 닫아주는 것이다. 여기서는 try-resource-with 구문을 사용하여 자동으로 닫아주었다.

 

 

인덱스 삭제 API

 

1
DELETE -> http://localhost:9200/sample-korean-index
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
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    @RequestMapping("/delete")
    public Object delete() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection()
        ){
            
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 삭제 요청 객체
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            
            DeleteIndexResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 삭제에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스 삭제가 완료되었습니다.":"인덱스 삭제에 실패하였습니다.";
    }
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
}
 
cs

 

인덱스 open/close

운영중인 인덱스의 설정 정보를 변경하려면 다음과 같은 프로세스를 거쳐야한다.

인덱스 close -> 설정변경 요청 -> 인덱스 open 

 

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
POST -> http://localhost:9200/sample-korean-index/_close
POST -> http://localhost:9200/sample-korean-index/_open
 
@RestController
@RequestMapping("/es/index")
public class IndexAPIController {
    
    @RequestMapping("/open")
    public Object open() {
        
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection();
        ){
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 open
            OpenIndexRequest request = new OpenIndexRequest(indexName);
            
            OpenIndexResponse response = client.indices().open(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 open에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스를 open 하였습니다.":"인덱스 open에 성공하였습니다.";
    }
    
    @RequestMapping("/close")
    public Object close() {
        boolean acknowledged = false;
        
        try(
                RestHighLevelClient client = createConnection();
        ){
            //index name
            String indexName = "sample-korean-index";
            
            //인덱스 close
            CloseIndexRequest request = new CloseIndexRequest(indexName);
            
            CloseIndexResponse response = client.indices().close(request, RequestOptions.DEFAULT);
            
            acknowledged = response.isAcknowledged();
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "인덱스 close에 실패하였습니다. - catch";
        }
        
        return acknowledged == true ? "인덱스를 close 하였습니다.":"인덱스 close에 성공하였습니다.";
    }
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
}
 
cs

 

직접 엘라스틱서치에 요청을 보낼때는 맨위 두개의 요청이다. 해당 요청들을 클라이언트를 이용하여 요청하면 아래 소스코드와 같아진다.

 

 

문서 생성

문서 색인 요청이다. 색인 요청에는 인덱스명, 타입명, 문서 ID가 필요하다. 필자가 검색엔진을 이용하여 개발을 하였을 때는 문서 ID는 따로 RDB에서 시퀀스로 관리하였고 DB에서 다음 ID 값을 불러와 문서를 생성하였었다. 예제는 해당 내용이 포함되어 있지 않지만 나중에 활용하면 좋을 듯 하다.

 

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
POST -> http://localhost:9200/sample-korean-index/_doc/2
 
{
    "question":"사용자 발화 예상문입니다.",
    "answer":"사용자에게 보여질 답변 혹은 의도명입니다."
}
 
 
<Client Code>
 
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서추가
     */
    @RequestMapping("/create")
    public Object create() {
        
        
        try(RestHighLevelClient client = createConnection();){
            
            //인덱스명 대신 별칭으로 색인한다.
            String indexName = "sample-korean-index";
            //타입명
            String typeName = "_doc";
            
            //문서 ID(대게 RDB에 문서 키값정도는 가져와서 저장한다. 만약 자동생성을 하게 된다면 엘라스틱서치는 UUID로 ID를 생성)
            String docId = "DOC_1";
            
            //문서 색인
            IndexRequest request = new IndexRequest(indexName,typeName,docId);
            
            request.source(
                XContentFactory.jsonBuilder()
                    .startObject()
                        .field("question","사용자 발화 예상문입니다.")
                        .field("answer","사용자에게 보여질 답변 혹은 의도명입니다.")
                    .endObject()
            );
            
            IndexResponse response = client.index(request, RequestOptions.DEFAULT);
            
            return response.status() ;
            
        }catch (ElasticsearchException e) {
            // TODO: handle exception
            e.printStackTrace();
            if(e.status().equals(RestStatus.CONFLICT)) {
                return "동일한 DOC_ID 문서가 존재합니다.";
            }
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "문서 생성에 실패하였습니다.";
        }
        
        return null;
    }
}
 
 
cs

 

만약 응답결과로 "CREATED"가 오면 정상적으로 문서색인이 완료된 것이다. 예외처리의 CONFLICT는 문서 아이디가 중복된 것이 존재할 경우에 예외 발생을 시키기위한 처리이다.

 

 

문서조회

문서의 ID값을 이용해 조회하는 예제이다.

 

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
GET -> http://localhost:9200/chatbotInstance/_doc/DOC_1
 
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 조회
     */
    @RequestMapping("/search")
    public Object search() {
        
        //인덱스 별칭
        String aliasName = "chatbotInstance";
        //문서 타입
        String typeName ="_doc";
        //문서 ID
        String docId = "DOC_1";
        
        //문서조회 요청
        GetRequest request = new GetRequest(aliasName,typeName,docId);
        
        //문서조회 결과
        GetResponse response = null;
        try(RestHighLevelClient client = createConnection();){
            
            response = client.get(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
            return "문서조회에 실패하였습니다.";
        }
        Map<String, Object> sourceAsMap = null;
        if(response.isExists()) {
            long version = response.getVersion();
            sourceAsMap = response.getSourceAsMap();
        }
        
        return gson.toJson(sourceAsMap);
    }
}
 
cs

 

문서존재여부

문서가 존재하는 지 여부를 알기위한 exist query이다. 결과는 true & false 이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 존재 여부, exist
     */
    @RequestMapping("/exist")
    public Object exist() {
        
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        GetRequest request = new GetRequest(aliasName,typeName,docId);
        boolean exist = false;
        try(RestHighLevelClient client = createConnection();){
            
            exist = client.exists(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
        }
        
        return exist;
    }
}
 
cs

 

문서 삭제

문서삭제 요청이다. 결과값으로 "DELETED"가 나온다면 성공적으로 삭제된 것이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서삭제
     */
    @RequestMapping("/delete")
    public Object delete() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        DeleteRequest request = new DeleteRequest(aliasName,typeName,docId);
        DeleteResponse response = null;
        try(RestHighLevelClient client = createConnection();){
            
            response = client.delete(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

문서 업데이트

문서 업데이트 요청은 여러가지 종류가 존재한다. 스크립트를 이용한 업데이트, Upsert, 부분 문서 수정 등 요구사항에 맞는 업데이트 기능을 사용하면 될듯하다.

 

문서 업데이트 - script

스크립트를 이용하여 문서를 수정하는 예제이다. 쿼리를 이용하여 매치된 문서에 대한 일괄수정도 가능하고 예제처럼 특정 문서 ID의 문서만 수정가능하다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - script
     */
    @RequestMapping("/update-1")
    public Object update_1() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        UpdateRequest request = new UpdateRequest(aliasName, typeName, docId);
        Map<String, Object> parameters = Collections.singletonMap("string"" - update");
        
        Script inline = new Script(ScriptType.INLINE,"painless","ctx._source.answer += params.string",parameters);
        
        request.script(inline);
        
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - 부분문서수정
     */
    @RequestMapping("/update-2")
    public Object update_2() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_1";
        
        
        
        UpdateRequest request = null;
        
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("answer","수정된 답변입니다.")
                    .endObject();
            
            request = new UpdateRequest(aliasName, typeName, docId).doc(builder);
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

문서 업데이트 - upsert

만약 문서가 존재하면 update, 존재하지 않을 시 insert하는 요청이다. 만약 문서가 존재하지 않으면 IndexRequest에 담긴 Builder에 담긴 문서가 생성되고 문서가 존재한다면 UpsertRequest에 담긴 Builder 내용으로 수정된다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    /*
     * 문서 수정 - upsert
     */
    @RequestMapping("/update-3")
    public Object update_3() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        String docId = "DOC_3";
        
        
        IndexRequest index = null;
        UpdateRequest request = null;
        UpdateResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            index = new IndexRequest(aliasName,typeName,docId);
            index.source(XContentFactory.jsonBuilder()
                    .startObject()
                        .field("question","주문할게요")
                        .field("answer","어떤걸 주문하시게요?")
                    .endObject()
            );
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                        .field("answer","어떤걸 주문하시게요?")
                    .endObject();
            
            request = new UpdateRequest(aliasName, typeName, docId).doc(builder).upsert(index);
            
            response = client.update(request, RequestOptions.DEFAULT);
            
        }catch (ElasticsearchException e) {
            e.printStackTrace();
            if(e.status().equals(RestStatus.NOT_FOUND)) {
                return "수정할 문서가 존재하지 않습니다.";
            }
        }catch (Exception e) {
            /*
             * 기타 예외처리
             */
        }
        
        return response.getResult();
    }
}
 
cs

 

Bulk API

여러건의 문서를 Create,Update,Delete 할 수 있는 요청이다. 만약 여러개의 문서를 반복문으로 하나씩 요청하고 있다면 해당 기능을 이용하면 깔끔하게 하나의 요청으로 완료될 것이다.

 

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
@RestController
@RequestMapping("/es/doc")
public class DocAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    /*
     * Bulk API
     */
    @RequestMapping("/bulk")
    public Object bulk() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        BulkRequest request = new BulkRequest();
        BulkResponse response = null;
        
        try(RestHighLevelClient client = createConnection();){
            
            request.add(new IndexRequest(aliasName,typeName,"DOC_4")
                    .source(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add")
                                .field("answer","벌크 답변 - add")
                            .endObject()
                    )
            );
            
            request.add(new IndexRequest(aliasName,typeName,"DOC_5")
                    .source(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add2")
                                .field("answer","벌크 답변 - add2")
                            .endObject()
                    )
            );
            
            request.add(new UpdateRequest(aliasName,typeName,"DOC_4")
                    .doc(XContentFactory.jsonBuilder()
                            .startObject()
                                .field("question","벌크 질문 - add2")
                                .field("answer","벌크 답변 - add2")
                            .endObject()
                    )
            );
            
            request.add(new DeleteRequest(aliasName,typeName,"DOC_5"));
            
            response = client.bulk(request, RequestOptions.DEFAULT);
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        List<DocWriteResponse> results = new ArrayList<>();
        
        for(BulkItemResponse bulkItemResponse : response) {
            
            DocWriteResponse itemResponse = bulkItemResponse.getResponse();
            
            if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.INDEX)) {
                IndexResponse indexResponse = (IndexResponse)itemResponse;
                results.add(indexResponse);
            }else if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.UPDATE)) {
                UpdateResponse updateResponse = (UpdateResponse)itemResponse;
                results.add(updateResponse);
            }else if(bulkItemResponse.getOpType().equals(DocWriteRequest.OpType.DELETE)) {
                DeleteResponse deleteResponse = (DeleteResponse)itemResponse;
                results.add(deleteResponse);
            }
        }
        
        List<Object> resultList = results.stream().map(i->i.getResult()).collect(Collectors.toList());
        
        return Arrays.toString(resultList.toArray());
    }
}
 
cs

 

 

검색(Search) API

검색 API는 SearchRequest 클래스를 이용해 검색 요청을 할 수 있다. 쿼리에는 Tranport 클라이언트에서 사용하던 Query DSL 클래스를 동일하게 사용할 수 있다.

 

match-all Query

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/match-all")
    public String matchAll(){
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
    }
}
 
cs

 

Full Text Query API - Match Query

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/match")
    public String match(){
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchQuery(fieldName, "질문"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
            
        }catch (Exception e) {
            /*
             * 예외처리
             */
            e.printStackTrace();
        }
        
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
    }
}
 
cs

 

common term query

많이 검색되는 단어와 적게 검색되는 단어 중 어떤 단어가 더 중요한지를 판단해서 검색 스코어링을 변경하는 알고리즘을 가지고 있는 쿼리이다. 예를 들어 "the red fox"라는 문장이 있을 경우 the,red,fox라는 텀으로 문장을 자르고 검색할때 각각을 검색해서 최종적으로 스코어를 계산한다. the라는 단어는 너무 흔한 단어로서 맨 나중에 검색되도록 가중치를 조절할 수 있다.

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    private Gson gson = new Gson();
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/commonTerm")
    public Object commmonTerm() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.commonTermsQuery(fieldName, "질문"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return gson.toJson(resultMap); 
    }
    
    private class Answer{
        private String question;
        private String answer;
        private float score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public float getScore() {
            return score;
        }
        public void setScore(float score) {
            this.score = score;
        }
    }
}
 
cs

 

Query String Query

검색할 때 연산자를 따로 지정하는 것이 아니라 쿼리문 자체에 AND, OR 절을 사용하고 싶을때 사용하는 쿼리이다. 혹은 특정 키워드를 필수 조건으로 빼거나 넣거나 할 수도 있다.

 

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
@RestController
@RequestMapping("/es/search")
public class SearchAPIController {
    
    
    /*
     * connection create method
     */
    public RestHighLevelClient createConnection() {
        return new RestHighLevelClient(
                    RestClient.builder(
                            new HttpHost("127.0.0.1",9200,"http")
                    )
                );
    }
    
    @RequestMapping("/queryString")
    public Object queryString() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        String question = "+질문 -벌크";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//        searchSourceBuilder.query(QueryBuilders.queryStringQuery("질문 OR 벌").field(fieldName));
        searchSourceBuilder.query(QueryBuilders.queryStringQuery(question).field(fieldName));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
    
    private class Answer{
        private String question;
        private String answer;
        private String score;
        
        public String getQuestion() {
            return question;
        }
        public void setQuestion(String question) {
            this.question = question;
        }
        public String getAnswer() {
            return answer;
        }
        public void setAnswer(String answer) {
            this.answer = answer;
        }
        public String getScore() {
            return score;
        }
        public void setScore(String score) {
            this.score = score;
        }
    }
}
 
cs

 

Term Query

지정된 필드에 정확한 텀이 들어있는 문서를 찾을 때 사용한다. Keyword 타입으로 설정된 필드에서만 검색가능하다.

 

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
@RequestMapping("/term")
    public Object term() {
        String aliasName = "chatbotInstance";
        String fieldName = "question";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.termQuery("keywordFiled","keyword"));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
cs

 

Bool Query

must, mustNot, should, filter 등을 조합해서 쿼리를 구성한다.

 

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
@RequestMapping("/bool")
    public Object bool() {
        String aliasName = "chatbotInstance";
        String typeName = "_doc";
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("keywordField""value"))
                .mustNot(QueryBuilders.termQuery("keywordField2""value2"))
                .should(QueryBuilders.termQuery("keywordField3""value3"))
                .filter(QueryBuilders.termQuery("keywordField4""value4")));
        searchSourceBuilder.from(0);
        searchSourceBuilder.size(5);
        searchSourceBuilder.sort(new FieldSortBuilder("answer").order(SortOrder.DESC));
        
        SearchRequest request = new SearchRequest(aliasName);
        request.types(typeName);
        request.source(searchSourceBuilder);
        
        
        SearchResponse response = null;
        SearchHits searchHits = null;
        List<Answer> resultMap = new ArrayList<>();
        
        try(RestHighLevelClient client = createConnection();){
            response = client.search(request, RequestOptions.DEFAULT);
            searchHits = response.getHits();
            for( SearchHit hit : searchHits) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                Answer a = new Answer();
                a.setQuestion(sourceAsMap.get("question")+"");
                a.setAnswer(sourceAsMap.get("answer")+"");
                resultMap.add(a);
            }
        }catch (Exception e) {
            e.printStackTrace();
            /*
             * 예외처리
             */
        }
        return resultMap; 
    }
cs

 

기타 많은 쿼리들을 있지만 이번 포스팅은 여기까지만 다룬다. 이번 포스팅의 목적은 엘라스틱서치의 기능이 목적은 아니고 엘라스틱서치 클라이언트를 적응하는 것이 목적이었다.

 

posted by 여성게
:

 

이번 포스팅에서 다루어볼 내용은 이전 포스팅에서 다룬 한글 형태소분석기를 이용한 기타 고급검색 기법에 대해 다루어볼 것이다. 물론 해당 고급검색에는 간략한 내용만 다루어보고 집계쿼리나 기타 유사도검색 기반 검색들은 다른 포스팅에서 다루어볼 것이다. 이번에 다루어볼 내용들이다.

 

  1. 검색 결과 하이라이팅
  2. 스크립트를 이용한 동적필드 추가
  3. 검색 템플릿을 이용한 동적 쿼리 제공
  4. 별칭을 이용하여 항상 최신 인덱스 유지하기
  5. 스냅샷을 이용한 백업과 복구

위 내용들을 다루어볼 것이다. 검색의 성능을 향상시키거나 그런 내용은 아니지만 알아두면 아주 유용한 기능들이기에 다루어볼 것이다.

 

 

검색결과 하이라이팅

하이라이팅은 문서 검색 결과에 사용자가 입력한 검색어를 강조하는 기능이다. 해당 기능을 통해 사용자가 입력한 검색어가 어디에 일치되는 지는 쉽게 눈으로 확인가능하다. 즉, 사용자의 편의를 위한 기능중 한가지라고 볼 수 있을 것이다.

 

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
POST http://localhost:9200/movie_search/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    },
    "highlight":{
        "fields":{
            "movieNm":{}
        }
    }
}
 
result =>
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 49,
        "max_score": 10.243236,
        "hits": [
            {
                "_index": "movie_search",
                "_type": "_doc",
                "_id": "cz3KqmkBjjM-ebDbBcoO",
                "_score": 10.243236,
                "_source": {
                    "movieCd": "2009A438",
                    "movieNm": "장미",
                    "movieNmEn": "",
                    "prdtYear": "",
                    "openDt": "",
                    "typeNm": "",
                    "prdtStatNm": "기타",
                    "nationAlt": "",
                    "genreAlt": "",
                    "repNationNm": "",
                    "repGenreNm": "",
                    "directors": [],
                    "companys": []
                },
                "highlight": {
                    "movieNm": [
                        "<em>장미</em>"
                    ]
                }
            }
        ]
    }
}
cs

 

크게 어렵지 않다. 위처럼 쿼리를 날리게 되면 결과로 highlight가 나오는 걸 볼 수 있다. 화면에 뿌려줄때에는 highlight부분을 검색결과 링크로 뿌려주어도 될 것 같다. 그리고 em 태그 말고 다른 태그로 하이라이팅을 하고 싶을 수도 있다. 그럴때에는 몇개의 옵션만 추가해주면된다.

 

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
POST http://localhost:9200/movie_search/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    },
    "highlight":{
        "pre_tags":["<string>"],
        "post_tags":["</string>"],
        "fields":{
            "movieNm":{}
        }
    }
}
 
result =>
{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 49,
        "max_score": 10.243236,
        "hits": [
            {
                "_index": "movie_search",
                "_type": "_doc",
                "_id": "cz3KqmkBjjM-ebDbBcoO",
                "_score": 10.243236,
                "_source": {
                    "movieCd": "2009A438",
                    "movieNm": "장미",
                    "movieNmEn": "",
                    "prdtYear": "",
                    "openDt": "",
                    "typeNm": "",
                    "prdtStatNm": "기타",
                    "nationAlt": "",
                    "genreAlt": "",
                    "repNationNm": "",
                    "repGenreNm": "",
                    "directors": [],
                    "companys": []
                },
                "highlight": {
                    "movieNm": [
                        "<em>장미</em>"
                    ]
                }
            }
        ]
    }
}
cs

 

위와 같이 pre_tags,post_tags 옵션을 주어 하이라이팅할 단어에 태그를 지정해줄 수도 있다.

 

 

스크립트를 이용하여 동적으로 필드 추가하기

엘라스틱서치는 스크립트를 이용해 사용자가 특정 로직을 삽입하는 것이 가능하다. 이러한 방식을 스크립팅이라고 한다. 스크립팅을 이용하면 두 개 이상의 필드 스코어를 하나로 합하거나 계산된 스코어를 특정 수식으로 재계산하는 등의 작업이 가능해진다. 사용자는 이를 활용해 검색 요청시 특정 필드를 선택적으로 반환하거나 필드의 특정 요소를 수정하는 등 광범위한 작업을 할 수 있다.

 

엘라스틱서치에서는 스크립팅을 지원하는 두가지 방식이 존재한다.

  1. config 폴더에 스크립팅을 저장하여 콜하는 방식
  2. 요청바디에 스크립트를 포함하는 방식

그리고 지금 엘라스틱서치에서는 스크립팅 전용 언어인 페인리스(painless)가 개발되어 사용된다. 엘라스틱서치는 기본적으로 업데이트를 허용하지 않는다. 재색인을 통해 설정한 _id의 문서를 삭제하고 다시 생성할 뿐이다. 하지만 _update API를 제공하고 있는데, 이는 내부적으로 스크립팅을 사용하여 업데이트가 이루어지는 것이다. 오늘 다루어볼 스크립트는 문서의 필드를 동적으로 추가하고 삭제하는 예제이다.

 

1
2
3
4
5
6
7
8
9
PUT http://localhost:9200/movie_script/_doc/1
 
{
    "movieList":{
        "Death_Wish":5.5,
        "About_Time":7,
        "Suits":3.5
    }
}
cs

 

 

인덱스 및 문서를 하나 생성해준다.

 

1
2
3
4
5
POST http://localhost:9200/movie_script/_doc/1/_update
 
{
    "script":"ctx._source.movieList.Black_Panther=3.7"
}
cs

 

위와 같이 _update API를 이용하여 내부적으로 스크립트를 작성해 존재하지 않는 필드를 하나 생성해준다. ctx._source는 스크립트에서 제공하는 특수한 문법이다. 이는 색인된 문서에 접근하기 위한 문법이라고 생각하면 된다.

 

1
2
3
4
5
POST http://localhost:9200/movie_script/_doc/1/_update
 
{
    "script":"ctx._source.movieList.remove(\"Suits\")"
}
cs

 

기존에 존재하고 있는 Suits라는 필드를 삭제한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET http://localhost:9200/movie_script/_doc/1
 
result =>
{
    "_index": "movie_script",
    "_type": "_doc",
    "_id": "1",
    "_version": 4,
    "found": true,
    "_source": {
        "movieList": {
            "Death_Wish": 5.5,
            "About_Time": 7,
            "Black_Panther": 3.7
        }
    }
}
cs

 

결과값으로 Suits 필드가 삭제되고 Black_Panther가 추가된 것을 볼 수 있다. 이와 같이 스크립트를 이용하면 필드를 동적으로 추가,제거할 수도 있고, 어떠한 결과값을 재정의 하는 것도 가능해진다.

 

 

검색 템플릿을 이용한 동적 쿼리 제공

검색 템플릿은 엘라스틱서치 1.1 버전에 추가된 오래된 기능이다. 하지만 이를 통해 복잡한 검색 로직을 템플릿화하여 저장하고 활용할 수 있기때문에 매우 유용하다. 검색 템플릿의 필드명과 파라미터를 사용해서 쿼리를 전송하고 템플릿에 제공한 파라미터로 실제 검색이 이뤄진다.즉, 검색 템플릿을 사용하여 클라이언트쪽 코드가 아주 간략해지는 것이다.

 

또한 클라이언트 프로그램을 열어 검색의 요구사항이 변경될때마다 코드를 수정하고 다시 배포하는 것이 아니라 엘라스틱서치에 저장돼 있는 템플릿의 기존 쿼리를 수정하고 새 쿼리를 작성하면 되므로 이점이 있다. 검색 템플릿은 Mustache라는 템플릿 엔진을 사용해서 표현된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST http://localhost:9200/_scripts/movie_search_template
 
{
    "script":{
        "lang":"mustache",
        "source":{
            "query":{
                "match":{
                    "movieNm":"{{movie_nm}}"
                }
            }
        }
    }
}
cs

 

검색 템플릿 하나를 생성해준다. 생성이 완료되면 밑의 요청을 보내 검색템플릿이 정상적으로 생성되었는지 정보를 확인한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET http://localhost:9200/_scripts/movie_search_template
 
result =>
{
    "_id": "movie_search_template",
    "found": true,
    "script": {
        "lang": "mustache",
        "source": "{\"query\":{\"match\":{\"movieNm\":\"{{movie_nm}}\"}}}",
        "options": {
            "content_type": "application/json; charset=UTF-8"
        }
    }
}
cs

 

이제는 해당 템플릿을 이용하여 검색을 해보자.

 

1
2
3
4
5
6
7
8
9
10
11
POST http://localhost:9200/movie_search/_doc/_search/template
 
{
    "id":"movie_search_template",
    "params":{
        "movie_nm":"그대"
    }
}
 
 
 
cs

 

아이디 값으로 검색 템플릿 이름을 주고 파라미터에 정의된 파라미터 명을 쓰고 값을 넣어주면 된다. 그리고 추후에 검색 템플릿의 변경이 생긴다면 생성과 동일한 요청으로 변경된 템플릿 요청을 보내주면 수정이 완료된다. 이렇게 클라이언트 코드의 수정이 없이도 쿼리의 변경이 가능하니 아주 편리한 기능이다.

 

 

**별칭을 이용해 항상 최신 인덱스 유지하기

엘라스틱서치 클러스터를 운영하는 중에 인덱스 매핑 설정이 변경되거나 인덱스가 깨진다면 기존에 생성된 인덱스를 삭제하고 다시 생성해야 할것이다. 그런데 운영중인 클러스터라면 인덱스 변경을 위하여 인덱스가 삭제되어있는 중에 사용자의 쿼리 요청을 받게되면 인덱스가 존재하지 않는다는 에러메시지를 인덱스가 다시 생성될 때까지 사용자는 보게 될 것이다. 이것이 운영중인 클러스터 상황에 나와야하는 상황일까? 절대 아니다. 절대 나와서는 안되는 상황인 것이다. 이러한 문제를 방지하기 위하여 엘라스틱서치에는 별칭(Alias)라는 기능을 제공한다. 이러한 기능은 Apache Solr에도 거의 동일하게 존재하는 기능이 있다. 필자는 솔라를 이용할때 해당 별칭기능을 이용하여 Active/Standby 컬렉션을 운영했던 경험이 있다. 아마 엘라스틱서치도 비슷한 구성으로 운영가능하지 않을까 생각한다. 그리고 하나의 인덱스에만 별칭을 줄 수 있는 것은 아니다. 여러개의 인덱스에 동일한 별칭을 주어 여러개의 인덱스를 하나의 별칭으로 검색 할 수 있도록 할 수도 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST http://localhost:9200/_aliases
 
{
    "actions":[
        {
            "add":{"index":"movie_search","alias":"movie_alias"}
        },
        {
            "add":{"index":"movie_search2","alias":"movie_alias"}
        }
    ]
}
 
 
 
cs

 

위와 같이 하나 이상의 인덱스를 동일한 별칭으로 설정할 수 있다. 멀티테넌시 기능을 이용하기 보다는 해당 별칭기능을 이용하는 편이 훨씬 나을 듯하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
POST http://localhost:9200/movie_alias/_search
 
{
    "size":1,
    "query":{
        "match":{
            "movieNm":"그대 장미"
        }
    }
}
 
 
 
cs

 

별칭으로 검색하는 예제이다. 이와같이 변경이 잦은 인덱스는 별칭을 이용하는 것이 좋다. 예를 들어 매일같이 변경되는 인덱스가 있다고 생각해보자. 그러면 인덱스명을 "인덱스명_타임스탬프" 형식으로 생성하고 다음날에는 이전 인덱스의 별칭을 삭제함과 동시에 새로운 인덱스에 별칭을 주면 사용자 입장에서는 변경되는 것을 알아차리지 못하고 별칭을 이용하여 새로운 인덱스에 요청을 보내게 된다. 이렇게 실무에서는 별칭을 이용하여 항상 최신의 인덱스를 유지하여 사용할 수 있다. 그리고 위에서 잠깐 이야기했지만, Active/StandBy 인덱스를 나누어서 사용해야할 일이 있다면 항상 Active에 별칭을 주고, Switch되는 이벤트가 발생하여 StandBy 인덱스가 Active로 가야한다면 별칭만 StandBy로 추가해주고 Active의 별칭을 삭제하면 Active<->StandBy 스위치가 이러나게 되는 것이다. 이렇게 별칭을 여러가지 용도로 사용가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
POST http://localhost:9200/_aliases
 
{
    "actions":[
        {
            "remove":{"index":"movie_search","alias":"movie_alias"}
        }
    ]
}
 
 
 
cs

 

마지막으로 별칭을 삭제하는 요청이다. 해당 요청에는 위와 동일하게 add,remove를 같이 배열로 요청을 보낼 수 있으므로, 인덱스 변경시에는 add와 remove를 동시에 요청을 보내면 될듯하다.

 

 

**스냅샷을 이용한 백업&복구

엘라스틱서치 클러스터를 관리하다 보면 항상 하게 되는 고민 중 하나인 데이터 백업 정책이다. 검색 서버가 항상 안정적이라는 보장은 없기때문에 항상 백업과 복구를 염려해야하는 것은 당연한 것이다. 단순히 데이터 소스를 제공하는 시스템에서 다시 데이터를 Full 색인하면 된다라는 생각을 버려야한다. 데이터가 아주 크다면 아주 오랜시간 동안 색인 작업이 이루어질 것이고, 그러면 그 시간동안 서비스는 불가능 할 것이다. 이러한 고민을 해결해주기 위해 엘라스틱서치는 _snapshot API를 제공해준다. 해당 기능을 이용하여 개별 인덱스를 백업할 수도 있고, 클러스터 전체를 스냅샷으로 만드는 것도 가능하다. 그리고 스냅 샷은 점진적으로 가져온다. 즉, 인덱스의 스냅 샷을 만들 때, Elasticsearch는 동일한 인덱스의 이전 스냅 샷의 일부로 이미 리포지토리에 저장된 데이터를 복사하지 않도록한다. 따라서 클러스터의 스냅 샷을 자주 가져 오는 것이 효율적일 수 있다.

 

1)백업 디렉토리 생성

스냅샷 기능을 이용하기 위해서는 스냅샷이 저장될 물리적인 디렉토리를 생성해주어야 한다.원하는 위치에 디렉토리를 하나 생성해준다.

 

2)백업 디렉토리 등록

엘라스틱서치가 물리적인 백업용 디렉토리를 인식할 수 있도록 elasticsearch.yml 파일에 설정해준다.

백업디렉토리는 위와 같이 배열로 여러 경로를 지정해줄 수 있다. 용도별 스냅샷 디렉토리를 구분할 필요가 있다면 여러개 설정해주어도 될듯하다. 엔진을 재시작한다.

 

3)레포지토리 생성

다음은 스냅샷들을 저장하는 논리적인 저장공간을 만들것이다. 이를 레포지토리라고 부른다. 레포지토리는 설정 내에 물리적인 디렉토리 내부를 저장소로 이용한다. 또한 레포지토리를 생성할 때 다양한 설정 값을 지정할 수 있다. 스냅샷을 백업&복구하는 작업은 시스템 리소스를 많이 사용하기에 몇가지 옵션을 잘 조합해서 사용하는 것이 좋다. 즉, 해당 기능은 엘라스틱서치가 파일시스템을 백업과 복구를 이용하기 위해 등록하는 과정이라고 보면된다.

 

매개변수 설정
location 스냅샷의 물리적인 저장 경로설정.
compress 스냅샷 생성이 압축 수행여부. 이때 데이터 자체는 압축되지 않고 메타데이터만 압축 대상이 된다.
chunk_size 생성되는 파일을 특정 크기로 나누어서 생성할 수 있다. 기본적으로 스냅샷은 하나의 파일로 생성된다.
max_restore_bytes_per_sec 스냅샷 복원 시 속도를 설정한다. 기본적으로 초당 40MB의 속도이다.
max_snapshot_bytes_per_sec 스냅샷 생성(백업)시 속도를 설정한다. 기본적으로 초당 40MB의 속도이다.
readonly 레포지토리를 읽기 전용으로 생성한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
PUT http://localhost:9200/_snapshot/movie_data_backup
 
{
    "type":"fs",
    "settings":{
        "location":"/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot",
        "compress":true
    }
}
 
 
 
cs

 

위 요청으로 레포지토리를 생성해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET http://localhost:9200/_snapshot/movie_data_backup
 
result=>
{
    "movie_data_backup": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot"
        }
    }
}
 
 
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
GET http://localhost:9200/_snapshot/search*,agg*,movie*
 
result=>
{
    "searchexam": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/book_backup/search_example"
        }
    },
    "aggexam": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/book_backup/agg_example"
        }
    },
    "movie_data_backup": {
        "type": "fs",
        "settings": {
            "compress": "true",
            "location": "/Users/yun-yeoseong/elasticsearch-file/elasticsearch-6.4.3/backup_snapshot"
        }
    }
}
 
 
cs

 

또한 위와 같이 "*"를 이용하여 여러개의 레포지토리를 조회할 수도 있다.

 

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
29
30
31
32
33
PUT http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511?wait_for_completion=true
 
{
    "indices":"movie_search",
    "ignore_unavailable":true,
    "include_global_state":false
}
 
result=>
{
    "snapshot": {
        "snapshot": "movie_snapshot_20190511",
        "uuid": "m9glqYcyS_mNiv7ZTJmZ8Q",
        "version_id": 6040399,
        "version": "6.4.3",
        "indices": [
            "movie_search"
        ],
        "include_global_state": false,
        "state": "SUCCESS",
        "start_time": "2019-05-11T06:37:08.496Z",
        "start_time_in_millis": 1557556628496,
        "end_time": "2019-05-11T06:37:08.880Z",
        "end_time_in_millis": 1557556628880,
        "duration_in_millis": 384,
        "failures": [],
        "shards": {
            "total": 5,
            "failed": 0,
            "successful": 5
        }
    }
}
cs

 

wait_for_completion=true 옵션을 이용하면 스냅샷 생성이 완료될 때까지 기다린다. 만약 false로 설정하면 스냅샷 초기화가 완료된 직후에 결과를 반환한다. 그리고 내부적으로 백업할 인덱스명을 옵션으로 줄 수 있다.(indices) 하지만 해당 옵션을 사용하지 않으면 기본값으로 해당 클러스터 내부의 모든 인덱스를 백업대상으로 삼는다. 인덱스명은 ","구분으로 여러개를 지정할 수 있다. 그리고 ignore_unavailable 옵션을 true로 설정하면 명시한 indices 옵션에 설정한 인덱스가 존재하지 않는다면 해당 인덱스를 무시하고 다른 인덱스들 백업을 하게 된다. 만약 false로 설정하면 백업을 지정한 인덱스가 존재하지 않으면 에러를 내뱉는다. include_global_state는 클러스터 글로벌 설정을 백업의 일부로 포함시키지 않는다. 만약 해당 설정을 true로 설정하면 모든 샤드에 대해 글로벌 설정이 맞지 않으면 백업이 실패한다. 그리고 만약 스냅샷의 이름의 일부로 날짜등의 데이터를 넣어야 한다면 날짜 표현식을 넣을 수도 있다. 날짜를 넣는다면 데일리 백업에 있어 구분하기 쉬운 방법이 될 것이다. 그리고 배치등을 이용하여 다시 복구할때 소스의 수정없이 날짜 표현식을 이용하면 된다. 해당 표현식은 반드시 URL 엔코딩이 된 문자열이여야한다.

 

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
# PUT /_snapshot/my_backup/<snapshot-{now/d}>
PUT http://localhost:9200/_snapshot/movie_data_backup/%3Csnapshot-%7Bnow%2Fd%7D%3E?wait_for_completion=true
 
{
    "indices":"movie_search",
    "ignore_unavailable":true,
    "include_global_state":false
}
 
result=>
{
    "snapshot": {
        "snapshot": "snapshot-2019.05.11",
        "uuid": "k3GeN2rrS2meMFptYPfAOA",
        "version_id": 6040399,
        "version": "6.4.3",
        "indices": [
            "movie_search"
        ],
        "include_global_state": false,
        "state": "SUCCESS",
        "start_time": "2019-05-11T06:55:51.344Z",
        "start_time_in_millis": 1557557751344,
        "end_time": "2019-05-11T06:55:51.366Z",
        "end_time_in_millis": 1557557751366,
        "duration_in_millis": 22,
        "failures": [],
        "shards": {
            "total": 5,
            "failed": 0,
            "successful": 5
        }
    }
}
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
GET http://localhost:9200/_snapshot/movie_data_backup/*
GET http://localhost:9200/_snapshot/movie_data_backup/_all
GET http://localhost:9200/_snapshot/movie_data_backup/snapshot*
 
result=>
{
    "snapshots": [
        {
            "snapshot": "movie_snapshot_20190511",
            "uuid": "m9glqYcyS_mNiv7ZTJmZ8Q",
            "version_id": 6040399,
            "version": "6.4.3",
            "indices": [
                "movie_search"
            ],
            "include_global_state": false,
            "state": "SUCCESS",
            "start_time": "2019-05-11T06:37:08.496Z",
            "start_time_in_millis": 1557556628496,
            "end_time": "2019-05-11T06:37:08.880Z",
            "end_time_in_millis": 1557556628880,
            "duration_in_millis": 384,
            "failures": [],
            "shards": {
                "total": 5,
                "failed": 0,
                "successful": 5
            }
        },
        {
            "snapshot": "snapshot-2019.05.11",
            "uuid": "k3GeN2rrS2meMFptYPfAOA",
            "version_id": 6040399,
            "version": "6.4.3",
            "indices": [
                "movie_search"
            ],
            "include_global_state": false,
            "state": "SUCCESS",
            "start_time": "2019-05-11T06:55:51.344Z",
            "start_time_in_millis": 1557557751344,
            "end_time": "2019-05-11T06:55:51.366Z",
            "end_time_in_millis": 1557557751366,
            "duration_in_millis": 22,
            "failures": [],
            "shards": {
                "total": 5,
                "failed": 0,
                "successful": 5
            }
        }
    ]
}
cs

 

마지막으로 결과값에 포함된 state의 값의 의미이다.

설명
IN_PROGRESS 스냅샷 생성중.
SUCCESS 스냅샷이 정상적으로 생성됨.
FAILED 스냅샷이 정상적으로 생성되지 못하였으며, 어떠한 데이터도 저장되지 않음.
PARTIAL 글로벌 클러스터 상태가 저장되었지만 하나 이상의 샤드의 데이터가 성공적으로 저장되지 않음.
INCOMPATIBLE

스냅 샷은 이전 버전의 Elasticsearch로 작성되었으므로 현재 버전의 클러스터와 호환되지 않음.

 

공식도큐먼트 글

인덱스 스냅 샷을 만드는 과정에서 Elasticsearch는 이미 리포지토리에 저장된 인덱스 파일 목록을 분석하고 마지막 스냅 샷 이후에 작성되었거나 변경된 파일 만 복사합니다. 따라서 여러 스냅 샷을 압축 된 형태로 저장소에 보존 할 수 있습니다. 스냅 샷 프로세스는 비 차단 방식으로 실행됩니다. 스냅 샷을 생성하는 인덱스에 대해 모든 인덱싱 및 검색 작업을 계속 실행할 수 있습니다. 그러나 스냅 샷은 스냅 샷이 생성 된 시점의 인덱스의 특정 시점보기를 나타내므로 스냅 샷 프로세스가 시작된 후 인덱스에 추가 된 레코드는 스냅 샷에 표시되지 않습니다. 스냅 샷 프로세스는 시작되었지만 현재 재배치되지 않은 기본 샤드에 대해 즉시 시작됩니다. 버전 1.2.0 이전에는 클러스터에 스냅 샷에 참여하는 색인의 재배치 또는 초기화 기본이있는 경우 스냅 샷 작업이 실패합니다. 버전 1.2.0부터 Elasticsearch는 Shard의 재배치 또는 초기화가 스냅 샷을 만들기 전에 완료 될 때까지 대기합니다.

각 인덱스의 복사본을 만드는 것 외에도 스냅 샷 프로세스는 영구 클러스터 설정 및 템플릿을 포함하는 글로벌 클러스터 메타 데이터를 저장할 수 있습니다. 일시적 설정 및 등록 된 스냅 샷 저장소는 스냅 샷의 일부로 저장되지 않습니다.

언제든지 하나의 스냅 샷 프로세스 만 클러스터에서 실행할 수 있습니다. 특정 샤드의 스냅 샷이 생성되는 동안이 샤드는 다른 노드로 이동할 수 없으므로 균형 조정 프로세스 및 할당 필터링을 방해 할 수 있습니다. Elasticsearch는 스냅 샷이 완료되면 샤드를 다른 노드로 이동시킬 수 있습니다 (현재 할당 필터링 설정 및 재조정 알고리즘에 따라).

 

스냅샷 복구

1
POST http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511/_restore
cs

 

위와 같은 요청으로 스냅샷 복구가 가능하다. 하지만 여기서 주의해야 할것은 이미 스냅샷에 저장된 인덱스가 존재할 경우에는 복구에 실패하게 된다. 이말은 즉, 동일한 인덱스가 존재한다면 삭제하고 복구해야한다는 것이다.

 

스냅샷 삭제

 

1
POST http://localhost:9200/_snapshot/movie_data_backup/movie_snapshot_20190511
cs

위의 요청을 보내면 스냅샷이 삭제됨과 동시에 물리적인 디렉토리에서도 삭제되는 것을 볼 수 있다.

 

자세한 설명은 공식 Reference를 참고하면 될듯하다. 스냅샷의 여러 옵션등을 참고 할 수 있다.

 

Snapshot and Restore | Elasticsearch Reference [7.0] | Elastic

The snapshot format can change across major versions, so if you have clusters on different versions trying to write the same repository, snapshots written by one version may not be visible to the other and the repository could be corrupted. While setting t

www.elastic.co

 

지금까지 다양한 검색과 백업/복구 등의 예제를 다루었다. 사실 이것보다 훨씬 다양하고 많은 옵션,기능이 존재한다. 모든 것을 다 다룰 수는 없기때문에 필요한 것은 도큐먼트를 참고하거나 혹은 엘라스틱서치 커뮤니티를 이용해도 좋을 것 같다.

posted by 여성게
:

 

엘라스틱서치 혹은 솔라와 같은 검색엔진들은 모두 한글에는 성능을 발휘하기 쉽지 않은 검색엔진이다. 그 이유는 한글은 다른 언어와 달리 조사나 어미의 접미사가 명사,동사 등과 결합하기 때문에 기본 형태소분석기로는 분석하기 쉽지 않다. 그렇기 때문에 검색엔진을 한글에 적용하기 위해서 별도의 한글 형태소 분석기가 필요하다. 솔라도 물론 가능하고 엘라스틱서치도 역시 한글 형태소 분석기를 내장할 수 있다. 이번 포스팅에서 다루어볼 한글 형태소 분석기는 요즘 뜨고 있는 Nori 형태소분석기를 플러그인 할 것이다. Nori 형태소 분석기는 루씬 프로젝트에서 공식적으로 제공되는 한글 형태소 분석기로써 엘라스틱서치 6.4버전에서 공식적으로 배포됬다. 내부적으로 세종 말뭉치와 mecab-ko-dic 사전을 사용하며, 기존 형태소 분석기와는 다르게 사전을 모두 압축하여 사용하고 있기 때문에 30%이상 빠르고 메모리 사용량도 현저하게 줄었으며, 시스템 전반적으로 영향을 주지 않게 최적화하였다.(아이러니하게 이 형태소분석기를 만든 사람은 한국 사람이 아니다..아마 일본어 형태소 분석기를 만든 사람으로 알고 있다.) 공식적으로 릴리스되었음에도 불구하고 Nori는 기본 플러그인으로 포함되어있지 않기때문에 직접 수동으로 플러그인해야한다(아마 최신에는 기본 내장되어 있을 수도 있다. 만약 그렇다면 댓글 부탁드린다.)

 

Nori 형태소 분석기 플러그인 설치

 

 

플러그인을 위와 같이 설치해준다. 설치가 완료되었다면 plugins 디렉토리에 Nori Analyzer가 설치되어 있을 것이다.

(혹은 ./elasticsearch-plugin list로 analysis-nori가 설치 되었는지 확인해보자. 설치 이후에 엘라스틱서치를 재시작하고 노리 플러그인이 잘 로드되는지 스타트업 로그를 확인하자)

 

직접 사용해보기전에 Nori 형태소 분석기에 대략적인 설명을 하면, Nori 형태소 분석기는 하나의 토크나이저와 두 개의 토큰 필터로 구성돼 있으며 사용자 설정 사전과 불용어 처리를 위한 stoptags를 지원한다.

 

  • nori_tokenizer : 토크나이저
  • nori_part_of_speech : 토큰필터
  • nori_readingform : 토큰필터

 

norir_tokenizer 토크나이저

토크나이저는 루씬에서 사용자 질의 혹은 색인되는 문장을 형태소(토큰)형태로 분리하는 데 사용된다. Nori에서는 해당 토크나이저를 사용할 때에 두 가지 파라미터를 지원한다.

 

  1. decompound_mode : 복합명사를 토크나이저가 처리하는 방식
  2. user_dictionary : 사용자 사전 정의

 

decompound_mode

decompound_mode는 토크나이저가 복합명사를 처리하는 방식을 결정한다. 복합명사가 있을 경우 단어를 어떻게 분리할지를 결정한다.

 

파라미터명 파라미터 값 설명 예제
decompound_mode none 복합명사로 분리하지 않는다 잠실역
사회보장제도
discard 복합명사로 분리하고 원본 데이터는 삭제한다. 잠실역=>[잠실,역]
사회보장제도=>[사회,보장,제도]
mixed 복합명사로 분리하고 원본 데이터도 유지한다. 잠실역=>[잠실,역,잠실역]
사회보장제도=>[사회,보장,제도,사회보장제도]

 

user_dictionary

Nori 형태소 분석기는 위에서도 말햇듯이 시스템사전으로 세종 말뭉치와 mecab-ko-dic 사전을 압축된 형태로 사용한다. 이 사전들 이외로 사용자 사전을 등록하여 사용할 수 있다.(명사,복합명사) 사전 파일은 config 디렉토리 밑에 user_dictionary.txt 와 같은 txt 파일을 만들어 사용한다. 사전파일의 경로는 인덱스 매핑 시 분석기의 파라미터로 지정한다. 

 

사전 등록 방법(config/user_dictionary.txt)

  • 삼성전자
  • 삼성전자 삼성 전자
  • 잠실역

첫번째는 명사 등록이고, 두번째는 복합명사 등록이다. 물론 해당 Nori 사용자 사전 이외에도 엘라스틱서치(루씬의)의 토큰 필터인 synonym filter의 동의어 사전과 stop filter의 불용어 사전등을 관리할 수도 있다. 그렇지만 이것은 사실 Nori 형태소 분석 이후 토큰에 대한 필터이기에 한글 형태소 분석기에 내장된 기능이라고는 볼 수없기에 마지막쯤에 따로 다루겠다.

 

 

Nori Tokenizer를 사용하는 커스텀 분석기 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
설정) PUT http://localhost:9200/korean_analyzer
 
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer"
                }
            }
        }
    }
}
cs

 

korean_analyzer라는 인덱스에 Nori tokenizer를 사용하는 분석기를 사용하는 설정이다. decompound_mode는 mixed이며 config/user_dictionary.txt를 사용하는 nori_tokenizer를 가진 분석기를 설정한것이다. 사실 전처리 필터, 토큰 필터등 더 다양한 옵션을 지정할 수 있다. 지금부터 해당 옵션들을 하나하나 다루어볼 것이다. 우선 위의 분석기가 잘 작동하는지 확인하자.

 

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
설정) POST http://localhost:9200/_analyze
 
{
    "analyzer":"nori",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "역",
            "start_offset": 2,
            "end_offset": 3,
            "type": "word",
            "position": 1
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 3
        },
        {
            "token": "있",
            "start_offset": 9,
            "end_offset": 10,
            "type": "word",
            "position": 5
        }
    ]
}
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
설정) POST http://localhost:9200/korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아역",
            "start_offset": 0,
            "end_offset": 3,
            "type": "word",
            "position": 0
        },
        {
            "token": "은",
            "start_offset": 3,
            "end_offset": 4,
            "type": "word",
            "position": 1
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 2
        },
        {
            "token": "에",
            "start_offset": 7,
            "end_offset": 8,
            "type": "word",
            "position": 3
        },
        {
            "token": "있",
            "start_offset": 9,
            "end_offset": 10,
            "type": "word",
            "position": 4
        },
        {
            "token": "죠",
            "start_offset": 10,
            "end_offset": 11,
            "type": "word",
            "position": 5
        }
    ]
}
cs

 

여러가지로 결과가 다른 것을 볼 수 있다. 일단 사용자 정의사전이 적용되어 복합명사로 분리되지 않고 하나의 명사로 인식된것만 확인하자.

 

nori_part_of_speech 토큰 필터

nori_part_of_speech 토큰 필터는 품사 태그 세트와 일치하는 토큰을 찾아 제거하는 토큰 필터이다. 즉, 모든 명사를 역색인으로 생성하는 것이 아니라, 역색인 문서를 생성하기 전에 명시한 품사 태그와 일치하는 토큰을 제거하여 역색인을 생성한다. 이러한 삭제할 품사 태그는 stoptags라는 파라미터를 이용하여 지정한다.

 

앞에서 만들었던 korean_analyzer 인덱스의 nori_analyzer에 토큰 필터를 추가할 것이다. 그전에 우리는 이미 생성한 인덱스를 변경하려면 인덱스를 close상태로 만들었다가 인덱스를 변경하고 다시 open해주는 과정을 거처야한다.

 

1
2
3
설정) POST http://localhost:9200/korean_analyzer/_close
 
 
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
설정) PUT http://localhost:9200/korean_analyzer/_settings
 
{
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter"    
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E",
                        "IC",
                        "J",
                        "MAG",
                        "MM",
                        "NA",
                        "NR",
                        "SC",
                        "SE",
                        "SF",
                        "SH",
                        "SL",
                        "SN",
                        "SP",
                        "SSC",
                        "SSO",
                        "SY",
                        "UNA",
                        "UNKNOWN",
                        "VA",
                        "VCN",
                        "VCP",
                        "VSV",
                        "VV",
                        "VX",
                        "XPN",
                        "XR",
                        "XSA",
                        "XSN",
                        "XSV"
                    ]
                }
            }
        }
}
 
POST http://localhost:9200/korean_analyzer/_open
 
 
cs

 

거의 대부분의 품사를 제거하도록 stoptags에 설정해주었다.  분석기 설정을 변경한 후에 인덱스의 상태를 open으로 바꾸어준다.

 

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
설정) POST http://localhost:9200/korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"미아역은 어디에 있죠?"
}
 
result =>
 
{
    "tokens": [
        {
            "token": "미아역",
            "start_offset": 0,
            "end_offset": 3,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 5,
            "end_offset": 7,
            "type": "word",
            "position": 2
        }
    ]
}
 
 
cs

 

이전과 동일한 분석요청을 보냈음에도 불구하고 변경된 분석기 설정에 의해 대부분의 토큰이 제거된 것을 볼 수 있다. 이렇게 pos 필터를 이용하여 불필요한 토큰들을 제거할 수 있다. 하지만 너무 과한 토큰 삭제는 분석과정에 영향을 미치니 신중하게 결정해야한다. 품사 태그는 아마 엘라스틱서치 공식 홈페이지에 가면 볼 수 있을 것이다. 하지만 중요한 것은 매번 이렇게 close-open 해가며 품사 제거를 테스트하기는 번거롭다. 필자는 직접 Lucene 라이브러리를 임포트해서 자바클래스에서 테스트를 하니 훨씬 편했다. 소스가 필요하다면 댓글남겨주시면 될 것같다.

 

<품사 태그 의미>

예시가 담긴 정보는 아래 테이블 참고(참조: https://prohannah.tistory.com/73)

값(tag) 영문명 한글명 예시
E Verbal endings 어미 사랑/하(E)/다
IC Interjection 감탄사 와우(IC), 맙소사(IC)
J Ending Particle 조사 나/는(J)/너/에게(J)
MAG General Adverb 일반 부사 빨리(MAG)/달리다, 과연(MAG)/범인/은/누구/인가
MAJ Conjunctive adverb 접속 부사 그런데(MAJ), 그러나(MAJ)
MM (*) ES:Modifier(한정사), 루씬 API:Determiner(관형사) 설명이 다름 맨(MM)/밥
NA Unknown 알 수 없음  
NNB Dependent noun (following nouns) 의존명사  
NNBC Dependent noun 의존명사(단위를 나타내는 명사)  
NNG General Noun 일반 명사 강아지(NNG)
NNP Proper Noun 고유 명사 비숑(NNP)
NP Pronoun 대명사 그것(NP), 이거(NP)
NR Numeral 수사 하나(NR)/밖에, 칠(NR)/더하기/삼(NR)
SC(*) Separator (· / :) 구분자 nori_tokenizer가 특수문자 제거
SE(*) Ellipsis 줄임표(...) nori_tokenizer가 특수문자 제거
SF(*) Terminal punctuation (? ! .) 물음표, 느낌표, 마침표 nori_tokenizer가 특수문자 제거
SH Chinese character 한자 中國(SH)
SL Foreign language 외국어 hello(SL)
SN Number 숫자 1(SN)
SP Space 공백  
SSC(*) Closing brackets 닫는 괄호 ),] nori_tokenizer가 특수문자 제거
SSO(*) Opening brackets 여는 괄호 (,[ nori_tokenizer가 특수문자 제거
SY Other symbol 심벌  
UNA Unknown 알 수 없음  
UNKNOWN Unknown 알 수 없음  
VA Adjective 형용사 하얀(VA)/눈
VCN Negative designator 부정 지정사(서술격조사) 사람/이/아니(VCN)/다
VCP Positive designator 긍정 지정사(서술격조사) 사람/이(VCN)/다
VSV Unknown 알 수 없음  
VV Verb 동사 움직이(VV)/다,먹(VV)/다
VX Auxiliary Verb or Adjective 보조 용언 가지/고/싶(VX)/다, 먹/어/보(VX)/다
XPN(*) Prefix 접두사(체언 접두사?) ES에서 매핑되는 단어를 찾지 못함
XR(*) Root 어근 ES에서 매핑되는 단어를 찾기 못함
XSA Adjective Suffix 형용사 파생 접미사 멋/스럽(XSA)/다
XSN(*) Noun Suffix 명사 파생 접미사 ES에서 매핑되는 단어를 찾기 못함
XSV(*) Verb Suffix 동사 파생 접미사 ES에서 매핑되는 단어를 찾기 못함

 

nori_readingform 토큰 필터

사용자 질의에 존재하는 한자를 한글로 변경하는 역할을 하는 필터이다. 위와 같이 동일한 close-open 과정을 통해 분석기를 변경해보자.

 

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
설정) PUT http://localhost:9200/korean_analyzer/_settings
 
{
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E",
                        "IC",
                        "J",
                        "MAG",
                        "MM",
                        "NA",
                        "NR",
                        "SC",
                        "SE",
                        "SF",
                        "SH",
                        "SL",
                        "SN",
                        "SP",
                        "SSC",
                        "SSO",
                        "SY",
                        "UNA",
                        "UNKNOWN",
                        "VA",
                        "VCN",
                        "VCP",
                        "VSV",
                        "VV",
                        "VX",
                        "XPN",
                        "XR",
                        "XSA",
                        "XSN",
                        "XSV"
                    ]
                }
            }
        }
}
 
 
POST http://localhost:9200/korean_analyzer/_analyze
{
    "analyzer":"nori_analyzer",
    "text":"中國은 어디에 있죠?"
}
result=>
 
{
    "tokens": [
        {
            "token": "중국",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 4,
            "end_offset": 6,
            "type": "word",
            "position": 2
        }
    ]
}
cs

 

한자가 한글로 바뀐 것을 볼 수 있다. 마지막으로는 동의어필터, 불용어 필터를 옵션으로 넣어 조금더 풍부한 분석기를 만들어 볼 것이다. 분석기 설정은 이전에 설정한 것을 수정하려고 했으나, 이미 생성된 인덱스에 추가적으로 필터를 등록하면 예외를 내뱉는다.. 이유는 모르지만 어쨌든 새로 인덱스를 생성하였다.(혹시나 이미 생성된 인덱스에 커스텀 필터를 추가하는 방법을 아시는 분은 꼭 댓글로 남겨주시길 바랍니다...)  ->이미 생성된 인덱스도 추가적으로 필터를 추가하는 것이 되는 것으로 확인(테스트 완료)

 

Nori Tokenizer + Nori POS Filter + Nori readingform Filter + Synonym Filter + Stopword Filter

사실 토큰 필터 말고도 전처리 필터를 적용할 수도 있지만 이번 예제는 생략한다. 인덱스를 close-open하는 과정은 동일함으로 생략한다

.

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
설정) PUT http://localhost:9200/custom_korean_analyzer
 
{
    "settings":{
        "number_of_shards": 5,
        "number_of_replicas": 1,
        "analysis":{
            "tokenizer":{
                "korean_nori_tokenizer":{
                    "type":"nori_tokenizer",
                    "decompound_mode":"mixed",
                    "user_dictionary":"user_dictionary.txt"
                }
            },
            "analyzer":{
                "nori_analyzer":{
                    "type":"custom",
                    "tokenizer":"korean_nori_tokenizer",
                    "filter":[
                        "nori_posfilter",
                        "nori_readingform",
                        "synonym_filtering",
                        "stop_filtering"
                    ]
                }
            },
            "filter":{
                "nori_posfilter":{
                    "type":"nori_part_of_speech",
                    "stoptags":[
                        "E","IC","J","MAG","MM","NA","NR","SC",
                        "SE","SF","SH","SL","SN","SP","SSC","SSO",
                        "SY","UNA","UNKNOWN","VA","VCN","VCP","VSV",
                        "VV","VX","XPN","XR","XSA","XSN","XSV"
                    ]
                },
                "synonym_filtering":{
                    "type":"synonym"
                    ,"synonyms_path":"synonymsFilter.txt"
                },
                "stop_filtering":{
                    "type":"stop"
                    ,"stopwords_path":"stopFilter.txt"
                }
            }
        }
    }
}
 
 
cs

 

동의어 필터와 불용어 필터를 추가해주었다. 각 경로의 사전의 내용은 아래와 같다.

 

  • 동의어 사전 : 여성게->주인  *여성게를 주인으로 변환해준다. 만약 "여성게,주인" 형태로 사전을 작성한다면 여성게라는 토큰을 날리는 것이 아니고 여성게라는 토큰이 들어오면 역색인에 여성게와 주인 두개의 토큰을 색인한다.
  • 불용어 사전 : 바보  *바보라는 단어가 들어오면 불용어 필터에 걸려 토큰을 날린다.
  • 사용자 사전 : 여성게  *여성게라는 단어를 명사로 인식시켰다.

혹시나 인덱스를 생성하고 사전내용을 추가하였다면 꼭 사전파일을 인식시키기위해 close-open 과정을 추가적으로 수행한다!

 

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
설정) POST http://localhost:9200/custom_korean_analyzer/_analyze
 
{
    "analyzer":"nori_analyzer",
    "text":"中國은 어디에 있죠? 그리고 여성게은 누구죠? 바보"
}
 
result =>
{
    "tokens": [
        {
            "token": "중국",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "token": "어디",
            "start_offset": 4,
            "end_offset": 6,
            "type": "word",
            "position": 2
        },
        {
            "token": "그리고",
            "start_offset": 12,
            "end_offset": 15,
            "type": "word",
            "position": 6
        },
        {
            "token": "주인",
            "start_offset": 16,
            "end_offset": 19,
            "type": "SYNONYM",
            "position": 7
        },
        {
            "token": "누구",
            "start_offset": 21,
            "end_offset": 23,
            "type": "word",
            "position": 9
        }
    ]
}
 
 
cs

 

중국이라는 한자를 한글로 바꿔주었고, 품사 태그 리스트에 해당되면 모두 토큰을 삭제했고, 여성게를 주인으로 동의어 처리를 하였으며 마지막으로 바보라는 토큰을 불용어 처리하여 삭제하였다.

 

이렇게 엘라스틱서치에서 Nori 형태소분석기를 사용하는 방법을 간략히 다루어보았다. 사실 이보다 더 응용할 것이 넘치고 넘친다. 토큰 필터만 수십개이고 다양한 조합을 통해 더욱 용도에 맞는 형태소 분석기로 거듭날 수 있다.

posted by 여성게
: