엘라스틱서치 혹은 솔라와 같은 검색엔진들은 모두 한글에는 성능을 발휘하기 쉽지 않은 검색엔진이다. 그 이유는 한글은 다른 언어와 달리 조사나 어미의 접미사가 명사,동사 등과 결합하기 때문에 기본 형태소분석기로는 분석하기 쉽지 않다. 그렇기 때문에 검색엔진을 한글에 적용하기 위해서 별도의 한글 형태소 분석기가 필요하다. 솔라도 물론 가능하고 엘라스틱서치도 역시 한글 형태소 분석기를 내장할 수 있다. 이번 포스팅에서 다루어볼 한글 형태소 분석기는 요즘 뜨고 있는 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 여성게
:
Search-Engine/Lucene 2019. 2. 2. 13:23

Lucene - 유사어,동의어필터(SynonymFilter)를 이용한 커스텀 Analyzer



Lucene에는 사용자가 입력한 질의 혹은 색인 할때의 토큰화 과정에서 여러가지 필터를 등록할 수 있다. 토큰의 종류는 아주 많다. StopFiler(불용어처리,불용어처리 단어의 리스트가 필요),SynonymFiler 등 의 필터들이 존재한다. 그 말은 단순히 토큰화된 텀들을 그대로 사용하는 것이 아니라 전처리,후처리를 필터를 이용해서 처리하여 토큰화된 텀에게 여러가지 효과?를 적용할 수 있는 것이다. 여기서는 간단히 유사어필터를 이용한 Custom한 분석기를 만들어 볼 것이며, 유사어 필터의 특징을 간단히 설명할 것이다.









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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
public class SynonymAnalyzerTest {
 
    
 
    
 
    public void testJumps(String text) throws IOException {
 
        System.out.println("Analyzing \"" +text+ "\"");
 
        System.out.println("\n");
 
        
 
        SynonymAnalyzer analyzer = new SynonymAnalyzer();
 
        
 
        String name = analyzer.getClass().getSimpleName();
 
        System.out.println("  "+name+"  ");
 
        System.out.print("    ");
 
        AnalyzerUtils.displayTokens(analyzer,text);
 
        
 
        System.out.println("\n");
 
        
 
    }
 
    
 
    
 
    public static void main(String[] args) throws IOException {
 
        // TODO Auto-generated method stub
 
        SynonymAnalyzerTest t = new SynonymAnalyzerTest();
 
        t.testJumps("나는 jumps 할거야");
 
    }
 
}
 
class SynonymAnalyzer extends Analyzer{
 
    
 
    @Override
 
    protected TokenStreamComponents createComponents(String fieldName) {
 
        // TODO Auto-generated method stub
 
        
 
        SynonymMap.Builder builder = new SynonymMap.Builder(true);
 
        builder.add(new CharsRef("JUPMS"), new CharsRef("점프,뛰다"), true);
 
        
 
        SynonymMap map = null;
 
        
 
        try {
 
            map=builder.build();
 
        }catch (Exception e) {
 
            // TODO: handle exception
 
            e.printStackTrace();
 
        }
 
        
 
        Tokenizer tokenizer = new StandardTokenizer();
 
        TokenStream filter = new LowerCaseFilter(tokenizer);
 
        filter = new SynonymFilter(filter,map,true);
 
        return new TokenStreamComponents(tokenizer,filter);
 
    }
 
    
 
}
 
class AnalyzerUtils{
 
    public static void displayTokens(Analyzer analyzer,String text) throws IOException {
 
        displayTokens(analyzer.tokenStream("content"new StringReader(text)));
 
    }
 
    
 
    public static void displayTokens(TokenStream stream) throws IOException {
 
        
 
        //텀 속성확인
 
        CharTermAttribute cattr = stream.addAttribute(CharTermAttribute.class);
 
        
 
        //위치증가값 속성 확인
 
        PositionIncrementAttribute postAtrr = stream.addAttribute(PositionIncrementAttribute.class);
 
        //오프셋위치확인
 
        OffsetAttribute offsetAttr = stream.addAttribute(OffsetAttribute.class);
 
        //텀타입 속성 확인
 
        TypeAttribute typeAttr = stream.addAttribute(TypeAttribute.class);
 
        
 
        //stream.incrementToken을 위해 필요
 
        stream.reset();
 
        
 
        int position = 0;
 
        
 
        while (stream.incrementToken()) {
 
            int increment = postAtrr.getPositionIncrement();
 
            
 
            position = position + increment;
 
            System.out.println();
 
            System.out.print(position + ": ");
 
            System.out.print("[ "+cattr.toString()+" : " + offsetAttr.startOffset()+"->"+offsetAttr.endOffset()+"                         : "+typeAttr.type()+" ]");
 
        }
 
 
 
        stream.end();
 
        stream.close();
 
        
 
    }
 
}
cs



-> 이 소스를 간단히 설명하면 커스텀한 분석기를 만들고 그 분석기를 이용해 분석된 사용자 입력 문장을 결과로 뿌려주는 역할을 하는 소스이다.

     1)사용자 정의 분석기

- 지금 작성한 커스텀 분석기는 StandardTokenizer에 SynonymFiler를 붙인 것이다. 여기서 빌더패턴을 이용하여 SynonymMap이란 객체를 다루고 있는데, 이것은 유사어 필터에게 유사어 목록이 담긴 맵을 전달해주기 위한 과정이다. 그리고 사용할 Tokenizer 클래스를 생성하고 사용할 필터의 input으로 토크나이저 객체를 전달해준다. 그리고 유사어 목록이 담긴 맵을 전달해주고, 원 단어를 저장할 것인가 안할 것인가를 지정하는 boolean타입의 매개변수까지 전달을 해준다. 그리고 마지막으로 TokenStreamComponents를 리턴해준다. 여기서 하나빼먹은 설명은 filter는 여러개가 될 수 있다는 점이다. 그래서 유사어 필터전 모든 텀을 소문자로 바꿔주는 LowerCaseFilter를 적용했다. 그런데 조금 설명이 필요한 점이라면 컴포지트 패턴을 이용하여 필터를 이어붙이고 있다는 점이다


   2)분석기 적용 결과

- 분석기의 tokenStream 메소드를 호출하면 최종적인 처리가된 TokenStream객체를 리턴해준다. 이 TokenStream 객체를 이용하여 분석된 결과를 출력할 수 있다.(최종적으로 색인에 들어가는 데이터는 TokenStream에 텀과 여러가지 메타데이터가 담기는 데이터이다.) 나머지 소스는 주석으로 충분히 예측가능할 것이다.






마지막 결과를 확인하면 JUMPS라는 단어가 소문자로 되어 유사어 필터가 적용되는 것을 볼 수 있다. 하지만 조금 특이한 점이 있다.


<결과>

Analyzing "나는 JUMPS 할거야"



  SynonymAnalyzer  

    

1: [ 나는 : 0->2 : <HANGUL> ]

2: [ jumps : 3->8 : <ALPHANUM> ]

2: [ 점프,뛰다 : 3->8 : SYNONYM ]

3: [ 할거야 : 9->12 : <HANGUL> ]


원단어와 유사어 처리된 단어가 위치 값이 같은 것이다. 즉, 색인에는 원단어는 물론 유사어까지 같은 포지션을 갖고 색인된다는 것이다. 이 말은 색인과정에서 유사어 필터를 등록한다면 검색에서는 유사어가 포함이 되어 있는 구문으로 구문검색을 해도 색인했던 원문 Document가 검색될 수 있다는 점이다. 아주 좋은 기능일 것 같다. 하지만 유사어 필터는 결코 가벼운 작업이 아니기에 꼭 색인 혹은 검색 둘중하나의 과정에만 적용시키면 된다. 보통 색인과 검색에 둘다 유사어 필터가 담긴 분석기를 사용하기도 하는데, 나중에 아주 데이터가 커지고 애플리케이션이 커지면 영향을 미칠 수도 있을 것같다.


posted by 여성게
: