Study/elasticsearch

Elasticsearch: 애널라이저, 토크나이저, 노멀라이저

voider 2023. 8. 5. 18:18

애널라이저는 9개 이상의 캐릭터 필터, 1개의 토크나이저, 0개 이상의 토큰 필터로 구성된다. 동작 역시 캐릭터 필터 -> 토크나이저 -> 토큰 필터 순서로 수행된다. 애널라이저는 입력한 텍스트에 캐릭터 필터를 적용하여 문자열을 변형시킨 뒤 토크나이저를 적용하여 여러 토큰으로 쪼갠다. 쪼개진 토큰의 스트림에 토큰 필터를 적용해서 토큰에 특정한 변형을 가한 결과가 최종적으로 분석 완료된 텀이다.

엘라스틱서치는 애널라이저의 동작을 테스트할 수 있는 API를 제공한다.

example

GET _analyze
{
  "analyzer": "standard",
  "text": ["Hello, HELLO, World!"]
}

//result
{
  "tokens": [
    {
      "token": "hello",
      "start_offset": 0,
      "end_offset": 5,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "hello",
      "start_offset": 7,
      "end_offset": 12,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "world",
      "start_offset": 14,
      "end_offset": 19,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

character filter

캐릭터 필터는 텍스트를 캐릭터 스트림으로 받아서 특정한 문자를 추가, 변경, 삭제한다. 애널라이저에는 0개 이상의 캐릭터 필터를 지정할 수 있다. 여러 캐릭터 필터가 지정됐다면 순서대로 수행된다.

tokenizer

토크나이저는 캐릭터 스트림을 받아서 여러 토큰으로 쪼개어 토큰 스트림을 만든다. 애널라이저에는 한 개의 토크나이저만 지정할 수 있다.

standard tokenizer

텍스트를 단어 단위로 나누는 가장 기본적인 토크나이저다. 대부분의 문장 부호가 사라진다. 필드 매핑엩 ㅡㄱ정 애널라이저를 지정하지 않으면 기본값으로 standard 애널라이저가 적용된다. 스탠다드 애널라이저가 사용하는 토크나이저가 스탠다드 토크나이저다.

keyword tokenizer

들어온 텍스트를 쪼개지 않고 그대로 내보낸다.

GET _analyze
{
  "tokenizer": "keyword",
  "text": ["Hello, HELLO, World!"]
}

//result
{
  "tokens": [
    {
      "token": "Hello, HELLO, World!",
      "start_offset": 0,
      "end_offset": 20,
      "type": "word",
      "position": 0
    }
  ]
}

토크나이저에서 특별한 동작을 수행하지 않기 때문에 쓸모없어 보이지만, 여러 캐릭터 필터, 토큰 필터와 조합하면 다양한 커스텀 애널라이저 지정이 가능하다고 한다.

ngram tokenizer

텍스트를 min_ngram 값 이상 max_ngram 값 이하 단위로 쪼갠다. min_gram: 2, max_gram=3으로 지정한 뒤, hello라는 텍스트를 토크나이징 하면 "he", "hel", "el", "hell", "ll", "llo" 총 5개의 토큰이 나온다.

ngram example

GET _analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 3,
    "max_gram": 4
  },
  "text": ["Hello, World!"]
}
//result
{
  "tokens": [
    {
      "token": "Hel",
      "start_offset": 0,
      "end_offset": 3,
      "type": "word",
      "position": 0
    },
    {
      "token": "Hell",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 1
    },
    {
      "token": "ell",
      "start_offset": 1,
      "end_offset": 4,
      "type": "word",
      "position": 2
    },
    {
      "token": "ello",
      "start_offset": 1,
      "end_offset": 5,
      "type": "word",
      "position": 3
    },
    {
      "token": "llo",
      "start_offset": 2,
      "end_offset": 5,
      "type": "word",
      "position": 4
    },
    {
      "token": "llo,",
      "start_offset": 2,
      "end_offset": 6,
      "type": "word",
      "position": 5
    },
    {
      "token": "lo,",
      "start_offset": 3,
      "end_offset": 6,
      "type": "word",
      "position": 6
    },
    {
      "token": "lo, ",
      "start_offset": 3,
      "end_offset": 7,
      "type": "word",
      "position": 7
    },
    {
      "token": "o, ",
      "start_offset": 4,
      "end_offset": 7,
      "type": "word",
      "position": 8
    },
    {
      "token": "o, W",
      "start_offset": 4,
      "end_offset": 8,
      "type": "word",
      "position": 9
    },
    {
      "token": ", W",
      "start_offset": 5,
      "end_offset": 8,
      "type": "word",
      "position": 10
    },
    {
      "token": ", Wo",
      "start_offset": 5,
      "end_offset": 9,
      "type": "word",
      "position": 11
    },
    {
      "token": " Wo",
      "start_offset": 6,
      "end_offset": 9,
      "type": "word",
      "position": 12
    },
    {
      "token": " Wor",
      "start_offset": 6,
      "end_offset": 10,
      "type": "word",
      "position": 13
    },
    {
      "token": "Wor",
      "start_offset": 7,
      "end_offset": 10,
      "type": "word",
      "position": 14
    },
    {
      "token": "Worl",
      "start_offset": 7,
      "end_offset": 11,
      "type": "word",
      "position": 15
    },
    {
      "token": "orl",
      "start_offset": 8,
      "end_offset": 11,
      "type": "word",
      "position": 16
    },
    {
      "token": "orld",
      "start_offset": 8,
      "end_offset": 12,
      "type": "word",
      "position": 17
    },
    {
      "token": "rld",
      "start_offset": 9,
      "end_offset": 12,
      "type": "word",
      "position": 18
    },
    {
      "token": "rld!",
      "start_offset": 9,
      "end_offset": 13,
      "type": "word",
      "position": 19
    },
    {
      "token": "ld!",
      "start_offset": 10,
      "end_offset": 13,
      "type": "word",
      "position": 20
    }
  ]
}

min_gram 3, max_gram 4로 지정하고 "Hello, World!"라는 문자열을 토크나이징 하면 총 21개의 토큰이 나온다. 여기에는 사실상 무의미한 공백이나 문장 부호 같은 토큰도 포함됐다. 이런 문제를 피하기 위해 ngram 토크나이저에는 token_chars라는 속성을 통해 토큰에 포함시킬 타입의 문자를 지정할 수 있다.

token_chars

  • Letter: 언어의 글자로 분류되는 문자
  • Digit: 숫자로 분류되는 문자
  • whitespace: 띄어쓰기 또는 줄바꿈 문자 등 공백으로 인식되는 문자
  • punctutation: 문장 부호
  • symbol: 기호
  • custom: custom_token_chars 설정을 통해 따로 지정한 커스텀 문자
GET _analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 3,
    "max_gram": 4,
    "token_chars": ["Letter"]
  },
  "text": ["Hello, World!"]
}

// result
{
  "tokens": [
    {
      "token": "Hel",
      "start_offset": 0,
      "end_offset": 3,
      "type": "word",
      "position": 0
    },
    {
      "token": "Hell",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 1
    },
    {
      "token": "ell",
      "start_offset": 1,
      "end_offset": 4,
      "type": "word",
      "position": 2
    },
    {
      "token": "ello",
      "start_offset": 1,
      "end_offset": 5,
      "type": "word",
      "position": 3
    },
    {
      "token": "llo",
      "start_offset": 2,
      "end_offset": 5,
      "type": "word",
      "position": 4
    },
    {
      "token": "Wor",
      "start_offset": 7,
      "end_offset": 10,
      "type": "word",
      "position": 5
    },
    {
      "token": "Worl",
      "start_offset": 7,
      "end_offset": 11,
      "type": "word",
      "position": 6
    },
    {
      "token": "orl",
      "start_offset": 8,
      "end_offset": 11,
      "type": "word",
      "position": 7
    },
    {
      "token": "orld",
      "start_offset": 8,
      "end_offset": 12,
      "type": "word",
      "position": 8
    },
    {
      "token": "rld",
      "start_offset": 9,
      "end_offset": 12,
      "type": "word",
      "position": 9
    }
  ]
}

token_chars 설정에 letter를 지정하면 문장 부호와 공백을 제거한 문자열만 토크나이징 한다.
여기서 공백 문자를 전후해 위치한 문자인 oWo 같은 토큰은 포함되지 않는다. token_chars 속성에 letter만을 포함하게 지정했으므로 공백 문자를 무시하고 "oWo" 토큰을 생성해야 할 것 같지만 결과는 그렇지 않다.

ngram 토크나이저는 먼저 token_chars에 지정되지 않은 문자를 기준으로 텍스트를 쪼갠다. 이렇게 하면 token_chars에 지정되지 않은 문자는 자연스럽게 최종 결과에 포함되지 않는다. 그 다음 단어를 min_gram 이상 max_gram 이하의 문자 길이를 가진 토큰으로 쪼갠다.

ngram 토크나이저는 엘라스틱서치에서 RDB로 따지자면 LIKE 검색과 유사한 검색을 구현하고 싶을 때, 자동 완성 관련 서비스를 구현하고 싶을 때 활용한다.

min_gram과 max_gram의 값 차이가 2 이상으로 벌어지면 분석 시도가 실패한다. 이 제한은 index.max_ngram_diff 설정에서 지정할 수 있다.

edge_ngram tokenizer

edge_ngram 토크나이저는 ngram 토크나이저와 유사하다. 입력된 텍스트를 token_chars에 지정되지 않은 문자 기준으로 삼아 단어 단위로 쪼갠다. 그 다음 각 단어를 min_gram 이상 max_gram 이하의 문자 길이를 가진 토큰으로 쪼갠다. 하지만 ngram 토크나이저와 다르게 생성된 모든 토큰의 시작 글자를 단어의 시작 글자로 고정시켜서 생성한다. edge_ngram 토크나이저에서 토큰의 시작은 단어의 시작 글자여야 한다.

example

GET _analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 3,
    "max_gram": 4,
    "token_chars": ["letter"]
  },
  "text": ["Hello, World!"]
}

// result
{
  "tokens": [
    {
      "token": "Hel",
      "start_offset": 0,
      "end_offset": 3,
      "type": "word",
      "position": 0
    },
    {
      "token": "Hell",
      "start_offset": 0,
      "end_offset": 4,
      "type": "word",
      "position": 1
    },
    {
      "token": "Wor",
      "start_offset": 7,
      "end_offset": 10,
      "type": "word",
      "position": 2
    },
    {
      "token": "Worl",
      "start_offset": 7,
      "end_offset": 11,
      "type": "word",
      "position": 3
    }
  ]
}

토큰 필터

토큰 필터는 토큰 스트림을 받아서 토큰을 추가, 변경, 삭제한다. 하나의 애널라이저에 토큰 필터를 0개 이상 지정할 수 있다. 토큰 필터가 여러 개 지정된 경우에는 순차적으로 적용된다. 엘라스틱서치는 몇 가지 빌트인 토큰 필터를 제공한다.

  • lowercase / uppercase
  • stop: 불용어 지정하여 제거
  • synonym: 유의어 사전을 지정하여 지정된 유의어를 치환한다
  • pattern_replace: 정규식을 사용하여 토큰의 내용을 치환한다
  • stemmer: 지원되는 몇몇 언어의 어간 추출을 수행. 한국어 지원X
  • trim: 토큰 전후에 위치한 공백 제거
  • truncate: 지정한 길이로 토큰을 자른다.

analyze API에서 토큰 필터도 테스트 할 수 있다.

GET _analyze
{
  "filter": ["lowercase"],
  "text": ["Hello, World!"]
}

//result
{
  "tokens": [
    {
      "token": "hello, world!",
      "start_offset": 0,
      "end_offset": 13,
      "type": "word",
      "position": 0
    }
  ]
}

built in analyzer

애널라이저는 캐릭터 필터, 토크나이저, 토큰 필터를 조합하여 구성된다. 엘라스틱서치에는 내장 캐릭터 필터, 토크나이저, 토큰 필터를 조합하여 미리 만들어 둔 다양한 내장 애널라이저가 있다.

  • standard analyzer: standard tokenizer + lowercase token filter로 구성된다. 애널라이저를 지정하지 않으면 적용되는 기본 애널라이저다.
  • simple analyzer: letter가 아닌 문자 단위로 토큰을 쪼갠 뒤 lowercase 토큰 필터를 적용한다.
  • whitespace: analyzer: whitespace tokenizer로 구성된다. 공백 문자 단위로 토큰을 쪼갠다.
  • stop analyzer: standard 애널라이저와 같지만 뒤에 stop 토큰 필터를 적용해서 불용어 제거함
  • pattern analyzer: keyword 토크나이저로 구성됨. 분석하지 않고 큰 토큰을 그대로 반환
  • 등등

인덱스 매핑에 적용

PUT analyzer_test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": {
          "type": "keyword" // default analyzer 지정
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "defaultText": {
        "type": "text"
      },
      "standardText": {
        "type": "text",
        "analyzer": "standard" // default analyzer와 다른 애널라이저 지정
      }
    }
  }
}

노멀라이저

노멀라이저는 애널라이저와 비슷한 역할을 하나 적용 대상이 text 타입이 아닌 keyword 타입 필드라는 차이가 있다. 또한 애널라이저와 다르게 단일 토큰을 생성한다.

노멀라이저는 토크나이저 없이 캐릭터 필터, 토큰 필터로 구성된다. 또한 캐릭터 필터와 토큰 필터를 모두 조합할 수 있는 것은 아니다. 최종적으로 단일 토큰을 생성해야 하기 때문에 글자 단위로 작업을 수행하는 필터만 사용할 수 있다. 엘라스틱서치에서 제공하는 빌트인 노멀라이저는 lowercase밖에 없다. 다른 방법으로 keyword 타입 필드를 처리하려면 커스텀 노멀라이즈를 사용해야 한다.

'Study > elasticsearch' 카테고리의 다른 글

elasticsearch join type field  (0) 2023.08.05
Elasticsearch object와 nested 타입 비교  (2) 2023.08.05