string인 text
와 keyword
field에 대해서 알아보기 이전에 normalizer
에 대해서 알아보자. normalizer
는 keyword
에 사용하는 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
}
]
}
문자열 자료형을 담는 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
에서 keywordString
의 type
은 keyword
이고 textString
은 text
이다.
이제 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!
라는 문자열을 hello
와 world
라는 두 개의 term으로 쪼개버린다. 이 두 term으로 inverted index를 구성하기 때문에 hello
라는 검색어로 document를 찾을 수 있던 것이다.
반면에 keyword
type field는 analyzer를 통해서 문자열을 쪼개지 않는다. 대신 normalizer를 사용하는데 normalizer는 문자열을 여러 token으로 쪼개지 않고 전처리 작업 이후 단일 토큰만을 생성한다. 즉, Hello, World!
라는 문자열이 들어올 때, 별다른 전처리가 없다면 Hello, World!
라는 문자열로 inverted index를 생성한다. 따라서 hello
라는 검색어로는 document를 찾을 수 없고 Hello, World!
라고 정확하게 입력해야 가능하다.
field | inverted index | docs |
---|---|---|
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"
}
}
}
다음의 textString
은 text
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
라는 캐시를 사용하기 때문이다.
엘라스틱서치의 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
이다.
text
타입은 파일 시스템 기반의 캐시인 doc_values
를 사용할 수 없다. text
필드 대상으로 정렬, 집계, 스크립트 작업을 수행할 대에는 fielddata
라는 캐시를 이용한다. fielddata
를 사용한 정렬이나 집계 등의 작업 시에는 inverted index전체를 읽어들려 heap메모리에 올린다. 때문에 OOM등 많은 문제를 발생시킬 수 있어서 fielddata
는 기본적으로 비활성화이다. 이 역시도 mapping을 지정할 때 true
로 지정하면 켜진다.
단, text
field의 fielddata
를 활성화하는 것은 매우 신중해야한다. text
field는 이미 analyzer를 통해서 나온 결과인 term
이 분석된다. 따라서 집계, 정렬 등의 결과가 원하는 결과가 나오지 않을 수 있으며, 수 많은 term을 heap메모리에 올리는 것은 OOM발생 위험을 야기시킨다.
마지막으로 text
와 keyword
를 정리하면 다음과 같다.
text | keyword | |
---|---|---|
분석 | analyzer로 분석하여 여러 token으로 쪼개진 term을 inverted index에 넣는다. | normalizer로 전처리한 단일 term을 inverted index에 넣는다. |
검색 | 전문 검색에 적합하다. | 단순 완전 일치 검색에 적합하다. |
정렬, 집계 , 스크립트 | fielddata 를 사용하므로 적합하지 않다. | doc_values 를 사용하므로 적합하다. |
doc_values | fielddata | |
---|---|---|
적용 타입 | text, annotated_text를 제외한 거의 모든 타입 | text, annotated_text |
동작 방식 | 디스크 기반이며 파일 시스템 캐시를 활용 | 메모리에 inverted index내용 전체를 올린다. OOM 주의 |
default | true | false |
이제 기본적인 type
에 대한 이야기는 완료되었다. type
이외에 mapping
field에 존재하는 주요 설정들에 대해서 더 알아보도록 하자.
_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.codec
은 default
, best_compression
이 있다. default는 LZ4
압축을 진행하고 best_compression
은 DEFLATE
압축을 사용한다. 이 설정은 동적 변경이 불가능하기 때문에 처음부터 설정해야한다.
index
속성은 해당 field의 inverted index를 만들 것인지 지정한다. 기본적으로 true
이고 false
설정 시에 해당 field는 inverted index가 없기 때문에 일반적인 검색 대상이 되지 않는다. 검색 대상이 되지 않아도 다른 field를 대상으로 한 검색 query에 document가 검색된다면, index
값이 false
로 지정된 field의 내용도 검색 결과에 포함된다. 왜냐하면 document의 내용 자체는 inverted index 생성 여부와 상관없이 _source
라는 metafield에 저장되기 때문이다.
또한, doc_values
를 사용하는 type이라면 정렬이나 집계의 대상이 될 수 있다. 즉 정리하면 index
를 false
로 두어서 inverted index가 안만들어지는 것이고 검색의 대상이 되지 않지만, 집계나 정렬, 결과 포함의 대상에서 벗어나는 것은 아니다.
index
를 false
로 두어서 inverted index를 만들지 않은 대신 디스크 공간을 절약할 수 있는 것이다. 따라서 index
값을 false
로 지정해 성능상의 이득을 볼 수 있다.
다음의 예시를 보도록 하자.
PUT mapping_test/_mapping
{
"properties": {
"notSearchableText": {
"type": "text",
"index": false
},
"docValuesSearchableText": {
"type": "keyword",
"index": false
}
}
}
text
와 keyword
type field에 index
를 false
로 주었다. 다음으로 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"
}
}
}
index
를 false
로 했기 때문에 검색이 불가능하다.
이번에는 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
또한 index
가 false
임에도 결과에 같이 나오는 것을 볼 수 있다. docValuesSearchableText
역시도 마찬가지이다.
이번에는 docValuesSearchableText
로 search query를 전달해보도록 하자. 버전에 따라 다를 수 있지만 elasticsearch8 이상은 성공할 것이다. 이는 index
가 false이지만 doc_value
를 사용하기 때문에 inverted index가 없더라도 doc_values
검색을 수행한 것이다.