본문 바로가기

AWS/ElasticSearch를 이용한 검색

3) nori 한글 분석기 테스트 및 품사 정의

반응형

앞에서 설정한 nori 한글분석기가 어떻게 동작을 하는지 테스트 해 보자. 

기본 분석기와의 차이점과 한글 품사에 대해 알아 본다.

먼저 앞에서 등록한 검색 데이터의 내용을 기본 분석기와 nori 분석기로 토크나이저 했을떄 차이점을 보자. 

테스트 데이터

이것은 테스트하고 있는 컨텐츠

기본 분석기로 테스트 해보자 . 테스트 방식은 엘라스틱서치의 api 중에 _analyze  api 이용해서 본다.

localhost:9200/content-index-1654676543411/_analyze

(request body)
{
    "analyzer": "default",
    "text": "이것은 테스트하고 있는 컨텐츠",
    "explain": true
}

(결과)

{
    "tokens": [
        {
            "token": "이것은",
            "start_offset": 0,
            "end_offset": 3,
            "type": "<HANGUL>",
            "position": 0
        },
        {
            "token": "테스트하고",
            "start_offset": 4,
            "end_offset": 9,
            "type": "<HANGUL>",
            "position": 1
        },
        {
            "token": "있는",
            "start_offset": 10,
            "end_offset": 12,
            "type": "<HANGUL>",
            "position": 2
        },
        {
            "token": "컨텐츠",
            "start_offset": 13,
            "end_offset": 16,
            "type": "<HANGUL>",
            "position": 3
        }
    ]
}

 

위 결과에서 보면 알수 있듯이 기본 분석기는 SP(space)를 기준으로 토크나이저를 하고 있는것을 알 수 있다.

이제 한글 분석기 nori는 어떻게 동작하는지 보자 

 

localhost:9200/content-index-1654676543411/_analyze
(request body)
{
    "analyzer": "nori",
    "text": "이것은 테스트하고 있는 컨텐츠",
    "explain": false
}

(결과)
{
    "tokens": [
        {
            "token": "이것",
            "start_offset": 0,
            "end_offset": 2,
            "type": "word",
            "position": 0
        },
        {
            "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": 4,
            "end_offset": 7,
            "type": "word",
            "position": 2
        },
        {
            "token": "하",
            "start_offset": 7,
            "end_offset": 8,
            "type": "word",
            "position": 3
        },
        {
            "token": "고",
            "start_offset": 8,
            "end_offset": 9,
            "type": "word",
            "position": 4
        },
        {
            "token": "있",
            "start_offset": 10,
            "end_offset": 11,
            "type": "word",
            "position": 5
        },
        {
            "token": "는",
            "start_offset": 11,
            "end_offset": 12,
            "type": "word",
            "position": 6
        },
        {
            "token": "컨텐츠",
            "start_offset": 13,
            "end_offset": 16,
            "type": "word",
            "position": 7
        }
    ]
}

조금 더 한글에서의 의미가 있는 부분으로 토크나이저 된것을 확인이 된다. 그럼 어떤  각 단어들이 정말 필요한것일까 ? 

위 nori 분석기로 검색 데이터가 인덱싱 된 상태에서 검색을 해보면 "하" , "고 " 등의 단어로도 검색이 되는것을 확인이 된다.

위에서 검색 결과에서 보면 "하"만 검색으로 해도 하가 들어가된 컨텐츠가 다 검색이 된다. 

뭐 이렇게 검색되는것을 원 한것일수도 있지만 그렇지 않는 경우가 대부분이라 생각된다. 의미가 있는 단어로만 검색이 되길 원하며 

검색의 질을 높이고 싶을때 의미 없는 조사등의 품사는 토크나이징 되지 않고 싶을때 어떻게 해야 할까?

먼저 앞에서 테스트 한 문구에서는 어떤 품사가 토크나이징 된것인지 확인해보자. 

앞에 테스트 옵션중에 explan을 true로 해보자. 

{
    "analyzer": "nori",
    "text": "이것은 테스트하고 있는 컨텐츠",
    "explain": true
}


{
    "detail": {
        "custom_analyzer": true,
        "charfilters": [],
        "tokenizer": {
            "name": "nori_mixed",
            "tokens": [
                {
                    "token": "이것",
                    "start_offset": 0,
                    "end_offset": 2,
                    "type": "word",
                    "position": 0,
                    "bytes": "[ec 9d b4 ea b2 83]",
                    "leftPOS": "NP(Pronoun)",
                    "morphemes": "이거/NP(Pronoun)",
                    "posType": "INFLECT",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NP(Pronoun)",
                    "termFrequency": 1
                },
                {
                    "token": "이거",
                    "start_offset": 0,
                    "end_offset": 2,
                    "type": "word",
                    "position": 0,
                    "bytes": "[ec 9d b4 ea b1 b0]",
                    "leftPOS": "NP(Pronoun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NP(Pronoun)",
                    "termFrequency": 1
                },
                {
                    "token": "은",
                    "start_offset": 2,
                    "end_offset": 3,
                    "type": "word",
                    "position": 1,
                    "bytes": "[ec 9d 80]",
                    "leftPOS": "J(Ending Particle)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "J(Ending Particle)",
                    "termFrequency": 1
                },
                {
                    "token": "테스트",
                    "start_offset": 4,
                    "end_offset": 7,
                    "type": "word",
                    "position": 2,
                    "bytes": "[ed 85 8c ec 8a a4 ed 8a b8]",
                    "leftPOS": "NNG(General Noun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NNG(General Noun)",
                    "termFrequency": 1
                },
                {
                    "token": "하",
                    "start_offset": 7,
                    "end_offset": 8,
                    "type": "word",
                    "position": 3,
                    "bytes": "[ed 95 98]",
                    "leftPOS": "XSV(Verb Suffix)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "XSV(Verb Suffix)",
                    "termFrequency": 1
                },
                {
                    "token": "고",
                    "start_offset": 8,
                    "end_offset": 9,
                    "type": "word",
                    "position": 4,
                    "bytes": "[ea b3 a0]",
                    "leftPOS": "E(Verbal endings)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "E(Verbal endings)",
                    "termFrequency": 1
                },
                {
                    "token": "있",
                    "start_offset": 10,
                    "end_offset": 11,
                    "type": "word",
                    "position": 5,
                    "bytes": "[ec 9e 88]",
                    "leftPOS": "VX(Auxiliary Verb or Adjective)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "VX(Auxiliary Verb or Adjective)",
                    "termFrequency": 1
                },
                {
                    "token": "는",
                    "start_offset": 11,
                    "end_offset": 12,
                    "type": "word",
                    "position": 6,
                    "bytes": "[eb 8a 94]",
                    "leftPOS": "E(Verbal endings)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "E(Verbal endings)",
                    "termFrequency": 1
                },
                {
                    "token": "컨텐츠",
                    "start_offset": 13,
                    "end_offset": 16,
                    "type": "word",
                    "position": 7,
                    "bytes": "[ec bb a8 ed 85 90 ec b8 a0]",
                    "leftPOS": "NNG(General Noun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NNG(General Noun)",
                    "termFrequency": 1
                }
            ]
        },
        "tokenfilters": []
    }
}

leftPos , rightPos 부분의 해당 단어의 품사를 나타내는 부분이다. 각각의 품사에 대한 정보는 아래를 참조.

(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에서 매핑되는 단어를 찾기 못함

여기서는 의미가 없어 보이는 "은" , "하", "고", "있", "는" 을 없애 보자.

앞에서 생성했던 index template을 아래와 같이 수정했다.

분석기에 stoptags(등록된 품사를 제외시킨다.) 가 있는 filter를 달아서 원하는 품사만 나오게 했다.

{
    "order": 0,
    "version": 1,
    "index_patterns": [
        "content-index-*"
    ],
    "settings": {
        "index": {
            "analysis": {
                "analyzer": {
                    "nori": {
                        "type": "custom",
                        "tokenizer": "nori_mixed",
                        "filter": [
                            "my_posfilter"
                        ]
                    }
                },
                "tokenizer": {
                    "nori_mixed": {
                        "type": "nori_tokenizer",
                        "decompound_mode": "mixed"
                    }
                },
                "filter": {
                    "my_posfilter": {
                        "type": "nori_part_of_speech",
                        "stoptags": [
                            "J",
                            "XSV",
                            "E",
                            "VX"
                        ]
                    }
                }
            },
            "number_of_shards": "1",
            "number_of_replicas": "0"
        }
    },
    "mappings": {
        "properties": {
            "contentText": {
                "analyzer": "nori",
                "type": "text"
            },
            "title": {
                "analyzer": "nori",
                "type": "text"
            }
        }
    },
    "aliases": {}
}

템플릿을 수정하고 다시 검색데이터 인덱싱을 한 후에 앞에서 테스트 한 문자열이 어떻게 토크나이징 되는지 보자.

{
    "analyzer": "nori",
    "text": "이것은 테스트하고 있는 컨텐츠",
    "explain": true
}


{
    "detail": {
        "custom_analyzer": true,
        "charfilters": [],
        "tokenizer": {
            "name": "nori_mixed",
            "tokens": [
                {
                    "token": "이것",
                    "start_offset": 0,
                    "end_offset": 2,
                    "type": "word",
                    "position": 0,
                    "bytes": "[ec 9d b4 ea b2 83]",
                    "leftPOS": "NP(Pronoun)",
                    "morphemes": "이거/NP(Pronoun)",
                    "posType": "INFLECT",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NP(Pronoun)",
                    "termFrequency": 1
                },
                {
                    "token": "이거",
                    "start_offset": 0,
                    "end_offset": 2,
                    "type": "word",
                    "position": 0,
                    "bytes": "[ec 9d b4 ea b1 b0]",
                    "leftPOS": "NP(Pronoun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NP(Pronoun)",
                    "termFrequency": 1
                },
                {
                    "token": "은",
                    "start_offset": 2,
                    "end_offset": 3,
                    "type": "word",
                    "position": 1,
                    "bytes": "[ec 9d 80]",
                    "leftPOS": "J(Ending Particle)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "J(Ending Particle)",
                    "termFrequency": 1
                },
                {
                    "token": "테스트",
                    "start_offset": 4,
                    "end_offset": 7,
                    "type": "word",
                    "position": 2,
                    "bytes": "[ed 85 8c ec 8a a4 ed 8a b8]",
                    "leftPOS": "NNG(General Noun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NNG(General Noun)",
                    "termFrequency": 1
                },
                {
                    "token": "하",
                    "start_offset": 7,
                    "end_offset": 8,
                    "type": "word",
                    "position": 3,
                    "bytes": "[ed 95 98]",
                    "leftPOS": "XSV(Verb Suffix)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "XSV(Verb Suffix)",
                    "termFrequency": 1
                },
                {
                    "token": "고",
                    "start_offset": 8,
                    "end_offset": 9,
                    "type": "word",
                    "position": 4,
                    "bytes": "[ea b3 a0]",
                    "leftPOS": "E(Verbal endings)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "E(Verbal endings)",
                    "termFrequency": 1
                },
                {
                    "token": "있",
                    "start_offset": 10,
                    "end_offset": 11,
                    "type": "word",
                    "position": 5,
                    "bytes": "[ec 9e 88]",
                    "leftPOS": "VX(Auxiliary Verb or Adjective)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "VX(Auxiliary Verb or Adjective)",
                    "termFrequency": 1
                },
                {
                    "token": "는",
                    "start_offset": 11,
                    "end_offset": 12,
                    "type": "word",
                    "position": 6,
                    "bytes": "[eb 8a 94]",
                    "leftPOS": "E(Verbal endings)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "E(Verbal endings)",
                    "termFrequency": 1
                },
                {
                    "token": "컨텐츠",
                    "start_offset": 13,
                    "end_offset": 16,
                    "type": "word",
                    "position": 7,
                    "bytes": "[ec bb a8 ed 85 90 ec b8 a0]",
                    "leftPOS": "NNG(General Noun)",
                    "morphemes": null,
                    "posType": "MORPHEME",
                    "positionLength": 1,
                    "reading": null,
                    "rightPOS": "NNG(General Noun)",
                    "termFrequency": 1
                }
            ]
        },
        "tokenfilters": [
            {
                "name": "my_posfilter",
                "tokens": [
                    {
                        "token": "이것",
                        "start_offset": 0,
                        "end_offset": 2,
                        "type": "word",
                        "position": 0,
                        "bytes": "[ec 9d b4 ea b2 83]",
                        "leftPOS": "NP(Pronoun)",
                        "morphemes": "이거/NP(Pronoun)",
                        "posType": "INFLECT",
                        "positionLength": 1,
                        "reading": null,
                        "rightPOS": "NP(Pronoun)",
                        "termFrequency": 1
                    },
                    {
                        "token": "이거",
                        "start_offset": 0,
                        "end_offset": 2,
                        "type": "word",
                        "position": 0,
                        "bytes": "[ec 9d b4 ea b1 b0]",
                        "leftPOS": "NP(Pronoun)",
                        "morphemes": null,
                        "posType": "MORPHEME",
                        "positionLength": 1,
                        "reading": null,
                        "rightPOS": "NP(Pronoun)",
                        "termFrequency": 1
                    },
                    {
                        "token": "테스트",
                        "start_offset": 4,
                        "end_offset": 7,
                        "type": "word",
                        "position": 2,
                        "bytes": "[ed 85 8c ec 8a a4 ed 8a b8]",
                        "leftPOS": "NNG(General Noun)",
                        "morphemes": null,
                        "posType": "MORPHEME",
                        "positionLength": 1,
                        "reading": null,
                        "rightPOS": "NNG(General Noun)",
                        "termFrequency": 1
                    },
                    {
                        "token": "컨텐츠",
                        "start_offset": 13,
                        "end_offset": 16,
                        "type": "word",
                        "position": 7,
                        "bytes": "[ec bb a8 ed 85 90 ec b8 a0]",
                        "leftPOS": "NNG(General Noun)",
                        "morphemes": null,
                        "posType": "MORPHEME",
                        "positionLength": 1,
                        "reading": null,
                        "rightPOS": "NNG(General Noun)",
                        "termFrequency": 1
                    }
                ]
            }
        ]
    }
}

위 결과에서 보면 filter가 없는 상태의 토크나이저 부분과 필터링된 토크나이저 부분이 보인다. 

없애고자했던 품사들이 없어진것을 확인!! 

이제 실제 엘라스틱서치에서 search api로 인덱싱된 데이터에서 "하" 를 검색했을때 결과를 확인해 보자.

검색 된 결과가 없는것을 확인! 다시 "테스트"로 검색을 하면 검색 결과가 잘 나오는것을 볼 수 있다.

 

이렇게 품사를 조절하면서 검색의 질을 높이는 방법에 대해 알아 보았다.