Elastic Search (3) 데이터 색인과 텍스트 분석

rin·2020년 7월 17일
9

Elastic Search

목록 보기
3/4
post-thumbnail

ref. https://esbook.kimjmin.net/
위 링크의 내용을 요약, 정리합니다.

데이터 색인과 텍스트 분석

ES가 검색을 위해 텍스트 데이터를 어떻게 처리하고 어떤 과정으로 인덱싱되는지 살펴본다.

역 인덱스

RDBMS :

ES :

ES에서는 위와같이 Inverted Index(역 인덱스) 구조를 만들어 저장한다. 이 때 추출된 각 키워드를 term이라고 부른다.

데이터가 늘어나도 찾아갈 행이 늘어나는 것이 아니라 역 인덱스가 가리키는 id의 배열값이 증가하는 것이므로 큰 속도 저하가 일어나지 않는다. 이런 역 인덱스를 데이터가 저장되는 과정에서 만드므로 데이터를 입력할 때 저장이 아닌 색인한다고 표현한다.

텍스트 분석

문자열 저장시 여러 단계의 처리 과정을 거친다.

  • 이러한 전체 과정을 Text Analysis(텍스트 분석)이라고 하고
  • 이 과정을 "처리하는 기능"을 Analyzer한다.
    • 0~3개의 Character Filter : 필요에 따라 전체 문장에서 특정 문자를 대치하거나 제거
    • 1개의 Tokenizer : 문장에 속한 단어들을 텀 단위로 하나씩 분리, 반드시 1개만 적용이 가능
    • 0~n개의 TokenFilter : 분리된 텀을 하나씩 가공 → 동일한 텀이된 토큰은 merge

TokenFilter

  • lowercase : 대문자를 모두 소문자로 변경
  • stop : (stopword, 불용어) 검색어로서의 가치가 없는 단어는 토큰에서 제외
  • snowball : 형태소 분석, 문법상 변형된 단어를 기본 형태로 변환
  • synonym : 동의어 추가

Analyzer

_analyze API

분석된 문장을 _analyze API를 이용해 확인할 수 있다. 이 때 body에 토크나이저는 tokenizer(하나만 적용 → key:value 형식), 토큰 필터를 filter(여러개 적용 → key:array 형식)으로 입력하면 된다.

GET _analyze
{
  "text": "The quick brown fox jumps over the lazy dog",
  "tokenizer": "whitespace",
  "filter": [
    "lowercase",
    "stop",
    "snowball"
  ]
}

//response
{
  "tokens" : [
    {
      "token" : "quick",
      "start_offset" : 4,
      "end_offset" : 9,
      "type" : "word",
      "position" : 1
    },
    {
      "token" : "brown",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "word",
      "position" : 2
    },
    {
      "token" : "fox",
      "start_offset" : 16,
      "end_offset" : 19,
      "type" : "word",
      "position" : 3
    },
    {
      "token" : "jump",
      "start_offset" : 20,
      "end_offset" : 25,
      "type" : "word",
      "position" : 4
    },
    {
      "token" : "over",
      "start_offset" : 26,
      "end_offset" : 30,
      "type" : "word",
      "position" : 5
    },
    {
      "token" : "lazi",
      "start_offset" : 35,
      "end_offset" : 39,
      "type" : "word",
      "position" : 7
    },
    {
      "token" : "dog",
      "start_offset" : 40,
      "end_offset" : 43,
      "type" : "word",
      "position" : 8
    }
  ]
}

여러 토큰 필터 입력 시, 순서대로 필터링에 사용되기 때문에 이를 고려하여 작성하도록 한다.

analyzer 항목을 이용해 tokenizer와 filter를 한번에 표현할 수 있다.
앞서 사용한

"tokenizer": "whitespace",
"filter": [
    "lowercase",
    "stop",
    "snowball"
  ]

"analyzer": "snowball"로 표현할 수 있으므로(ES에 사전정의 되어있다.) 다음처럼 요청을 보내도 결과가 동일하다.

GET _analyze
{
  "text": "The quick brown fox jumps over the lazy dog",
  "analyzer": "snowball"
}

아래는 인덱스를 생성할 때 "analyzer": "snowball"를 추가하고 검색 쿼리를 날린 결과이다.

검색 결과로 출력된 도큐먼트에는 "jumping"이라는 단어가 없지만 snowball 애널라이저에 의해 "jumps"가 "jump"라는 텀으로 변환되어 저장되었을 것이다. 검색어 "jumping"또한 동일한 애널라이저를 거쳐 "jump"로 검색할 것이기 때문에 위와 같은 결과가 나타났다.

Term 쿼리

term 쿼리는 match 쿼리와 문법은 유사하나 입력한 검색어에 애널라이저가 적용되지 않는다.

동일하게 message : jumping으로 검색했으나 match를 사용했을 때와 달리 hit한 도큐먼트가 없다.

애널라이저, 캐릭터 필터, 토크나이저, 토큰필터 목록은 공식 도큐먼트에서 확인이 가능하다.

Custom Analyzer

_analyze API로 애널라이저, 토크나이저, 토큰필터들의 테스트가 가능하나 실제로 인덱스에 저장되는 데이터에 대한 설정은 애널라이저만 적용할 수 있다. 📌

ES에서 사전에 만들어져있는 애널라이저들은 홈페이지의 공식 도큐먼트를 참고하길 바란다.

사용자 정의 애널라이저는 인덱스 settings"index": { "analysis": 부분에 정의한다. 생성한 다음에는 해당 인덱스의 GET 또는 POST <인덱스>/_analyze 명령으로 사용 가능하다.

my_index3에 my_custom_analyzer라는 이름의 커스텀 analyzer를 추가한다.

PUT my_index3
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "my_custom_analyzer": {
            "type": "custom",
            "tokenizer": "whitespace",
            "filter": [
              "lowercase",
              "stop",
              "snowball"
            ]
          }
        }
      }
    }
  }
}

_analyze API에서 my_custom_analyzer analyzer를 이용해 문장의 역인덱싱 테스트를 해보자.

GET my_index3/_analyze
{
  "analyzer": "my_custom_analyzer",
  "text": [
    "The quick brown fox jumps over the lazy dog"
  ]
}

🔎 사용자 정의 토큰필터
토크나이저, 토큰필터의 경우에도 옵션을 지정하는 경우에도 이를 커스텀 토크나이저, 토큰필터로 만들어 추가해야 한다.

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"
            ]
          }
        }
      }
    }
  }
}

filter 부분에서 커스텀 필터를 정의하고 있다. "my_stop_filter"이라는 이름의 새로운 필터를 정의하고 있으며, 기존 토큰 필터인 "stop"의 형태를 띄며 "stopwords"로 "brown"을 추가한다.

"my_custom_analyzer"의 filter에 stop 대신 my_stop_filter를 넣어준 결과를 보도록 하자.

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

brown이 포함된 메세지를 가진 도큐먼트를 추가하였으나 커스컴 애널라이저가 적용되어 term에서 생략되기 때문에 검색되지 않는다.

_termvectors API

인덱싱된 도큐먼트의 역 인덱스의 내용을 확인할 때 사용한다.
GET <인덱스>/_termvectors/<도큐먼트id>?fields=<필드명>

이전에 커스텀을 등록한 인덱스이므로 아래 저장된 terms에 "brown"이 없음을 확인할 수 있다.

{
  "_index" : "my_index3",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "took" : 11,
  "term_vectors" : {
    "message" : {
      "field_statistics" : {
        "sum_doc_freq" : 7,
        "doc_count" : 1,
        "sum_ttf" : 8
      },
      "terms" : {
        "dog" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 8,
              "start_offset" : 40,
              "end_offset" : 43
            }
          ]
        },
        "fox" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 3,
              "start_offset" : 16,
              "end_offset" : 19
            }
          ]
        },
        "jump" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 4,
              "start_offset" : 20,
              "end_offset" : 25
            }
          ]
        },
        "lazi" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 7,
              "start_offset" : 35,
              "end_offset" : 39
            }
          ]
        },
        "over" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 5,
              "start_offset" : 26,
              "end_offset" : 30
            }
          ]
        },
        "quick" : {
          "term_freq" : 1,
          "tokens" : [
            {
              "position" : 1,
              "start_offset" : 4,
              "end_offset" : 9
            }
          ]
        },
        "the" : {
          "term_freq" : 2,
          "tokens" : [
            {
              "position" : 0,
              "start_offset" : 0,
              "end_offset" : 3
            },
            {
              "position" : 6,
              "start_offset" : 31,
              "end_offset" : 34
            }
          ]
        }
      }
    }
  }
}

Character Filter

  • 텍스트 분석 중 가장 먼저 처리되는 과정
  • 색인된 텍스트가 토크나이저에 의해 term으로 분리되기 전에 전체 문장에 대해 적용되는 전처리 도구
  • 종류 : HTML strip, Mapping, Pattern Replace
  • char_filter 항목에 배열로 입력하며 순차적으로 적용된다.
  • 0~3개 사용 가능

HTML Strip

  • 입력된 텍스트가 HTML인 경우 HTML 태그들을 제거하여 일반 텍스트로 만든다.
  • <>로 된 태그를 제거
  • &nbsp; 등의 HTML 문법 용어들도 해석
  • 입력 값은 html_strip

❗️ NOTE
애널라이저는 항상 최소 1개의 토크나이저를 필요로 하기 때문에 캐릭터 필터만 적용하면 오류가 발생하니 주의하자.

Mapping

Mapping 캐릭터 필터를 이용하면 지정한 단어를 다른 단어로 치환 가능하다.

아래 요청 결과를 보면 "C"가 포함되어 있는데, 도큐먼트 색인 시 standard 애널라이저가 적용되며 "C++"의 특수문자 +는 제거되고, 소문자로 바뀌어 저장되기 때문이다.

standard 뿐 아니라 대다수의 애널라이저들이 특수문자에 대해서는 불용어로 간주하고 제거한다.

따라서, 검색될 가능성이 있는 특수문자는 다른 문자로 치환해서 저장해줘야한다. 🤔

캐릭터 필터에 커스텀 필터를 정의해주고 이를 커스텀 애널라이저에 넣어준다.
_termvectors API를 사용하여 "C++"가 어떤 term으로 저장되었는지 보자!

Pattern Replace

정규식을 이용해 좀 더 복잡한 패턴들을 치환 할 수 있는 캐릭터 필터이다.

필터 타입은 pattern_replace이며 "pattern" 필드에 정규식을 넣어주면 된다. 위에서 정의한 "camel_filter"는 대문자가 시작하는 단위마다 공백을 삽입("replacement")하여 세부 단어별로 토크나이징 될 수 있도록 한다.

Tokenizer

🙋🏻 데이터 색인 과정에서 검색 기능에 가장 큰 영향을 미치는 단계가 토크나이저이다.

  • 반드시 한 개만 사용가능
  • tokenizer 항목에 단일값으로 설정

Standard, Letter, Whitespace

분석할 문장 : "THE quick.brown_FOx jumped! @ 3.5 meters."

standardletterwhitespace
공백으로 구분알파벳을 제외한 모든 공백, 숫자, 기호들을 기준으로 구분스페이스, 탭, 줄바꿈같은 공백(whitespace)만을 기준으로 텀을 분리
"@"와 같은 "일부" 특수문자제거
단어끝의 특수문자는 제거단어끝의 특수문자 제거단어끝의 특수문자도 남긴다.
단어 중간의 마침표나 밑줄은 제거or분리 안됨
📌 보통 많이 사용됨📌 검색 범위 넓어져 원하지 않는 결과 많이 나올 수 있음📌 특수문자를 거르지 않으므로 정확하게 검색해야함

UAX URL Email

  • 이메일 주소나 웹 URL 경로의 @, / 같은 특수문자를 제거하고 분리하면 문제가 될 수 있다.
  • 이를 방지하기 위해 사용 가능한 것이 UAX URL Email 토크나이저
  • 이메일 주소와 웹 URL 경로는 분리하지 않고 하나의 텀으로 저장

standard tokenizer :

uax_url_email tokenizer :

Pattern

  • 사용자가 지정한 특수한 문자를 구분자로 사용하여 텀을 분리하고자 할 때
  • 분리할 패턴을 기호 또는 Java 정규식 형태로 지정
  • 구분자 지정은 pattern 항목에 인덱스 생성시 설정
PUT pat_tokenizer
{
  "settings": {
    "analysis": {
      "tokenizer": {
        "my_pat_tokenizer": {
          "type": "pattern",
          "pattern": "/"
        }
      }
    }
  }
}

Path Hierarchy

  • 경로 데이터를 계층별로 저장해 하위 디렉토리에 속한 도큐먼트들을 수준 별로 검색하거나 집계하는 것이 가능하다.

분석할 문장 : "/usr/share/elasticsearch/bin"

"pattern":"/"로 정의된 커스텀 tokenizer를 이용한 경우path_hierarchy를 이용한 경우
  • delimiter : 경로 구분자를 지정 (default가 /이다.)
  • replacement : 소스의 구분자를 다른 구분자로 대치해서 저장

Token Filter

  • 토크나이저에 의해 분리된 텀들을 지정한 규칙에 따라 처리한다.
  • filter (token_filter가 아님) 항목에 배열값으로 나열해서 지정
  • 하나만 사용하더라도 배열 값으로 입력
  • 나열된 순서대로 처리되므로 순서를 잘 고려해야한다.

Lowercase, Uppercase

  • Lowercase 토큰 필터 : 텀들을 모두 소문자로 변경하여 저장
  • Uppercase 토큰 필터 : 텀들을 모두 대문자로 변경하여 저장

Stop

  • 불용어(stopword) : 큰 의미가 없는 조사나 전치사로써 검색어로 쓰이지 않는 단어
  • 불용어에 해당되는 텀들을 제거한다.
  • stopwords 항목에 불용어로 지정할 단어들을 배열 형태로 나열하거나 "_english_", "_german_" 같이 특정 언어를 지정할 수도 있다.

Synonym

  • 텀의 동의어 저장이 가능하다.
  • synonyms 항목에서 직접 동의어 목록을 입력하는 방법
  • 동의어 사전 파일을 만들어 synonyms_path로 지정하는 방법
  • 동의어 사전 명시 규칙
    • "A, B => C" : A,B 대신 C "텀"을 저장. A, B로 C의 검색(match)은 가능하나 반대로는 불가
    • "A, B" : A,B 각 텀이 A와 B 두개의 텀을 모두 저장. A, B 모두 서로의 검색어로 검색 가능

다음의 토큰 필터를 포함하는 인덱스가 있다고 했을 때,

"filter": {
        "syn_aws": {
          "type": "synonym",
          "synonyms": [
            "amazon => aws"
          ]
        }
      }

다음과 같이 두 개의 도큐먼트가 저장된 경우를 생각해보자.

PUT my_synonym/_doc/1
{ "message" : "Amazon Web Service" }

PUT my_synonym/_doc/2
{ "message" : "AWS" }

term에는 "amazon"이 존재하지 않는다. "_id":"1"인 도큐먼트에 대해서도 "amazon"대신 "aws" term이 저장된다. ( =>를 사용했기 때문에... )
→ 따라서 1️⃣ term 쿼리로 aws를 검색하면 두 개의 도큐먼트가 모두 검색되고, amazon을 검색하면 검색이 되지 않는다.

match 쿼리에는 검색어 amazon에도 애널라이저가 적용되어 실제로는 aws로 변환하여 검색을 한다. 따라서 amazon으로 검색해도 두 데이터가 모두 선택된다.

=>가 아니라 ,를 사용하는 필터를 쓰는 경우에는

      "filter": {
        "syn_aws": {
          "type": "synonym",
          "synonyms": [
            "amazon, aws"
          ]
        }
      }

"amazon"이나 "aws"를 포함하고 있다면 "amazon", "aws" 두 개의 텀들이 모두 저장될 것이다.

synonym 토큰 필터에는 추가적으로 다음과 같은 옵션들이 있다.

  • expand (true(default)/false)
    "expand": false"synonyms": "A, B, C, ..."인 경우에 모든 텀을 저장하지 않고 맨 처음에 명시된 텀만 저장한다.
    즉, "synonyms": "A, B, C, ... => A"와 동일하게 작동한다.
  • lenient (true/false(default))
    "lenient": true → synonym 설정에 오류가 있어도 무시하고 실행

NGram, Edge NGram, Shingle

NGram

  • 텀이 아닌 단어의 일부만 가지고도 검색해야 하는 기능이 필요한 경우
  • RDBMS의 LIKE 검색처럼 사용하는 wildcard 쿼리나 regexp(정규식) 쿼리도 지원하나 메모리 소모가 많고 느림 🤔
  • 검색 텀의 일부만 미리 분리해서 저장가능 → 단어의 일부를 나눈 부위 = NGram(=unigram(1글자), bigram(2글자))
  • "type": "nGram" 으로 설정

"house"라는 단어의 2글자를 NGram(bigram)으로 처리하면 "ho", "ou", "us", "se" 총 4개의 토큰이 추출되고 모두 저장된다. → "ho" 라고만 검색해도 house를 포함한 도큐먼트는 물론, "ho"를 포함한 도큐먼트도 모두 찾아진다.

❗️ NOTE
텀의 개수 기하급수적 증가 및 일부만 포함하고 있어도 모두 검색되므로 일반적인 텍스트 검색에 사용하지 않기를 추천.
카테고리 목록이나 태그 목록같이 전체 개수가 많지 않은 데이터 집단에 자동완성 같은 기능을 구현하는데 적합하다.

✔️ 옵션
min_gram (default 1) 최소 문자수의 토큰을 구분
max_gram (defalut 2) 최대 문자수의 토큰을 구분

house를 "min_gram": 2, "max_gram": 3 으로 설정 :

Edge NGram

  • 앞 쪽의 ngram만 저장
  • min_gram이 앞 쪽의 n개 중 최소개수
  • max_gram이 앞 쪽의 n개 중 최대대수
  • "type": "edgeNGram"

"min_gram": 2, "max_gram": 3으로 설정하고 "house" 분석 시 "ho", "hou" 2개의 토큰이 생성된다.

PUT my_ngram
{
  "settings": {
    "analysis": {
      "filter": {
        "my_ngram_f": {
          "type": "edgeNGram",
          "min_gram": 2,
          "max_gram": 3
        }
      }
    }
  }
}

Shingle

  • NGramEdge NGram : 하나의 단어로부터 토큰을 확장하는 토큰 필터
  • Shingle : 문자가 아닌 단어 단위로 구성된 묶음
  • "type": "shingle"
  • 단어 묶음이 아니라 각 (하나의) 단어 또한 텀으로 저장된다.
    "hello world" → "hello", "world", "hello world"

✔️ 옵션

  • min_shingle_size / max_shingle_size : shingle의 최소 / 최대 단어 개수를 지정. (default 2)
  • output_unigrams : Shingle 외에도 각각의 개별 토큰(unigram)도 저장 하는지의 여부를 설정. (default true)
  • output_unigrams_if_no_shingles : shingle 을 만들 수 없는 경우에만 개별 토큰을 저장하는지의 여부를 설정. (default false)
  • token_separator : 토큰 구분자를 지정. 디폴트는 " " (스페이스).
  • filler_token : shing을 만들 텀이 없는 경우 (📌 보통은 stop 토큰 필터와 함께 사용되어 offset 위치만 있고 텀이 없는 경우입니다) 대체할 텍스트를 지정. 디폴트는 _ 이다.

❗️ NOTE
위 세가지 모두 일반적인 텍스트 분석에 사용하기는 적합하지 않다.
자동 완성 기능을 구현하거나 프로그램 코드 내에서 문법이나 기능명을 검색하는 것과 같이 특수한 경우에 유용하다.

Unique

  • "white fox, white rabbit, white bear" 분석 → "white" 텀은 총 "3번" 저장
  • 역색인에는 텀이 1개만 있어도 이를 포함하는 도큐먼트를 가져올 수 있음 → 중복되는 텀은 삭제해도 됨

인덱스 생성 시 filter에 배열로 unique를 추가해주면 자동으로 중복되는 컴은 하나만 저장한다.

GET _analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    "unique"
  ],
  "text": [
    "white fox, white rabbit, white bear"
  ]
}

❗️ NOTE
match 쿼리를 사용해서 검색하는 경우, unique 토큰 필터가 적용되면 텀의 개수가 여러개인 경우에도 1개만 집계된다. → TF 값이 줄어들어 스코어 점수가 달라질 수 있음. 따라서 relevancy를 따져야하는 검색의 경우에는 unique 토큰 필터는 사용하지 않는 것이 바람직하다.

형태소 분석

문법에 따른 단어 변형에 상관 없이 검색이 가능해야한다. → 텍스트 데이터 분석시 각 텀에 있는 단어의 기본 형태(어간)을 추출해야한다. 이 과정이 어간 추출(or 형태소 분석)이며 영어로 stemming이라고 한다.

ES는 다양한 형태소 분석기를 지원하며, 공식적이지 않더라도 플러그인 형태로 사용가능한 오픈소스들이 많이 있다.

Snowball

  • ~ing, ~s 등을 제거하여 문장에 쓰인 단어들을 기본 형태로 변경한다.
  • 애널라이저, 토크나이저, 토큰 필터가 모두 정의되어 있다.

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

설치
elasticsearch 홈 디렉토리에서 다음 명령을 실행한다.
$> bin/elasticsearch-plugin install analysis-nori

제거
$> bin/elasticsearch-plugin remove analysis-nori

🔎 nori_tokenizer 토크나이저

standard :

nori_tokenizer :

standard 토크나이저가 공백 외에 아무런 분리를 하지 못한 것과 달리 nori_tokenizer 토크나이저는 명사를 모두 분리한 것을 볼 수 있다.

✔️옵션

  • user_dictionary : 사용자 사전이 저장된 파일의 경로를 입력한다.
  • user_dictionary_rules : 사용자 정의 사전을 배열로 입력한다.
  • decompound_mode : 합성어의 저장 방식을 결정한다. 다음 3개의 값을 사용 할 수 있다.
    • none : 어근을 분리하지 않고 완성된 합성어만 저장
    • discard (디폴트) : 합성어를 분리하여 각 어근만 저장
    • mixed : 어근과 합성어를 모두 저장

사용자 정의 사전에 우선순위가 높은 단어가 있는 경우 해당 단어가 term으로 분리되고 이것이 기준이 되서 나머지 term들을 생성한다.

예를 들어 "user_dictionary_rules": ["해물"] 이라는 옵션이 추가된 경우 "동해물과"는 "동", "해물", "과"로 나눠져 저장된다.

🔎 nori_part_of_speech 토큰 필터

🔎 nori_readingform 토큰 필터

  • 한자로 된 단어를 한글로 바꾸어 저장한다.
  • 별도 옵션없이 토큰 필터로 명시하면 바로 적용 가능.

❗️ NOTE
query 또는 _analuze API에서 "explain": true옵션을 추가하면 분석된 한글 형태소들의 품사 정보를 같이 볼 수 있다.

profile
🌱 😈💻 🌱

0개의 댓글