top bar

글 목록

레이블이 analysis인 게시물을 표시합니다. 모든 게시물 표시
레이블이 analysis인 게시물을 표시합니다. 모든 게시물 표시

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를 공부하면서 더욱더 그 이해가 깊어질 수 있었다.

2015년 5월 30일 토요일

[Elasticsearch] Inverted Index

개요



 이전 포스팅에서, Elasticsearch의 'Full Text Search' 라는 것이 어떠한 관점에서 수행되는지 살펴 보았다.

 몇개의 검색어로 자신이 찾고자 하는 정보를 정확하게 찾기는 매우 힘들겠지만, 사용자는 자신이 의도한 바와 '가까운' 검색결과를 얻기를 원한다. 이러한 정확도가 높을 수록 좋은 검색엔진인 것이다.

 이러한 'Full Text Search'라는 맥락에서, Elasticsearch는 정보를 색인할때 'Inverted Index' 라는 구조를 생성한다. 이것이 무엇일까? 오늘은 이 'Inverted Index' 라는것이 무엇인지 살펴 보고자 한다.

Inverted Index



원문 - https://www.elastic.co/guide/en/elasticsearch/guide/master/inverted-index.html

 이 개념을 이해하기는 쉽지 않았다. 인덱스면 인덱스지 'Inverted' 인덱스라..? 우리말로 억지로 해석하자면 '역인덱스' 정도가 되는건가? 아무튼 공식 가이드에 아주 친절하게 설명되어있다. (발 번역은 양해바란다)

 가령 2개의 document가 있고 'content'라는 field를 가지고 있다고 하자. 각 document의 content field에는 아래와같은 문장들이 각각 저장되어 있다.
 1. The quick brown fox jumped over the lazy dog
 2. Quick brown foxes leap over lazy dogs in summer
이때 Elasticsearch는 Inverted Index를 생성하기 위해 각 문장의 단어(term 또는 token이라고도 한다)를 분리하고 정렬한 다음, 각 단어가 어느 document에 있는지 표시한다. 그렇게되면 아래와같은 표가 완성될 것이다.
Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------
자, 이제 'quick brown' 이라는 검색어를 입력하였다고 치자.
그러면 아래와 같은 매칭 리스트가 만들어질 것이다.
Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1
'brown'과 'quick' 이란 단어는 Doc_1에서 는 모두 검색이 되었고, Doc_2에서는 'brown'이라는 단어 하나만 검색이 되었다. 따라서 Doc_1이라는 문서가 우리가 의도했던 검색결과에 가장 가까운 문서가 될 것이다.

이것이 'Inverted Index'를 생성해 검색을 수행하는 기본적인 원리이다. 그렇지만 계속 드는 의문중 하나, 왜 'Inverted' 일까? 그냥 인덱스와 '역'인덱스는 무슨 차이가 있을까? 아래 URL을 참고하자.

http://www.quora.com/Information-Retrieval/What-is-inverted-index

정말 헷갈렸지만, 위 URL에서 말하고자 하는바를 간단히 요약하자면,

  • 인덱스 = 책 앞부분의 목차
  • 역인덱스 = 책 뒷부분의 색인

인 것 이다!! 왠만한 기술서적은 맨 뒷부분에 해당 단어가 어떤 페이지에 나와있는지를 list up 한 '색인' 이라는 페이지가 있다. 바로 이것이 '역'인덱스 인것이다. 예를 들어 아래사진과 같다!


한마디로 걍(?)인덱스는 페이지를 어떠한 topic에 매핑 시킨다. 하지만 '역'인덱스는 단어(term)를 페이지에 매핑 시키는것이다. 어떻게보면 서로 반대되는 개념이다. 그래서 '역'이라는 단어가 붙었나 보다.

책 전체 에서 어떠한 단어를 찾을때 책 첫장부터 한장씩 넘기는 멍청한짓을 하는 사람은 없을 것이다. 따라서 이러한 '역'인덱스는 단어를 통해서 문서를 찾는 Full Text Search 에 최적의 속도를 내는 방식이라고 할 수 있다.

하지만 위의 예제는 아래와 같은 몇가지 문제점이 있다.

  • 'Quick'과 'quick'은 따로 리스트에 있지만 사실 같은 단어이다 (대소문자)
  • 'fox'와 'foxes'는 매우 비슷한 단어이고, 'dog'와 'dogs'도 마찬가지다. (복수형)
  • 'jumped'와 'leap'이라는 단어도 같은 의미이기에 'jumped'으로도 'leap' 매칭되어야 하는것이 아닌가? (동의어)

만약의 위의 역인덱스를 바탕으로 'Quick fox' 를 검색한다면 (두개의 단어가 AND 조건일때) 검색결과는 하나도 나타나지 않을 것이다.

 Doc_1은 'quick fox' 가 포함되어있고 Doc_2는 'Quick foxes' 가 포함되어 있기때문이다. 따라서 어디에도 매칭되지 않는다.

이러한 문제를 해결하기 위해 Elasticsearch는 'Token Filter'라는 모듈을 제공하여 단어를 가공한다. 예를들어 아래와 같은 작업을 하는 것이다.

  • 대문자로 시작하는 'Quick'은 lowercasing하여 'quick'으로 색인한다.
  • 'foxes'와 'dogs'는 그 뿌리가 되는 단어인 'fox'와 'dog'으로 색인한다.
  • 'jumped'와 'leap'은 동의어 이므로, 하나의 단어인 'jump'로 색인한다.

이러한 작업을 마치게되면 아래와같은 역인덱스가 완성된다.
Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
dog     |   X   |  X
fox     |   X   |  X
in      |       |  X
jump    |   X   |  X
lazy    |   X   |  X
over    |   X   |  X
quick   |   X   |  X
summer  |       |  X
the     |   X   |  X
------------------------
색인된 단어들이 정제되고 가공 되었다. 하지만 여전히 'Quick fox'는 검색되지 않는다. 가공하는 과정에서 'Quick'이라는 단어는 lowercasing 때문에 색인되지 않았기 때문이다. 이때 Elasticsearch는 위의 필터링규칙을 우리가 사용한 query string에 그대로 적용시킨다. 그렇게되면 우리가 입력한 검색어는 'quick fox'로 가공되고 그때야 비로소 Doc_1, Doc_2 두개의 문서가 검색결과로 나타나게 될 것이다.

이것이 Elasticsearch의 'Anaysis' 과정이다.

대략 두가지의 과정 즉, 단어의 분리와 가공의 과정을 거치는데 전자는 'Tokenizer', 후자는 'Token Filter'가 담당한다. 이러한 'Analysis'의 구체적인 과정은 다음 포스팅으로 미루겠다. 어짜피 나도 공신력(?) 있는 Elasticsearch 공식문서에서 거의 모든 정보를 얻고있다.

참고하자. https://www.elastic.co/guide/index.html

PS : 이러한 Inverted Index 원리는 비단 Elasticsearch에서만 사용되는것이 아니다. 그러므로 잘 알아두면 분명 좋을 것이라고 믿는(?)다.