Elastic - 색인과 텍스트 분석
🙊

Elastic - 색인과 텍스트 분석

Created
May 9, 2024 07:00 AM
Last edited time
Last updated May 10, 2024
Tags
ElasticSearch
Language
URL

Intro::

데이터 색인과 텍스트 분석에 대해서 알아봅시다.
 

역 인덱스 - Inverted Index

💡
전통적인 RDBMS 에서는 like 검색을 사용하기 떄문에 데이터가 늘어날수록 검색해야 할 대상이 늘어나 시간도 오래 걸리고, row안의 내용을 모두 읽어야 하기 때문에 기본적으로 속도가 느립니다. Elasticsearch의 경우 역 인덱스를 사용하여 이러한 문제를 해결할 수 있습니다.
 
  • 문서 1: "Quick brown fox"
  • 문서 2: "Lazy dog"
  • 문서 3: "Quick dog jumps"
 

역 인덱스 테이블

단어
문서 리스트
Quick
1, 3
brown
1
fox
1
Lazy
2
dog
2, 3
jumps
3
 

텍스트 분석 - Text Analysis

Elasticsearch는 문자열 필드가 저장될 때 데이터에서 검색어 토큰을 저장하기 위해 여러 단계의 처리 과정을 거칩니다. 이 전체 과정을 텍스트 분석(text analysis)이라고 하며 이 과정을 처리하는 기능을 Analyzer 라고 합니다. Elasticsearch의 애널라이저는 0-3개의 캐릭터 필터와 1개의 토크나이저, 그리고 0-n개의 토큰 필터로 이루어집니다.
notion image
 
💡
쉽게 설명을 하자면 토크나이저로 whitespace 단위로 토큰들을 분리한 뒤, 여러 조건들의 토큰 필터를 적용해 역 인덱스에 저장한다고 생각하면 된다.
 
토큰필터의 종류는 다음과 같다.
  • lowercase
    • 각 텀을 소문자로 바꿔준다. → 대소문자 텀들 병합
  • stop
    • 불용어라는 의미로, 영어에서 a, an, are, at, … 과 같은 가치 없는 단어들을 없애 주는 단계이다.
  • snowball
    • 텀을 기본형태로 변환하여 검색이 가능하게 합니다.
    • ~s, ~ing 와 같은 형태를 제거
  • synonym
    • 동의어로 저장하는 단계입니다.
 
 

애널라이저 Analyzer

_analyze API

// _analuzer API를 이용해서 텍스트 분 GET _analyze { "text": "The quick brown fox jumps over the lazy dog", "tokenizer": "whitespace", "filter": [ "lowercase", "stop", "snowball" ] }
 
💡
whitespace 토크나이저 그리고 lowercase, stop, snowball 토큰필터들을 조합한 것이 snowball 애널라이저 입니다. 때문에 위의 예제와 아래 예제의 결과는 동일합니다.
 
GET _analyze { "text": "The quick brown fox jumps over the lazy dog", "analyzer": "snowball" }
 

Term 쿼리

💡
match 쿼리와 문법은 유사하지만 term 쿼리는 입력한 검색어에 애널라이저를 적용하지 않고 입력된 검색어 그대로 일치하는 텀을 찾습니다. 쉽게 말하자면 필터까지 적용해서 역 인덱스화 된 텀으로 검색을 해야한다는 것이다.
 

사용자 정의 애널라이저 - Custom Analyzer

💡
실제 인덱스에 저장되는 데이터의 처리에 대한 설정은 애널라이저만 적용할 수 있습니다. 인덱스 매핑에 애널라이저를 적용할 때 보통은 사용자 정의 애널라이저를 주로 사용합니다.
PUT my_index3 { "settings": { "index": { "analysis": { "analyzer": { "my_custom_analyzer": { "type": "custom", "tokenizer": "whitespace", "filter": [ "lowercase", "stop", "snowball" ] } } } } } }
// 위와 같이 인덱스의 커스텀 애널라이저를 설정해 두면 해당 인덱스에서 사용할 수 있습니다. GET my_index3/_analyze { "analyzer": "my_custom_analyzer", "text": [ "The quick brown fox jumps over the lazy dog" ] }

사용자 정의 토큰 필터

💡
사용자 정의 애널라이저와 마찬가지로 토큰 필터도 사용자가 정의해서 사용할 수 있다.
// my_stop_filter 생성 후 my_custom_analyzer 에서 사용 PUT my_index3 { "settings": { "index": { "analysis": { "analyzer": { "my_custom_analyzer": { "type": "custom", "tokenizer": "whitespace", "filter": [ "lowercase", "my_stop_filter", "snowball" ] } }, "filter": { "my_stop_filter": { "type": "stop", "stopwords": [ "brown" ] } } } } } }
 

매핑에 사용자 정의 애널라이저 적용

💡
사용자 정의 애널라이저와 사용자 정의 토큰 필터를 적용하려면 매핑을 해주어야 한다. 매핑을 해주지 않는 경우 자동으로 기본 분석기를 사용한다.
PUT my_index3 { "settings": { "index": { "analysis": { "analyzer": { "my_custom_analyzer": { "type": "custom", "tokenizer": "whitespace", "filter": [ "lowercase", "my_stop_filter", "snowball" ] } }, "filter": { "my_stop_filter": { "type": "stop", "stopwords": [ "brown" ] } } } } }, "mappings": { "properties": { "message": { "type": "text", "analyzer": "my_custom_analyzer" } } } }
 
💡
인덱스에 도큐먼트를 추가할 때 역 색인화된다. 그렇다면 인덱스는 보통 역할이나 특징으로 분류되어져 있나?
엘라스틱서치의 인덱스는 특정 역할이나 특징에 따라 분류되고 구성될 수 있습니다. 인덱스는 엘라스틱서치에서 데이터를 저장하고 검색하는 기본 단위로, 주로 데이터의 특성, 용도, 접근 패턴에 따라 설계되며 이는 다양한 방식으로 구분될 수 있습니다.
 

텀 벡터 - _ termvectors API

💡
색인된 도큐먼트의 역 인덱스의 내용을 확인할 때는 도큐먼트 별로 _termvectors API를이용해서 확인이 가능합니다. GET <인덱스>/_termvectors/<도큐먼트id>?fields=<필드명> 형식으로 사용하며 6.x 이전 버전에서는 GET <인덱스>/<도큐먼트 타입>/<도큐먼트id>/_termvectors?fields=<필드명> 형식으로 사용합니다. 이해하기 쉽게 설명하자면 인덱스가 도큐먼트를 어떻게 역인덱스화 했는지 알려주는 API라고 생각하면 됩니다.
// my_index3/_doc/1 도큐먼트의 message 필드의 termvectors 확인 GET my_index3/_termvectors/1?fields=message
 

캐릭터 필터 - Character Filter

HTML Strip

💡
입력된 텍스트가 HTML 인 경우 태그들을 제거하여 일반 텍스트로 만듭니다. 또한 &nbsp; 같은 HTML 문법 용어들도 해석합니다. 입력 값은 html_strip 입니다.
// char_filter 를 이용해서 html 문장 처리 POST _analyze { "tokenizer": "keyword", "char_filter": [ "html_strip" ], "text": "<p>I&apos;m so <b>happy</b>!</p>" }
// 결과 { "tokens" : [ { "token" : """ I'm so happy! """, "start_offset" : 0, "end_offset" : 32, "type" : "word", "position" : 0 } ] }
 

Mapping

💡
C, C++ 와 같이 특수문자가 처리되었을때 같은 텀이 되는 경우 매핑을 통해 특수문자를 다른 문자로 치환해줄 수 있다. 예시로 특별한 처리를 해주지 않는 다면 C++ 의 텀은 C가 되므로 C++ ⇒ C_plus__plus_ 와 같은형태로 mapping를 사용해 준다고 이해하면 된다.
// coding 인덱스에 mapping 캐릭터 필터 설정 PUT coding { "settings": { "analysis": { "analyzer": { "coding_analyzer": { "char_filter": [ "cpp_char_filter" ], "tokenizer": "whitespace", "filter": [ "lowercase", "stop", "snowball" ] } }, "char_filter": { "cpp_char_filter": { "type": "mapping", "mappings": [ "+ => _plus_", "- => _minus_" ] } } } }, "mappings": { "properties": { "language": { "type": "text", "analyzer": "coding_analyzer" } } } }
 

Pattern Replace

💡
Pattern Replace 캐릭터 필터는 정규식(Regular Expression)을 이용해서 좀더 복잡한 패턴들을 치환할 수 있는 캐릭터 필터입니다.
// camel 인덱스에 pattern_replace 캐릭터 필터 설정 PUT camel { "settings": { "analysis": { "analyzer": { "camel_analyzer": { "char_filter": [ "camel_filter" ], "tokenizer": "standard", "filter": [ "lowercase" ] } }, "char_filter": { "camel_filter": { "type": "pattern_replace", "pattern": "(?<=\\p{Lower})(?=\\p{Upper})", "replacement": " " } } } } }
 

토크나이저

Standard, Letter, Whitespace

  • Standard
    • 공백으로 텀을 구분하고 일부 특수문자를 제거
    • GET _analyze { "tokenizer": "standard", "text": "THE quick.brown_FOx jumped! @ 3.5 meters." } // 텀 결과 "THE", "quick.brown_FOx", "jumped", "3.5", "meters"
  • Letter
    • 알파벳을 제외한 모든 공백, 숫자, 기호들을 기준으로 텀을 분리
    • GET _analyze { "tokenizer": "letter", "text": "THE quick.brown_FOx jumped! @ 3.5 meters." } // 텀 결과 "THE", "quick", "brown", "FOx", "jumped", "meters"
  • Whitespace
    • whitespace 기준으로 텀을 분리
    • GET _analyze { "tokenizer": "whitespace", "text": "THE quick.brown_FOx jumped! @ 3.5 meters." } // 텀 결과 "THE", "quick.brown_FOx", "jumped!", "@", "3.5", "meters."
💡
3개의 토크나이저 중에 Letter 토크나이저의 경우 검색 범위가 넓어져서 원하지 않는 결과가 많이 나올 수 있고, Whitespace의 경우 특수문자를 거르지 않기 때문에 정확하게 검색해야 원하는 결과가 나올 수 있습니다. 따라서 일반적으로 Standard 토크나이저를 많이 사용합니다.

UAX URL Email

💡
주로 사용되는 Standard 토크나이저의 경우 @, / 같은 특수문자를 기준으로도 분리하기 때문에 이메일 주소 또는 웹 URL 경로를 처리하는데 문제가 있습니다. 이를 해결하기 위한 방법이 UAX URL Emaill 토크나이저 입니다.
GET _analyze { "tokenizer": "uax_url_email", "text": "email address is my-name@email.com and website is https://www.elastic.co" } // 텀 결과 "email", "address", "is", "my-name@email.com", "and", "website", "is", "https://www.elastic.co"
 

Pattern

💡
일반적으로 공백을 기준으로 텀들을 분리하지만, 종종 사람이 읽는 문장이 아니라 서버 시스템이나 IoT 장비 등에서 수집된 머신 데이터인 경우 공백이 아닌 다른 문자들로 구분하는 경우가 존재합니다. 이럴 때 사용하는 것이 Pattern 토크나이저 입니다.
// pat_tokenizer 인덱스에 my_pat_tokenizer 토크나이저 생성 PUT pat_tokenizer { "settings": { "analysis": { "tokenizer": { "my_pat_tokenizer": { "type": "pattern", "pattern": "/" } } } } } // my_pat_tokenizer 토크나이저로 문장 분석 GET pat_tokenizer/_analyze { "tokenizer": "my_pat_tokenizer", "text": "/usr/share/elasticsearch/bin" } // 결과 "usr", "share", "elasticsearch", "bin",
 

Path Hierarchy

💡
디렉토리나 파일 경로 등에서 하위 디렉토리 명이 같은 경우 데이터 검색에 혼동이 올 수 있습니다. Path Hierarchy 토크나이저는 경로 데이터를 계층별로 저장해서 하위 디렉토리에 속한 도큐먼트들을 수준별로 검색하거나 집계하는 것이 가능합니다.
POST _analyze { "tokenizer": "path_hierarchy", "text": "/usr/share/elasticsearch/bin" } // 결과 "/usr", "/usr/share", "/usr/share/elasticsearch", "/usr/share/elasticsearch/bin"
delimiter 항목값으로 경로 구분자를 지정할 수 있습니다(디폴트 /). replacement 옵션을 이용해서 소스의 구분자를 다른 구분자로 대치해서 저장하는 것도 가능합니다.
PUT hir_tokenizer { "settings": { "analysis": { "tokenizer": { "my_hir_tokenizer": { "type": "path_hierarchy", "delimiter": "-", "replacement": "/"// - 를 / 로 대치한 기준으로 분 } } } } }
 

토큰 필터 - Token Filter

💡
토큰 필터는 토크나이저로부터 생성된 토큰들을 수정, 추가, 또는 제거하는 역할을 하며, 이 과정을 통해 최종적으로 검색에 사용될 토큰들을 조정합니다.
 

Lowercase, Uppercase

// lowercase 토큰 필터로 문장 분석 GET _analyze { "filter": [ "lowercase" ], "text": [ "Harry Potter and the Philosopher's Stone" ] } // 결과 { "tokens" : [ { "token" : "harry potter and the philosopher's stone", "start_offset" : 0, "end_offset" : 40, "type" : "word", "position" : 0 } ] }
 

Stop

💡
큰 의미 없는 조사나 전치사와 같은 불용어(stopword)를 텀에서 제거해주는 필터입니다. 불용어로 지정할 단어들을 배열 형태로 나열하거나, "_english_", "_german_" 같이 언어를 지정해서 해당 언어팩에 있는 불용어를 지정할 수도 있습니다. 텍스트 파일로 저장하고 경로를 지정해서도 사용가능합니다.
// my_stop 인덱스에 my_stop_filter 토큰필터 생성 PUT my_stop { "settings": { "analysis": { "filter": { "my_stop_filter": { "type": "stop", "stopwords": [ "in", "the", "days" ] // "stopwords_path": "user_dic/my_stop_dic.txt" } } } } }
// my_stop_filter 토큰 필터로 문장 분석 GET my_stop/_analyze { "tokenizer": "whitespace", "filter": [ "lowercase", "my_stop_filter" ], "text": [ "Around the World in Eighty Days" ] }
// 결과 { "tokens" : [ { "token" : "around", "start_offset" : 0, "end_offset" : 6, "type" : "word", "position" : 0 }, { "token" : "world", "start_offset" : 11, "end_offset" : 16, "type" : "word", "position" : 2 }, { "token" : "eighty", "start_offset" : 20, "end_offset" : 26, "type" : "word", "position" : 4 } ] }
 

Synonym

💡
동의어를 처리해주는 필터입니다.
PUT my_synonym { "settings": { "analysis": { "analyzer": { "my_syn": { "tokenizer": "whitespace", "filter": [ "lowercase", "syn_aws" ] } }, "filter": { "syn_aws": { "type": "synonym", "synonyms": [ "amazon => aws"// aws로 텀을 저장 // "amazon, aws"// 둘다 저장 ] // "synonyms_path": "user_dic/my_syn_dic.txt" } } } }, "mappings": { "properties": { "message": { "type": "text", "analyzer": "my_syn" } } } }
💡
term 쿼리는 검색어에 애널라이저를 적용하지 않고 그대로 검색하기 때문에 변환된 단어로 검색하지 않는다는 점을 주의해야 합니다. 하지만 match의 경우 애널라이저가 적용이 되므로 변환된 단어로도 검색이 됩니다.
동의어도 stopword와 마찬가지로 파일로 관리하는 것이 편하다.
 

NGram, Edge NGram, Shingle

NGram

💡
특정한 경우 단어의 일부만을 가지고 검색해야 하는 기능이 필요한 경우도 있습니다. wildcard 쿼리나 regexp 쿼리도 지원을 하지만, 이런 쿼리들은 메모리 소모가 많고 느리기 때문에 Elasticsearch의 장점을 활용하지 못합니다. 이를 해결하기 위해 NGram을 사용합니다.
  • house에 2글자의 NGram 적용
    • ho
    • ou
    • us
    • se
💡
텀의 개수가 기하급수적으로 늘어나기 때문에 일반적인 텍스트 검색보다는 카테고리 목록, 태크 목록과 같이 전체 개수가 많지 않은 데이터 집단에 자동완성 기능을 구현하는데 적합합니다.
 

Edge NGram

💡
검색의 경우 일반적으로 단어의 맨 앞부터 검색을 하기 때문에 텀의 앞쪽 ngram만 저장을 하는 방법이 Edge NGram 입니다.
  • house에 Edge NGram "min_gram": 1, "max_gram": 4 으로 설정
    • h
    • ho
    • hou
    • hous
 

Shingle

💡
문자가 아니라 단어 단위로 구성된 묶음을 Shingle이라고 합니다. NGram과 Edge Ngram 과 방식은 유사하지만 문장을 대상으로 적용한다고 이해하면 됩니다.
  • min_shingle_size / max_shingle_size : shingle의 최소 / 최대 단어 개수를 지정합니다. 디폴트는 모두 2 입니다.
  • output_unigrams : Shingle 외에도 각각의 개별 토큰(unigram)도 저장 하는지의 여부를 설정합니다. 디폴트는 true 입니다.
  • output_unigrams_if_no_shingles : shingle 을 만들 수 없는 경우에만 개별 토큰을 저장하는지의 여부를 설정합니다. 디폴트는 false 입니다.
  • token_separator : 토큰 구분자를 지정합니다. 디폴트는 " " (스페이스) 입니다.
  • filler_token : shing을 만들 텀이 없는 경우 (보통은 stop 토큰 필터와 함께 사용되어 offset 위치만 있고 텀이 없는 경우입니다) 대체할 텍스트를 지정합니다. 디폴트는 _ 입니다.
 
PUT my_shingle { "settings": { "analysis": { "filter": { "my_shingle_f": { "type": "shingle", "min_shingle_size": 3, "max_shingle_size": 4, "output_unigrams": false, "filler_token": "-" }, "my_stop_f": { "type": "stop", "stopwords": [ "is" ] } } } } }
// my_shingle 에서 "this is my sweet home" 분석 GET my_shingle/_analyze { "tokenizer": "whitespace", "filter": [ "my_stop_f", "my_shingle_f" ], "text": "this is my sweet home" }
// 토큰 결과 "this - my", "this - my sweet", "- my sweet", "- my sweet home", "my sweet home"
 

Unique

💡
"white fox, white rabbit, white bear" 같은 문장을 분석하면 "white" 텀은 총 3번 저장이 됩니다. 역 색인에는 텀이 1개만 있어도 텀을 포함하는 도큐먼트를 가져올 수 있기 때문에 중복되는 텀 들은 삭제 해 주어도 검색에는 보통 무방합니다. 이 경우 unique 토큰 필터를 사용해서 중복되는 텀 들은 하나만 저장하도록 할 수 있습니다.
 
GET _analyze { "tokenizer": "standard", "filter": [ "lowercase" ], "text": [ "white fox, white rabbit, white bear" ] }
 
💡
match 쿼리를 사용해서 검색하는 경우 unique 필터를 사용한다면 적용한 필드의 텀의 개수가 1개가 되기 때문에 TF값이 줄어 score가 달라질 수 있습니다. 때문에 match 쿼리를 이용해 relevancy를 따져야 하는 검색의 경우 unique 토큰 필터는 사용하지 않는 것이 좋습니다.
 

형태소 분석 - Stemming

💡
검색을 할 때는 문법에 따른 단어의 변형에 상관 없이 검색이 가능해야 하기 때문에 텍스트 데이터를 분석할 때 각각의 텀에 있는 단어들을 기본 형태인 어간을 추출하는 과정을 진행해야 합니다. 이 과정을 보통 어간 추출 또는 형태소 분석 이라고 하며 영어로는 stemming 이라고 합니다. Elasticsearch 에서 사용 가능한 형태소 분석기 중에서 가장 많이 알려진 형태소 분석 알고리즘인 Snowball 과 한글 형태소 분석기인 Nori에 대해 살펴보도록 하겠습니다.

Snowball

💡
~ing, ~s 등을 제거하여 문장에 쓰인 단어들을 기본형태로 변경합니다.
 

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

커뮤니티 한글 형태소 분석기

  • 아리랑 (arirang)
  • 은전한닢 (seunjeon)
  • Open Korean Text

Nori

💡
Elasic사에서 공식적으로 개발해 지원하는 한글 형태소 분석기 입니다.
  • nori_tokenizer 토크나이저
    • user_dictionary : 사용자 사전이 저장된 파일의 경로를 입력합니다.
    • user_dictionary_rules : 사용자 정의 사전을 배열로 입력합니다.
    • decompound_mode : 합성어의 저장 방식을 결정합니다. 다음 3개의 값을 사용 가능합니다.
      • none : 어근을 분리하지 않고 완성된 합성어만 저장합니다.
      • discard (디폴트) : 합성어를 분리하여 각 어근만 저장합니다.
      • mixed : 어근과 합성어를 모두 저장합니다.
  • nori_part_of_speech 토큰 필터
  • nori_readingform 토큰 필터
// nori_tokenizer 토크나이저로 "동해물과 백두산이" 문장 분석 GET _analyze { "tokenizer": "nori_tokenizer", "text": [ "동해물과 백두산이" ] }
// 토큰 결과 "동해", "물", "과", "백두", "산", "이"
 

nori_part_of_speech 와 품사 정보

💡
한글 검색에서는 명사, 동명사 정도만을 검색하고 조사, 형용사 등은 제거하는 것이 바람직합니다. nori_part_of_speech 토큰 필터를 이용해서 제거할 품사(POS - Part Of Speech) 정보의 지정이 가능하며, 옵션 stoptags 값에 배열로 제외할 품사 코드를 나열해서 입력해서 사용합니다.
notion image
 
"stoptags": [ "E", "IC", "J", "MAG", "MAJ", "MM", "SP", "SSC", "SSO", "SC", "SE", "XPN", "XSA", "XSN", "XSV", "UNA", "NA", "VSV" ]
 

nori_readingform

💡
한자로 된 단어를 한들로 바꾸어 저장을 해줍니다.
 

References::

Loading Comments...