Elasticsearch Analyzer(문장 분석)

Analyzer

Analyzer는 문장을 단어(term) 단위로 나누어 준다.

이 장에서는 다음 항목에 대해 설명한다.

  • Analyzer의 구성 요소
  • 노리(nori) 한글 형태소 분석기
  • Custom Analyzer(Analyzer 사용자 지정)

왜 Analyzer를 사용하여 단어로 나누어야 하는가?

“도큐먼트를 정확히 검색할 수 없으니까” 이게 답변이지만, 좀 더 구체적으로 예를 보면서 알아보도록 하겠다.

검색용 문장 준비

POST demo_standard_analyzer/_doc
{
  "text":"올해 서울시 예산이 결정되었다."
}

위의 문서에 대해 “서울"라는 단어로 검색해 보겠다.

GET demo_standard_analyzer/_search
{
  "query" : {
     "match" : {
       "text" : "서울"
    }
  }
}

검색 결과는 아래와 같이 조회되지 않는다.

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

“서울"이라는 단어로 “서울시"의 문서가 조회(hit)되지 않는다. “서울시"의 “서울” 부분만으로 조회되지 않기 때문이다.

analyze API를 사용하여 문서 문자열이 Analyzer에 의해 어떻게 문장이 분해되어 역색인에 저장되어 있는지 확인할 수 있으므로 원인을 조사해 보겠다.

기본 Standard Analyzer로 문장 분해

POST demo_standard_analyzer/_analyze
{
  "text": "올해 서울시 예산이 결정되었다."
}

기본 Standard Analyzer로 문장 분해된 결과

{
  "tokens" : [
    {
      "token" : "올해",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "<HANGUL>",
      "position" : 0
    },
    {
      "token" : "서울시",
      "start_offset" : 3,
      "end_offset" : 6,
      "type" : "<HANGUL>",
      "position" : 1
    },
    {
      "token" : "예산이",
      "start_offset" : 7,
      "end_offset" : 10,
      "type" : "<HANGUL>",
      "position" : 2
    },
    {
      "token" : "결정되었다",
      "start_offset" : 11,
      "end_offset" : 16,
      "type" : "<HANGUL>",
      "position" : 3
    }
  ]
}

“token” 키의 값을 볼 수 있듯이 Standard Analyzer는 문장을 한 문자씩 구분하여 역섹인에 저장되고 있다.

그러므로 제대된 조회를 하기 위해서는 한글을 대응되는 Analyzer 로 문장을 단어로 분해해야 한다.

Analyzer의 구성 요소

Analyzer는 다음 세 가지 요소로 구성된다.

  • Char Filter: 문장 변환
  • Tokenizer: 문장을 Token(단어)으로 분할
  • Token filter: Token(단어) 변환

Analyzer는 3가지 요소를 위에서 부터 순서대로 처리하여 문장을 Token(단어)로 분해하여 역색인을 생성한다.

Char Filter: 문장 변환의 예

  • HTML Strip Character Filter
    • HTML 태그를 제거합니다.
    • <p>foo</p> –(변환)–> foo

Tokenizer: 문장을 Token(단어)으로 분할하는 예

  • Standard Tokenizer(기본 토크나이저)
  • Nori Tokenizer(노리 토크나이저)

Token filter: Token(단어) 변환의 예

  • Lower case Token Filter
    • 토큰의 문자를 모두 소문자로 변환한다.
    • Google과 google을 동일한 토큰으로 보고 싶을 때 사용한다.
  • Stop Token Filter
    • 사용하지 않는 토큰을 삭제한다.
    • 조사 삭제 등에 이용된다.
  • Stemmer Token Filter
    • 스티밍 처리를 한다.
    • ‘즐겁게’과 ‘즐겁다’을 ‘즐거움’으로 변환한다.
  • Synonym Token Filter
    • 동의어를 정규화합니다.
    • ‘구글’와 ‘검색’을 같은 ‘검색’으로 변환한다.

노리(nori) 한글 형태소 분석기

Elasticsearch의 기본 Standard analyzer는 한글를 지원하지 않는다.

한글 검색을 올바르게 수행하려면 nori Analysis Plugin이라는 한글 형태소 분석용 플러그인을 사용하여 문장을 단어로 분할해야 한다.

nori Analysis Plugin 설치

nori Analysis Plugin 설치 명령어

bin/elasticsearch-plugin install analysis-nori

아래는 도커에서 실행한 내용이다.

sh-4.4# bin/elasticsearch-plugin install analysis-nori
-> Installing analysis-nori
-> Downloading analysis-nori from elastic
[=================================================] 100%?? 
-> Installed analysis-nori
-> Please restart Elasticsearch to activate any plugins installed
sh-4.4# 

참고로, 반대로 플러그인 삭제하는 방법은 아래와 같다. nori Analysis Plugin 설치 명령어

bin/elasticsearch-plugin remove analysis-nori

설치를 하고 나서는 nori 플러그인을 사용하도록 Elasticsearch를 다시 시작해야 한다.

systemctl 명령어를 사용하는 방법은 아래와 같다.

systemctl restart elasticsearch

설치된 환경에 따라 재시작하는 방법은 다르니 각각 환경에 맞게 재 시작하기 바란다.

Nori Analyzer 구성

  • Char Filter
    • 없음
  • Tokenizer
    • nori_tokenizer
  • Token filter
    • nori_part_of_speech
    • nori_readingform

Nori Tokenizer 사용 방법

문자열만 nori 토크분해 보자.

Nori Tokenizer로 문장 분석

GET _analyze
{
  "tokenizer": "nori_tokenizer",
  "text": [
    "올해 서울시 예산이 결정되었다."
  ]
}

Nori Tokenizer로 문장 분석 결과

{
  "tokens" : [
    {
      "token" : "올",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "해",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "서울",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "시",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "예산",
      "start_offset" : 7,
      "end_offset" : 9,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "이",
      "start_offset" : 9,
      "end_offset" : 10,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "결정",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "word",
      "position" : 6
    },
    {
      "token" : "되",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "었",
      "start_offset" : 14,
      "end_offset" : 15,
      "type" : "word",
      "position" : 8
    },
    {
      "token" : "다",
      "start_offset" : 15,
      "end_offset" : 16,
      "type" : "word",
      "position" : 9
    }
  ]
}

Nori Analyzer 사용 방법

실제로 demo_analyzer 인덱스에서 Nori Analyzer를 설정해 보자.

analyzer에 nori을 지정

PUT demo_analyzer
{
  "mappings" : {
     "properties" : {
       "text" :{
         "type" : "text" ,
         "analyzer" : "nori"
      }
    }
  }
}

다음으로 demo_analyzer 인덱스로 문서를 작성해, 한글 검색을 실행해 본다.

도큐먼트 생성

POST demo_analyzer/_doc
{
  "text": "올해 서울시 예산이 결정되었다."
}

“울시” 검색

GET demo_analyzer/_search
{
  "query" : {
     "match" : {
       "text" : "울시"
    }
  }
}

“울시” 검색 결과

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

“울시"이라는 단어로 “서울시"가 조회되지 되지 않는다. 정확히 다른 단어로 인식하는 것 같다.

nori Analyzer으로 어떤 역색인이 생성되는지 확인해 보자.

nori Analyzer에 형태소 분석

GET demo_analyzer/_analyze
{
  "analyzer" : "nori", 
  "text" : "올해 서울시 예산이 결정되었다."
}

nori Analyzer에 형태소 분석 결과

{
  "tokens" : [
    {
      "token" : "올",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "word",
      "position" : 0
    },
    {
      "token" : "해",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "서울",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "시",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "예산",
      "start_offset" : 7,
      "end_offset" : 9,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "결정",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "word",
      "position" : 6
    }
  ]
}

“서울"와 “시"을 분석해 주는 것 같다. 그래서 “서울시"에 “울시"가 조회되지 않게 된 것이다.

Custom Analyzer(Analyzer 사용자 지정)

Analyzer는 다음 3가지 요소를 결합하여 직접 자유롭게 커스텀하여 만들 수 있다.

  • Char Filter
  • Tokenizer
  • Token filter

예를 들어, 다음과 같은 Custom Analyzer를 작성한다.

Custom Analyzer 생성

PUT custome_analyze
{
  "settings" : {
     "analysis" : {
       "analyzer" : {
         "original_analyze" :{
           "char_filter" :[ "html_strip" ],
           "tokenizer" : "nori_tokenizer" ,
           "filter" :[ "my_stop" ]
        }
      },
      "filter" :{
         "my_stop" :{
           "type" : "stop" ,
           "stopwords" :[ "올", "해" , "이" ]  
        }
      }
    }
  }
}

만든 Analyzer으로 문장 분석을 해보자.

Custom Analyzer 사용

POST custome_analyze/_analyze
{
  "analyzer" : "original_analyze",
  "text" : "<p>올해 서울시 예산이 결정되었다</p>"
}

Custom Analyzer 사용한 결과

{
  "tokens" : [
    {
      "token" : "서울",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "시",
      "start_offset" : 8,
      "end_offset" : 9,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "예산",
      "start_offset" : 10,
      "end_offset" : 12,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "결정",
      "start_offset" : 14,
      "end_offset" : 16,
      "type" : "word",
      "position" : 6
    },
    {
      "token" : "되",
      "start_offset" : 16,
      "end_offset" : 17,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "었",
      "start_offset" : 17,
      "end_offset" : 18,
      "type" : "word",
      "position" : 8
    },
    {
      "token" : "다",
      "start_offset" : 18,
      "end_offset" : 19,
      "type" : "word",
      "position" : 9
    }
  ]
}

출력 결과는 커스텀 Analyzer “original_analyze"에 정의된 다음과 같은 처리를 반영되었다.

  • “html_strip"은 HTML의 <p> 태그를 제거한다.
  • “nori_tokenizer"로 문장을 Token(단어)으로 변환한다.
  • “my_stop” 으로 정의한 “stopwords"에 의해, "올", "해" , "이"를 제거한다.

참조