Skip to content

Elasticsearch에서 Tokenizer를 커스터마이징 해보자

Elasticsearcch2 min read

나는 사내에서 URL 데이터를 처리하는 프로젝트를 맡고 있었다. 데이터의 빠른 검색을 위해 Elasticsearch를 사용해 검색 테스트를 해보았는데, URL 데이터를 저장하고 검색했을 때 생각보다 제대로 검색이 되지 않는 문제를 발견했다.

준비물

  • Elasticsearch v7.x

왜 검색이 안 되죠?

내 Elasticsearch 인덱스에는 다음과 같은 URL 데이터가 저장돼 있다.

https://tk.welldnn.com/

Root domain을 기준으로 검색한다고 가정하고, 'welldnn' 혹은 'welldnn.com'으로 기본적인 전문 검색(Full text query)을 했다.

GET /_search
{
"query": {
"match": {
"message": {
"query": "welldnn"
}
}
}
}

위의 쿼리로 검색했을 때, 일치하는 검색 결과가 없었다. Elasticsearch에서 검색하면 일부 키워드로도 검색할 수 있다고 알고 있었는데, 어디서부터 잘못된 걸까?

텍스트 분석

해당 검색의 원인을 알기 위해서는 Elasticsearch가 데이터를 어떻게 저장하는지를 알아야 한다.

Elasticsearch는 텍스트를 저장할 때 '텍스트 분석(Text Analysis)'을 수행한다. 이를 통해 특유의 '역 색인(Inverted index)' 구조를 형성한다.

분석기

해당 과정은 '분석기(Analyzer)'가 수행한다. 분석기는 크게 문자 필터(Character filters)와 토크나이저(Tokenizer), 토큰 필터(Token filter)로 구성돼 있다. 각 요소는 개별적으로 사용자 지정이 가능하며, 이들이 모여 다양한 언어 및 텍스트 유형에 적합한 분석기를 구성한다.

토큰화

여기서 눈여겨보아야 할 부분은 토크나이저(Tokenizer)다. 토크나이저는 입력된 텍스트를 '토큰'이라고 하는 개별 단어들로 분할한다. 이 작업을 토큰화(Tokenization)라고 한다. 이 과정을 통해 사용자는 일부 단어만 입력해도 전체 구문을 검색할 수 있는 전문 검색(Full text query)이 가능하다.

Elasticsearch는 기본적으로 표준 분석기(Standard analyzer)를 제공하며, 그 안에는 표준 토크나이저(Standard tokenizer)가 있다.

다음과 같은 텍스트를 저장하는 경우,

"The 2 QUICK Brown-Foxes jumped.over the lazy dog's bone."

표준 토크나이저는 아래와 같이 토큰화한다. 표준 토크나이저의 분할 기준은 크게 공백(Whitespace)@과 같은 일부 특수문자다.

[ The, 2, Quick, Brown, Foxes, jumped.over, the, lazy, dog's, bone ]

분절된 단어들은 모두 앞서 저장된 텍스트를 가리킨다. 그래서 사용자는 'Quick'이나 'lazy'를 입력하면 위의 문장을 검색할 수 있게 된다.

이 규칙을 살펴보니 내가 저장했던 데이터는 왜 검색이 안 됐는지 짐작이 간다. 내가 저장했던 URL은 어떻게 토큰화가 되어 있는지 살펴보자.

GET _analyze?pretty
{
"field": "my_field",
"text": "https://tk.welldnn.com/"
}

"https", "tk.welldnn.com" 단 두 개의 토큰으로만 분할되어 있다.

{
"tokens" : [
{
"token" : "https",
"start_offset" : 0,
"end_offset" : 5,
"type" : "<ALPHANUM>",
"position" : 0,
},
{
"token" : "tk.welldnn.com",
"start_offset" : 22,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 1
}
]
}

표준 토크나이저는 "."를 구분자로 인식하지 않는다. 그래서 'welldnn.com'으로 검색하면 결과가 나오지 않았던 것이다.

토크나이저 사용자 지정

앞서 말했듯 토크나이저는 사용자 지정이 가능하다. 내가 원하는 규칙으로 텍스트를 분절하여 'wellldnn'이나 'welldnn.com'을 입력하면 검색이 되도록 해보자. 다행히 Elasticsearch는 내장 토크나이저들을 통해 다양한 분절 규칙을 제공한다.

N-gram Tokenizer

N-gram tokenizer는 입력된 문자열 중 하나의 문자가 들어올 때마다 지정된 길이의 N-gram을 출력한다. 공백을 사용하지 않는 언어들을 다룰 때 유용하다.

기본 ngram tokenizer는 텍스트를 최소 길이 1에서 최대 길이 2의 N-gram으로 분할한다.

POST _analyze
{
"tokenizer": "ngram",
"text": "Quick Fox"
}
# 응답
[ Q, Qu, u, ui, i, ic, c, ck, k, "k ", " ", " F", F, Fo, o, ox, x ]

공백 관계없이 문자열을 특정 길이에 따라 다양하게 분리를 해주는 것을 확인하니 URL을 분석하고 검색하는 데 효과적인 방법이라는 것을 알겠다. 이제 내 인덱스에 설정하고 테스트 해보자.

URL의 경우 최소 2 글자로 검색을 해야하는 경우가 있다. (e.g. http://www.lm.co.kr/) 최소 2 글자, 최대 30 글자로 gram의 단위를 설정한다. 토큰에 포함할 문자 유형(token_chars)으로는 일반 문자(letter), 숫자(digit), 구두점(punctuation)으로 설정했다.

💡 N-gram tokenizer의 세부 설정은 여기서 자세히 확인할 수 있다.

PUT my-index-url
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"tokenizer": "my_tokenizer"
}
},
"tokenizer": {
"my_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 30,
"token_chars": [
"letter",
"digit",
"punctuation"
]
}
}
}
}
}

설정한 분석기를 이용해 내가 검색할 URL의 분석 결과를 확인해보자.

분할되는 N-gram의 수가 많아 몇몇 구간은 생략했다. 설정대로 최소 2길이부터 토큰이 생성돼 있다. 'welldnn'과 'welldnn.com'이 토큰으로 들어 있음을 확인할 수 있다.

POST my-index-url/_analyze
{
"analyzer": "my_analyzer",
"text": "https://tk.welldnn.com/"
}
# 응답
[ ht, htt, (...), welldn, welldnn, welldnn., (...), welldnn.com, (...) ]

Mapping

사용자 지정한 분석기를 이용하려면 해당 필드에 직접 적용해줘야 한다. 이 과정을 매핑(Mapping)이라고 한다.

PUT /my-index-url
{
"mappings": {
"properties": {
"my_field": {
"type": "text",
"analyzer": "my_analyzer"
}
}
}
}`

다시 검색

매핑을 완료했다면 my_field에 데이터 https://welldnn.com/ 을 다시 저장하고, 검색을 진행해보자.

💡 bool - must: 쿼리에 일치하는 결과물을 모두 반환

💡 term: 입력된 검색어를 정확하게 포함하는 문서를 반환

GET /_search
{
"query": {
"bool": {
"must": {
"term": {
"my_field.ngram": "welldnn"
}
}
}
}
}

정상적으로 문서를 반환한다. 'welldnn.com'으로 했을 때에도 동일한 결과를 얻었다.

{
"took" : 43,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "my-indxe-url",
"_type" : "_doc",
"_id" : "YKolMHUBBpbv40eG180S",
"_score" : 1.0,
"_source" : {
"my_field" : "https://tk.welldnn.com/"
}
}
]
}
}

결론

실제로 해당 설정들을 적용할 땐 잘 몰랐는데, 글을 쓰면서 Elasticsearch에 저장하는 텍스트 데이터들이 어떤 원리로 저장되고 관리되는지 자세히 알 수 있었다.


참고