ElasticSearch 정리 6일차 - type2

0

elasticsearch

목록 보기
6/9

Analysis type2

Normalizer

string인 textkeyword field에 대해서 알아보기 이전에 normalizer에 대해서 알아보자. normalizerkeyword에 사용하는 analyzer로 생각하면 된다.

normalizer는 analyzer와 비슷한 역할을 하나 적용 대상이 text type이 아닌 keyword type field라는 차이가 있다. 또한 analyzer와는 다르게 단일 token을 생성한다. 이는 단일 이후에 나올 keyword의 성질과도 관련이 깊다.

normalizer의 구성은 다음과 같다.
1. character filter
2. token filter

즉, analyzer에서 tokenizer가 없는 버전이다.

|---character filter---|     |---token filter---| 
|                      |  -> |                  | -> single term
|----------------------|     |------------------|

오해하지 말것은 normalizer의 character filter, token filter와 analyzer의 character filter, token filter는 다르다. 따라서 analyzer에 있는 character filter라고해서 normalizer의 character filter에 적용시킬 수 있는 것은 아니다.

엘라스틱서치가 제공하는 built-in normalizer는 lowercase밖에 없다. 다른 방법으로 keyword type의 field를 처리하려면 custom normalizer를 조합해 사용해야한다. 다음과 같이 custom normalizer를 만들 수 있다.

PUT normalizer_test
{
  "settings": {
    "analysis": {
      "normalizer": {
        "my_normalizer": {
          "type": "custom",
          "char_filter": [],
          "filter": [
            "asciifolding",
            "uppercase"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "myNormalizerKeyword": {
        "type": "keyword",
        "normalizer": "my_normalizer"
      },
      "lowercaseKeyword": {
        "type": "keyword",
        "normalizer": "lowercase"
      },
      "defaultKeyword": {
        "type": "keyword"
      }
    }
  }
}

myNormalizerKeyword field에는 custom normalizer인 my_normalizer를 적용하고 lowercaseKeyword에는 built-in normalizer인 lowercase를 적용했다. defaultKeyword에는 특별한 설정을 해주지 않았다. 참고로 keyword type에 특별한 설정을 하지 않으면 아무런 normalizer도 적용되지 않는다.

이제 데이터를 넣고 normalizer가 어떻게 동작하는 지 테스트해보자.

GET normalizer_test/_analyze
{
  "field": "myNormalizerKeyword",
  "text": "Happy World!"
}

custom normalizer에서 token을 대문자로 만들기 때문에 다음의 token이 나오게 된다.

{
  "tokens" : [
    {
      "token" : "HAPPY WORLD!",
      "start_offset" : 0,
      "end_offset" : 12,
      "type" : "word",
      "position" : 0
    }
  ]
}

다음은 lowercaseKeyword로 lowercase normalizer가 적용되었기 때문에 소문자 token이 나올 것이다.

GET normalizer_test/_analyze
{
  "field": "lowercaseKeyword",
  "text": "Happy World!"
}

{
  "tokens" : [
    {
      "token" : "happy world!",
      "start_offset" : 0,
      "end_offset" : 12,
      "type" : "word",
      "position" : 0
    }
  ]
}

마지막으로 defaultKeyword는 normalizer가 없기 때문에 그대로 나올 것이다.

GET normalizer_test/_analyze
{
  "field": "defaultKeyword",
  "text": "Happy World!"
}

{
  "tokens" : [
    {
      "token" : "Happy World!",
      "start_offset" : 0,
      "end_offset" : 12,
      "type" : "word",
      "position" : 0
    }
  ]
}

text 타입과 keyword 타입

문자열 자료형을 담는 field에는 text, keyword type 중 하나를 선택할 수 있다.

text로 지정된 field는 analyzer가 적용된 후 indexing된다. 즉, 들어온 문자열 값 그대로를 가지고 inverted index를 구성하는 것이 아니라, 값을 분석하여 여러 token으로 쪼갠다. 이렇게 쪼개진 token으로 inverted index를 구성하는 것이다. 참고로, 쪼개진 token에 지정한 filter를 적용하는 등의 후처리 작업 후의 최종적으로 reverse index에 들어가는 형태를 term이라고 한다.

반면 keyword로 지정된 field에 들어온 문자열 값은 여러 token으로 쪼개지 않고 inverted index를 구성한다. analyzer로 분석하는 대신에 normalizer를 적용한다. normalizer는 간단한 전처리만을 거친 뒤 커다란 단일 term으로 inverted index를 구성한다.

간단한 document를 indexing하고 검색해보면 text타입과 keyword타입의 차이점을 알아보도록 하자.

PUT mapping_test
{
  "mappings": {
    "properties": {
      "keywordString": {
        "type": "keyword"
      },
      "textString": {
        "type": "text"
      }
    }
  }
}

mapping_test index를 만들어보도록 하자. properties에서 keywordStringtypekeyword이고 textStringtext이다.

이제 document를 만들어보도록 하자.

PUT mapping_test/_doc/3
{
  "keywordString": "Hello, World!",
  "textString": "Hello, World!"
}

두 field에 동일한 내용을 담았다. 이제 각 field에 동일한 search query를 전달해보도록 하자.

GET mapping_test/_search
{
  "query": {
    "match": {
      "textString": "hello"
    }
  }
}

다음의 결과가 나온다.

"hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.2876821,
    "hits" : [
      {
        "_index" : "mapping_test",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.2876821,
        "_source" : {
          "keywordString" : "Hello, World!",
          "textString" : "Hello, World!"
        }
      }
    ]
  }
...

text type인 textString을 대상으로 한 query는 문제없이 검색되는 것을 알 수 있다. 반면에 keyword type인 keywordString에 동일한 검색을 하게 될 경우 실패한다.

GET mapping_test/_search
{
  "query": {
    "match": {
      "keywordString": "hello"
    }
  }
}

다음과 같이 연결된 응답이 없다고 나올 것이다.

"hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }

이는 앞서 말했듯이 text type의 field는 들어오는 문자열을 analyzer를 통해 여러 token으로 쪼개고 각 token에 후처리를 적용한 term으로 만들어 inverted index를 구성한다. 위의 경우는 Hello, World!라는 문자열을 helloworld라는 두 개의 term으로 쪼개버린다. 이 두 term으로 inverted index를 구성하기 때문에 hello라는 검색어로 document를 찾을 수 있던 것이다.

반면에 keyword type field는 analyzer를 통해서 문자열을 쪼개지 않는다. 대신 normalizer를 사용하는데 normalizer는 문자열을 여러 token으로 쪼개지 않고 전처리 작업 이후 단일 토큰만을 생성한다. 즉, Hello, World!라는 문자열이 들어올 때, 별다른 전처리가 없다면 Hello, World!라는 문자열로 inverted index를 생성한다. 따라서 hello라는 검색어로는 document를 찾을 수 없고 Hello, World!라고 정확하게 입력해야 가능하다.

fieldinverted indexdocs
keywordString'Hello, World!'3
textString'hello', 'world'3

위와 같이 inverted index가 만들어진 것이다.

재밌는 것은 match query는 검색 대상 field가 text타입인 경우 검색 질의어도 analyzer로 분석한다.

GET mapping_test/_search
{
  "query": {
    "match": {
      "textString": "THE WORLD SAID HELLO"
    }
  }
}

다음의 textStringtext field이므로 THE WORLD SAID HELLO를 analyzer로 분석하여 term으로 쪼갠다. 결과적으로 4개의 term인 the, world, said, hello로 쪼개진다.

추가적으로 analyzer를 기본 standard이외에 다른 custom, build-in analyzer로 구동할 수 있다. 이에 대해서는 추후에 알아보도록 하자.

text 타입과 keyword타입은 이외에 정렬과 집계, 스크립트 작업을 수행할 때도 동작의 차이가 있다. 정렬과 집계, 스크립트 작업의 대상이 될 field는 text보다는 keyword 타입을 쓰는 것이 더 낫다. keyword 타입은 기본적으로 doc_values라는 캐시를 사용하고 text타입은 fielddata라는 캐시를 사용하기 때문이다.

doc_values

엘라스틱서치의 search는 inverted index기반으로 이루어진다. term을 보고 inverted index에서 document를 찾아내는 방식이다. 그러나 정렬, 집계, 스크립트 작업 시에는 접근법이 다르다. docment를 보고 field 내의 term을 찾는다. doc_values는 디스크를 기반으로 한 자료 구조로 파일 시스템 캐시를 통해 효율적으로 정렬, 집계, 스크립트 작업을 수행하도록 설계되었다.

text, annotated_text type이외의 거의 모든 field타입이 dc_values를 지원한다. 또한, mapping을 통해서 doc_values를 끌 수 있는데, 이는 정렬, 집계, 스크립트 작업을 할 일이 없는 필드의 경우이다. mapping을 지정할 때 doc_values 속성의 값을 false로 지정하면 된다. doc_values를 지원하는 field의 경우 default는 true이다.

fielddata

text타입은 파일 시스템 기반의 캐시인 doc_values를 사용할 수 없다. text필드 대상으로 정렬, 집계, 스크립트 작업을 수행할 대에는 fielddata라는 캐시를 이용한다. fielddata를 사용한 정렬이나 집계 등의 작업 시에는 inverted index전체를 읽어들려 heap메모리에 올린다. 때문에 OOM등 많은 문제를 발생시킬 수 있어서 fielddata는 기본적으로 비활성화이다. 이 역시도 mapping을 지정할 때 true로 지정하면 켜진다.

단, text field의 fielddata를 활성화하는 것은 매우 신중해야한다. text field는 이미 analyzer를 통해서 나온 결과인 term이 분석된다. 따라서 집계, 정렬 등의 결과가 원하는 결과가 나오지 않을 수 있으며, 수 많은 term을 heap메모리에 올리는 것은 OOM발생 위험을 야기시킨다.

text vs keyword 정리

마지막으로 textkeyword를 정리하면 다음과 같다.

textkeyword
분석analyzer로 분석하여 여러 token으로 쪼개진 term을 inverted index에 넣는다.normalizer로 전처리한 단일 term을 inverted index에 넣는다.
검색전문 검색에 적합하다.단순 완전 일치 검색에 적합하다.
정렬, 집계 , 스크립트fielddata를 사용하므로 적합하지 않다.doc_values를 사용하므로 적합하다.
doc_valuesfielddata
적용 타입text, annotated_text를 제외한 거의 모든 타입text, annotated_text
동작 방식디스크 기반이며 파일 시스템 캐시를 활용메모리에 inverted index내용 전체를 올린다. OOM 주의
defaulttruefalse

이제 기본적인 type에 대한 이야기는 완료되었다. type이외에 mapping field에 존재하는 주요 설정들에 대해서 더 알아보도록 하자.

_source

_source field는 document indexing시점에 엘라스틱서치에 전달된 원본 JSON문서를 저장하는 metadata field이다. 문서 조회 API나 검색 API가 클라이언트에게 반환할 문서를 확정하고 나면 이 _source에 저장된 값을 읽어 클라이언트에게 반환한다. _source field 자체는 역색인을 생성하지 않기 때문에 검색 대상이 되지 않는다.

_source는 JSON문서를 통째로 담기 때문에 저장공간을 많이 필요로 한다. _source에 data를 저장하지 않도록 mappings_source부분을 enabled false처리할 수 있다.

그러나 이렇게 비활성화하면 많은 문제가 생기게 된다. 가령 update가 안되며 reindex도 불가능하다. 두 작업 모두 기존의 원본 json을 기반으로 새롭게 만들기 때문이다.

만약, 디스크 공간을 절약해야만 하는 상황이라면 _source의 비활성화보다는 인덱스 데이터의 압축률을 높이는 편이 낫다.

PUT codec_test
{
  "settings": {
    "index": {
      "codec": "best_compression"
    }
  }
}

settings.index.codecdefault, best_compression이 있다. default는 LZ4압축을 진행하고 best_compressionDEFLATE압축을 사용한다. 이 설정은 동적 변경이 불가능하기 때문에 처음부터 설정해야한다.

index

index 속성은 해당 field의 inverted index를 만들 것인지 지정한다. 기본적으로 true이고 false 설정 시에 해당 field는 inverted index가 없기 때문에 일반적인 검색 대상이 되지 않는다. 검색 대상이 되지 않아도 다른 field를 대상으로 한 검색 query에 document가 검색된다면, index값이 false로 지정된 field의 내용도 검색 결과에 포함된다. 왜냐하면 document의 내용 자체는 inverted index 생성 여부와 상관없이 _source라는 metafield에 저장되기 때문이다.

또한, doc_values를 사용하는 type이라면 정렬이나 집계의 대상이 될 수 있다. 즉 정리하면 indexfalse로 두어서 inverted index가 안만들어지는 것이고 검색의 대상이 되지 않지만, 집계나 정렬, 결과 포함의 대상에서 벗어나는 것은 아니다.

indexfalse로 두어서 inverted index를 만들지 않은 대신 디스크 공간을 절약할 수 있는 것이다. 따라서 index값을 false로 지정해 성능상의 이득을 볼 수 있다.

다음의 예시를 보도록 하자.

PUT mapping_test/_mapping
{
  "properties": {
    "notSearchableText": {
      "type": "text",
      "index": false
    },
    "docValuesSearchableText": {
      "type": "keyword",
      "index": false
    }
  }
}

textkeyword type field에 indexfalse로 주었다. 다음으로 document를 추가해주도록 하자.

PUT mapping_test/_doc/4
{
  "textString": "Hello, World!",
  "notSearchableText": "World, Hello!",
  "docValuesSearchableText": "hello"
}

다음으로 text type인 notSearchableText를 search를 해보도록 하자.

GET mapping_test/_search
{
  "query": {
    "match": {
      "notSearchableText": "hello"
    }
  }
}

indexfalse로 했기 때문에 검색이 불가능하다.

이번에는 notSearchableText에서 textString으로 변경해서 요청하도록 하자

GET mapping_test/_search
{
  "query": {
    "match": {
      "textString": "hello"
    }
  }
}

...
"hits" : [
      {
        "_index" : "mapping_test",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.2876821,
        "_source" : {
          "textString" : "Hello, World!",
          "notSearchableText" : "World, Hello!",
          "docValuesSearchableText" : "hello"
        }
      }
    ]

성공하는 것을 볼 수 있다. 또한, notSearchableText 또한 indexfalse임에도 결과에 같이 나오는 것을 볼 수 있다. docValuesSearchableText역시도 마찬가지이다.

이번에는 docValuesSearchableText로 search query를 전달해보도록 하자. 버전에 따라 다를 수 있지만 elasticsearch8 이상은 성공할 것이다. 이는 index가 false이지만 doc_value를 사용하기 때문에 inverted index가 없더라도 doc_values검색을 수행한 것이다.

0개의 댓글