"nori_user_tokenizer": {
"type": "nori_tokenizer",
"user_dictionary_rules": ["1+1", "(G)I-DLE", "(G)IDLE", "{G}IDLE", "(주)교보문고", "교보문고", "C#"],
"decompound_mode": "discard",
"discard_punctuation": true
}
“C#” 을 명사사전에 등록할 경우 “C”로 명사사전에 색인이 된다. 따라서 사용자가 Cursed라는 단어를 검색한다면 “C” "ursed"로 형태소 분석되어 예상과 다른 결과를 출력해낸다. 해당 문제는 ES에선 기본적으로 tokenizer세팅 중 discard_punctuation 옵션이 true이기 때문에 구두점이 들어간 것들은 무시한채 형태소분석이 되기 때문이다.
1-2의 두 예시들은 형태소분석이 아예 되지 않는다.
discard_punctuation 옵션은 nori_tokenizer 내에서 구두점을 제거할지 여부를 설정한다.
true로 설정하면, 구두점이 포함된 단어가 토큰화 과정에서 제거된다.
false로 설정하면, 구두점이 포함된 단어도 토큰으로 유지된다.
문장 부호: 마침표(.), 쉼표(,), 물음표(?), 느낌표(!), 세미콜론(;), 콜론(:)
괄호: 소괄호((, )), 중괄호({, }), 대괄호([, ])
인용 부호: 큰따옴표("), 작은따옴표(')
하이픈 및 대시: 하이픈(-), 대시(—)
기타 기호: 앰퍼샌드(&), 별표(*), 슬래시(/), 백슬래시(), 퍼센트(%), 샵(#), 골뱅이(@)
형태소 분석 결과에서 명사사전으로 등록한 “(주)교보문고”, “(G)I-DLE”이 형태소 분석되지 않은 것을 확인할 수 있다.
"tokenizer": {
"bigram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 2
},
"nori_user_tokenizer": {
"type": "nori_tokenizer",
"user_dictionary_rules": ["(주)교보문고", "교보문고", "1+1", "(G)I-DLE", "(G)IDLE", "{G}IDLE", "C#"],
"decompound_mode": "discard",
"discard_punctuation": true
}
}
POST /my_index/_analyze
{
"analyzer": "nori_user_analyzer",
"text": "안녕하세요. (주)교보문고에서 (G)I-DLE의 책을 구매했습니다. 가격은 1+1입니다."
}
// 결과
{
"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": 27,
"end_offset": 28,
"type": "word",
"position": 6
},
{
"token": "구매",
"start_offset": 30,
"end_offset": 32,
"type": "word",
"position": 8
},
{
"token": "하",
"start_offset": 32,
"end_offset": 33,
"type": "word",
"position": 9
},
{
"token": "가격",
"start_offset": 38,
"end_offset": 40,
"type": "word",
"position": 12
},
{
"token": "1+1",
"start_offset": 42,
"end_offset": 45,
"type": "word",
"position": 14
},
{
"token": "이",
"start_offset": 45,
"end_offset": 48,
"type": "word",
"position": 15
}
]
}
형태소 분석 결과에서 명사사전으로 등록한 “(주)교보문고”, “(G)I-DLE”이 형태소 분석된 것을 확인할 수 있다.
"tokenizer": {
"bigram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 2
},
"nori_user_tokenizer": {
"type": "nori_tokenizer",
"user_dictionary_rules": ["(주)교보문고", "교보문고", "1+1", "(G)I-DLE", "(G)IDLE", "{G}IDLE", "C#"],
"decompound_mode": "discard",
"discard_punctuation": false
}
}
POST /my_index/_analyze
{
"analyzer": "nori_user_analyzer",
"text": "안녕하세요. (주)교보문고에서 (G)I-DLE의 책을 구매했습니다. 가격은 1+1입니다."
}
// 결과
{
"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": 7,
"end_offset": 14,
"type": "word",
"position": 6
},
{
"token": "(g)i-dle",
"start_offset": 17,
"end_offset": 25,
"type": "word",
"position": 9
},
{
"token": "책",
"start_offset": 27,
"end_offset": 28,
"type": "word",
"position": 12
},
{
"token": "구매",
"start_offset": 30,
"end_offset": 32,
"type": "word",
"position": 15
},
{
"token": "하",
"start_offset": 32,
"end_offset": 33,
"type": "word",
"position": 16
},
{
"token": "가격",
"start_offset": 38,
"end_offset": 40,
"type": "word",
"position": 21
},
{
"token": "1+1",
"start_offset": 42,
"end_offset": 45,
"type": "word",
"position": 24
},
{
"token": "이",
"start_offset": 45,
"end_offset": 48,
"type": "word",
"position": 25
}
]
}
discard_punctuation 옵션을 false로 주고 “C#”을 명사 사전에 등록하더라도 “C”와 “#”으로 분리되어 나오는 것을 볼 수 있다. “C###”을 명사 사전에 등록해도 “C”, “###”으로 분리가 된다. “#”이외의 다른 문자의 경우 정상적으로 출력이 되지만, “#”만 명사 사전을 따르지 않는다.
char_filter옵션에서 pattern_replace를 사용하여 “#”을 대부분의 유저가 검색하지 않을 것 같은 “|@@|”으로 변경했다. nori_tokenizer에서 “#”을 어떻게 예외처리하는지는 모르겠지만 “#”을 다른 특수문자로 변경하니 하나의 토큰으로 응답하는 것을 확인할 수 있었다.
"char_filter": {
"remove_whitespace": {
"pattern": "\\\\s",
"type": "pattern_replace",
"replacement": ""
},
"hash_to_special": {
"pattern": "#",
"type": "pattern_replace",
"replacement": "|@@|"
}
},
"user_dictionary_rules": [
"1+1",
".G.IDLE", ",G,IDLE", "?G?IDLE", "!G!IDLE", ";G;IDLE", ":G:IDLE",
"(G)IDLE", "{G}IDLE", "[G]IDLE",
"IDLE", "'G'IDLE",
"-G-IDLE",
"&G&IDLE","*G*IDLE","/G/IDLE","\\\\G\\\\IDLE","%G%IDLE","|@@|G|@@|IDLE","@G@IDLE", "|G|IDLE",
"(주)교보문고", "교보문고", "정보검색#3", "C|@@|"
],
POST /test-jh/_analyze
{
"text": "#G#IDLE",
"analyzer": "nori_custom_analyzer"
}
// 결과
{
"tokens": [
{
"token": "|@@|g|@@|idle",
"start_offset": 0,
"end_offset": 7,
"type": "word",
"position": 0
}
]
}
특정 문자를 임의로 변경하는 것은 나중에 다른 개발자가 왔을 때 “#”이 왜 다른 문자로 바뀌어 분석되는지 의아해할 수도 있다. 따라서 임의로 변경한 세팅값에 대해선 나중에 다른 개발자가 예외 사항에 대해서 쉽게 이해할 수 있도록 문서화를 잘 해둬야 한다.
“#” 과 같은 예외사항을 찾긴 했으나, 추가적으로 다른 예외사항이 생길 수 있다. 구두점에 사이드이펙트에 대해서는 지속적으로 지켜봐야 한다.
bo-search-es-api 의 코드에서도 “#”이 들어왔을 때 “|@@|”으로 바꾸어 사전에 저장하는 로직을 추가해야한다.