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

2019. 5. 9. 00:54Search-Engine/Elasticsearch&Solr

 

엘라스틱서치 혹은 솔라와 같은 검색엔진들은 모두 한글에는 성능을 발휘하기 쉽지 않은 검색엔진이다. 그 이유는 한글은 다른 언어와 달리 조사나 어미의 접미사가 명사,동사 등과 결합하기 때문에 기본 형태소분석기로는 분석하기 쉽지 않다. 그렇기 때문에 검색엔진을 한글에 적용하기 위해서 별도의 한글 형태소 분석기가 필요하다. 솔라도 물론 가능하고 엘라스틱서치도 역시 한글 형태소 분석기를 내장할 수 있다. 이번 포스팅에서 다루어볼 한글 형태소 분석기는 요즘 뜨고 있는 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 형태소분석기를 사용하는 방법을 간략히 다루어보았다. 사실 이보다 더 응용할 것이 넘치고 넘친다. 토큰 필터만 수십개이고 다양한 조합을 통해 더욱 용도에 맞는 형태소 분석기로 거듭날 수 있다.