top bar

글 목록

2015년 6월 6일 토요일

[Elasticsearch] Tokenizer와 Token Filter

1. 개요



지난 포스트엔, Full Text Search 에 효율적인 'Inverted Index'에 대해서 알아 보았다. 요약하자면, Full Text에 대해서 해당 문장을 분리/가공 하고, 그 각각의 terms를 document와 매핑시키는 인덱스 리스트를 만드는 것이다. (책 뒷편의 '단어색인' 리스트와 비슷한 개념이다)

이번 포스트에는 Inverted Index를 만들기 위해 수행되는 분리/가공의 작업들이 Elasticsearch에서 어떤방식으로 일어나는지 알아 보고자 한다.


2. Tokenizer와 Token Filter



Inverted Index를 생성하는 과정을 되짚어보면, 먼저 각 단어(term또는 token)를 분리하는 작업이 먼저 수행되었고, 그 다음으로 분리된 단어들을 검색 가능 하도록(searchable) 가공하는 작업을 수행했다.

전자는 Tokenizer, 후자는 Token Filter가 수행한다. 이러한 Tokenizer와 Token Filter는 매우 다양한 종류가 있는데, 이 둘을 합친것이 바로 'Analyzer'(분석기) 인 것이다.


INFO : 문자열이 Tokenizing 되기전에 'Character Filtering'이라는 작업을 거치는데, 이는 문장이 분리되기전에 깔끔하게 정리하는(tidy up) 역할을 한다. 예를들어 contents 안에 들어있는 HTML태그를 걷어내는 작업이라던가, 문장속에 '&'을 영단어 'and'로 바꿔주는 작업 등이다. 이번 포스트에서는 상세한 설명을 생략하도록한다...(시간음슴)

2.1 Tokenizer


토큰들을 분리하는 작업을 수행한다. 대표적으로 'whitespace' 토크나이저가 있는데, 말그대로 공백을 기준으로 단어들을 분리한다.

'_analyze' API를 사용하여 테스트해보자
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=whitespace&pretty' -d 'Around the World in Eighty Days'
{
  "tokens" : [ {
    "token" : "Around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "World",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "Eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "Days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}
'tokenizer'라는 매개변수를 지정했고 결과는 보는 바와 같다. 
'Around th World in eighty Days' 문장이 공백 기준으로 분리된다.

하나만 더 살펴보자, letter 토크나이저는 공백뿐만아니라 특수문자를 기준으로 토큰을 분리한다. 다음과 같다.
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=letter&pretty' -d 'Around&the*World#in^Eighty@Days'
{
  "tokens" : [ {
    "token" : "Around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "World",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "Eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "Days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}
whitespace 토크나이저와 같은 결과이지만, '특수문자'로 토큰이 분리된것을 볼 수 있다.

더 많은 토크나이저는 아래를 참고하자
https://www.elastic.co/guide/en/elasticsearch/reference/1.4//analysis-tokenizers.html

2.2 Token Filter


분리된 토큰들을 가공한다. 토큰 필터는 마찬가지로 _analyze api로 테스트해 볼 수 있는데, 'filters'라는 매개변수를 통해 지정한다.

먼저 대표적인 필터인 'lowercase' 토큰필터를 테스트해보자.
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=whitespace&filters=lowercase&pretty' -d 'Around the World in Eighty Days'
{
  "tokens" : [ {
    "token" : "around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "world",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}

토크나이저는 whitespace를 사용했다. 분리된 토큰들이 모두 '소문자'로 바뀐것을 볼 수 있다.

더많은 필터들은 아래를 참고하자
https://www.elastic.co/guide/en/elasticsearch/reference/1.4//analysis-tokenfilters.html

3. 사용자 지정 Analyzer



이러한 다양한 토크나이저와 필터들을 조합하여 사용자 지정 분석기를 사용할 수 있다.
이러한 분석기는 인덱스를 생성할 때 아래와 같이 등록 할 수 있다.
$ curl -XPUT 'localhost:9200/test_index?pretty' -d
'{
   "settings" : {
      "analysis" : {
         "analyzer" : {
            "<analyzer_name>" : {
               "tokenizer" : "<tokenizer_name>",
               "filter" : ["<filter1_name>", "<filter2_name>", ... ]
            }
         }
       }
    }
}
자, 이제 'whitespace' 토크나이저와 'snowball', 'lowercase', 'synonym' 이렇게 3가지의 토큰 필터를 적용한 사용자지정 분석기를 적용 해보자.

먼저 아래와같이 'test_index'를 생성할때 분석기를 지정한다.
$ curl -XPUT 'localhost:9200/test_index?pretty' -d '{
   "settings" : {
      "analysis" : {
         "analyzer" : {
            "my_analyzer" : {
               "tokenizer" : "whitespace",
               "filter" : ["snowball", "lowercase", "my_filter"]
            }
         },
         "filter" : {
            "my_filter" : {
               "type" : "synonym",
               "synonyms" : ["quick, fast", "jump, hop => hop"]
            }
         }
      }
   }
}'

'synonyms' 필터는 따로 옵션을 설정해야하는 필터이므로, 'filter' 설정을 통해서 먼저 사용자지정 필터를 만들고나서, analyzer를 구성할때 등록한다

이렇게 지정한 분석기를 테스트해보자.
$ curl -XPOST 'localhost:9200/test_index/_analyze?analyzer=my_analyzer&pretty' -d 'The Quick Rabbit Jumped'
{
  "tokens" : [ {
    "token" : "the",
    "start_offset" : 0,
    "end_offset" : 3,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "quick",
    "start_offset" : 4,
    "end_offset" : 9,
    "type" : "SYNONYM",
    "position" : 2
  }, {
    "token" : "fast",
    "start_offset" : 4,
    "end_offset" : 9,
    "type" : "SYNONYM",
    "position" : 2
  }, {
    "token" : "rabbit",
    "start_offset" : 10,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "hop",
    "start_offset" : 17,
    "end_offset" : 21,
    "type" : "SYNONYM",
    "position" : 4
  } ]
}

먼저 whitespace 토크나이저가 공백으로 단어들을 분리했다. 그리고 'lowercase'가 각 단어들을 소문자로 바꾸었다. 그리고 'synonym' 토큰필터에 등록한대로, quick이라는 단어의 동의어로 'fast'라는 단어도 색인되었다.

흥미로운점은 Jumped라는 단어가 'jump'로 인식되어 hop으로 색인되었다는 것이다.
우리가 synonym 토큰필터에는 단지 'jump, hop => hop' 으로 설정하여, jump 또는 hop이라는 단어를 그냥 hop으로 색인되도록 설정했을뿐인데 어떻게 'Jumped'가 'jump'로 인식이 되었을까?

lowercasing을 거쳐서 jumped라고 색인되었겠지만 그렇다고 'jump'와 완전히 같은 단어는 아니다. 비밀은 'snowball'이라는 토큰필터였다. 이 'snowball'이라는 토큰필터는 다양한 언어의 형태소 분석을 수행한다.

정리하자면, 'Jumped'라는 단어는 lowercasing 필터를 거쳐 'jumped'로, snowball 필터를 거쳐서 뿌리가되는 단어인 'jump'로 인식이 되었고, synonym 필터를 거쳐서 동의어인 'hop'로 색인된 것이다!

4. 결론



이렇게 Elasticsearch는 토크나이저와 토큰필터를 이용해 Full Text를 분석하여 Inverted Index를 만든다. 이를 통해 Full Text Search를 가능하게 하는것이다.

'검색엔진' 이라는것이 처음에는 생소하고 막연했지만, Elasticsearch 를 공부하면서 모든것은 아니지만 어느정도 검색엔진이 어떻게 동작하는지 이해할 수 있는것 같다.

이번 Elasticsearch Analyzer를 공부하면서 더욱더 그 이해가 깊어질 수 있었다.

댓글 없음:

댓글 쓰기