wildcard 필드 타입
wildcard 필드 타입
wildcard 필드는 임의의 부분 문자열(substring)과 정규 표현식 매칭을 위해 설계된 keyword 필드의 변형이에요. 구조화되지 않은 로그 줄이나 컴퓨터 코드처럼 선행 와일드카드나 임의의 부분 문자열로 검색해야 하는 값에 사용하면 좋습니다.
출처: 문서
본문
2.15 버전에서 도입되었어요.
wildcard 필드는 임의의 부분 문자열(substring)과 정규 표현식 매칭을 위해 설계된 keyword 필드의 변형이에요. 선행 와일드카드나 임의의 부분 문자열로 검색해야 하는 값(예: 구조화되지 않은 로그 줄, 컴퓨터 코드)에 wildcard 필드를 사용하세요. keyword 필드에서는 이런 검색이 필드의 모든 용어를 스캔해야 하므로 고유 값의 수가 늘어날수록 느려져요. text 필드에서는 토큰화가 각 값을 단어로 나누므로, 단어 경계를 가로지르는 부분 문자열은 더 이상 매칭할 수 없어요. wildcard 필드는 부분 문자열 자체를 인덱싱하므로 이런 검색을 직접 지원해요.
wildcard 필드 타입은 keyword 필드 타입과 다르게 인덱싱돼요. keyword 필드가 원래 필드 값을 인덱스에 쓰는 반면, wildcard 필드 타입은 필드 값을 길이 3 이하인 부분 문자열들로 나누어 그 부분 문자열들을 인덱스에 써요. 예를 들어 test 문자열은 t, te, tes, e, es, est 문자열로 나뉘어요.
검색 시에는 쿼리 패턴에서 필요한 부분 문자열들을 인덱스와 매칭해 후보 문서를 만들고, 그 후 쿼리의 패턴에 따라 후보들을 필터링해요. 예를 들어 검색어 test에 대해 OpenSearch는 tes AND est에 대한 인덱스 검색을 수행해요. 검색어에 3자 미만의 문자가 포함되어 있으면 OpenSearch는 1자 또는 2자 길이의 문자 부분 문자열을 사용해요. 각 매칭 문서에 대해 소스 값이 test라면 그 문서가 결과에 반환돼요. 이로써 nikola tesla felt alternating current was best 같은 오탐(false positive) 값은 제외돼요.
일반적으로 정확히 일치하는 쿼리(term이나 terms 쿼리 등)는 wildcard 필드에서 keyword 필드보다 덜 효율적으로 동작하지만, wildcard, prefix, regexp 쿼리는 wildcard 필드에서 더 잘 동작해요.
wildcard 필드는 하이라이팅을 지원하지 않아요.
예시 (Example)
wildcard 필드가 있는 매핑을 생성하세요.
PUT logs
{
"mappings" : {
"properties" : {
"log_line" : {
"type" : "wildcard"
}
}
}
}
wildcard 필드의 모든 쿼리는 상수 점수(constant score), 보통 1을 가져요. 점수를 바꾸려면 쿼리에서 boost 파라미터를 설정하세요. 필드 매핑에서의 boost는 효과가 없어요.
파라미터 (Parameters)
다음 표는 wildcard 필드에서 사용할 수 있는 모든 파라미터를 나열해요.
| 파라미터 | 설명 | 기본값 | 동적 업데이트 가능 |
|---|---|---|---|
copy_to |
인덱스 시점에 이 필드의 값을 복사할 대상이 되는 다른 하나 이상의 필드 이름 | None | 예 |
doc_values |
집계, 정렬, 스크립팅에 사용할 수 있도록 필드를 디스크에 저장할지 여부를 지정하는 Boolean 값 | true | 아니요 |
fields |
동일한 값을 다른 필드 타입으로 인덱싱하는 하나 이상의 하위 필드. wildcard 타입이 지원하지 않는 연산(예: keyword 하위 필드에 대한 range 쿼리)을 지원하기 위해 하위 필드를 사용하세요. |
None | 예 |
ignore_above |
최대 문자열 길이를 지정하는 정수 값. 더 긴 문자열은 인덱싱되거나 doc values에 기록되지 않으므로 어떤 쿼리와도 매칭되지 않고 집계에도 나타나지 않아요. 값은 _source에 남아 있어요. |
2147483647 | 예 |
meta |
필드에 대한 메타데이터. OpenSearch는 이 메타데이터를 저장하고 매핑에서 반환하지만 사용하지는 않아요. | None | 예 |
normalizer |
인덱싱과 검색을 위해 값을 전처리하는 데 사용하는 정규화기. OpenSearch는 인덱싱된 값과 쿼리 양쪽에 이를 적용하며, doc values는 정규화된 형태를 저장하고 _source는 원본을 유지해요. 필드에서 대소문자 구분 없는 매칭을 수행하려면 lowercase 정규화기를 사용하세요. |
default(정규화 없음) | 아니요 |
null_value |
null 대신 인덱싱할 값. 문자열로 지정하세요. 다른 JSON 타입은 문자열 형태로 변환돼요. 이 파라미터를 지정하지 않으면 null 값은 누락으로 처리되어 exists 쿼리가 그 문서와 매칭되지 않아요. | null | 아니요 |
저장 공간 요구 사항 (Storage requirements)
wildcard 필드는 같은 값을 담은 keyword 필드보다 더 많은 디스크 공간을 사용해요. 두 필드 타입 모두 동일한 doc values를 쓰므로 추가 공간은 전적으로 인덱스에서 발생해요. keyword 필드는 값당 하나의 용어를 인덱싱하는 반면, wildcard 필드는 값의 각 문자 위치마다 하나의 부분 문자열을 인덱싱해요. 따라서 인덱싱되는 부분 문자열의 수는 고유 값의 수가 아니라 각 값의 길이에 따라 늘어나요.
인덱싱되는 부분 문자열의 수는 두 필드 타입이 공간을 다른 곳에 쓰기 때문에 크기 차이의 좋은 예측 변수는 아니에요. keyword 필드는 값당 하나씩, 몇 개 안 되는 긴 용어를 인덱싱하며 각각이 용어 사전(term dictionary)에 전체로 저장돼요. wildcard 필드는 어떤 데이터셋에서든 구별되는 세 글자 조합의 수가 제한적이므로, 작은 짧은 용어 집합에 대해 많은 발생 횟수를 인덱싱해요. 각각 약 116자의 20,000개의 구별되는 로그 줄 샘플에서, keyword 필드는 2.3MB의 용어 텍스트를 담은 20,000개의 용어를 인덱싱한 반면 wildcard 필드는 16KB를 담은 5,431개의 구별되는 용어를 인덱싱했어요. 따라서 116배 많은 부분 문자열을 인덱싱했지만 인덱스는 13% 더 큰 것에 그쳤어요.
크기 증가는 데이터에 따라 달라지므로 고정 승수는 없어요. 값이 길수록 문서당 더 많은 부분 문자열이 생겨요. 문자 집합의 폭이 훨씬 더 중요해요. 16진수 식별자처럼 좁은 집합에서 뽑은 값은 Base64 페이로드처럼 넓은 집합에서 뽑은 값보다 훨씬 적은 구별되는 부분 문자열을 만들어요. 문자 집합만 다른 20,000개의 구별되는 64자 값 두 샘플에서, wildcard 인덱스는 16진수 값의 경우 동등한 keyword 인덱스보다 6% 더 컸지만 Base64 값의 경우 55% 더 컸어요.
자신의 데이터에 미치는 영향을 추정하려면 대표 샘플을 두 번 인덱싱하세요. 한 번은 keyword 필드로, 한 번은 wildcard 필드로 인덱싱한 뒤 결과 인덱스 크기를 비교하면 돼요.
비교가 왜곡되지 않도록 두 필드 타입 각각에 대해 같은 수의 샤드와 레플리카를 사용해 인덱스를 하나씩 생성하세요.
PUT wildcard-sample
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"log_line": {
"type": "wildcard"
}
}
}
}
PUT keyword-sample
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"log_line": {
"type": "keyword"
}
}
}
}
Bulk API를 사용해 동일한 샘플 문서를 두 인덱스에 인덱싱하세요. 최소 하나 이상의 세그먼트를 채울 만큼 충분한 문서를 사용하세요. 보통 수만 개의 대표 값이면 충분해요.
삭제된 문서와 일부만 채워진 세그먼트가 측정을 왜곡하지 않도록 각 인덱스를 단일 세그먼트로 병합하세요.
POST wildcard-sample,keyword-sample/_forcemerge?max_num_segments=1
Force merge는 리소스를 많이 소모하는 작업이에요. 프로덕션 인덱스가 아니라 테스트 인덱스에서 실행하세요.
병합된 세그먼트가 디스크에 기록되도록 두 인덱스를 flush하세요. 보고되는 store 크기는 flush된 세그먼트만 집계하므로, flush가 완료되기 전에 측정하면 병합된 인덱스를 반영하지 않는 크기가 반환돼요.
POST wildcard-sample,keyword-sample/_flush
측정 전에 각 인덱스가 삭제된 문서 없이 세그먼트 하나를 보고하는지 확인하세요.
GET _cat/segments/wildcard-sample,keyword-sample?v&h=index,segment,docs.count,docs.deleted,size
Index Stats API를 사용해 크기를 비교하세요.
GET wildcard-sample,keyword-sample/_stats/store
primaries 아래의 size_in_bytes 값이 각 인덱스의 디스크 상 크기를 보고해요. 다음의 축약된 응답은 관련 필드를 보여줘요.
{
"indices": {
"wildcard-sample": {
"primaries": {
"store": {
"size_in_bytes": 4600819
}
}
},
"keyword-sample": {
"primaries": {
"store": {
"size_in_bytes": 4052054
}
}
}
}
}
두 값의 비율은 필드를 keyword 대신 wildcard로 매핑했을 때 전체 인덱스가 얼마나 커지는지를 나타내요. 이는 필드 자체 저장 공간의 비율이 아니며, 그 비율은 더 커요. _source와 문서별 구조는 두 인덱스에서 동일하므로 차이를 희석시켜요. 앞의 응답에서 전체 인덱스 비율은 1.14지만 필드만 놓고 보면 1.22예요. 용량 계획에는 전체 인덱스 비율을 사용하고, 그 결과에 레플리카 수 더하기 1을 곱하세요.
인덱스가 단일 세그먼트로 병합되므로 그 크기는 하한선(the floor)이에요. 프로덕션 인덱스는 여러 세그먼트와 삭제된 문서를 보유하며, 둘 다 두 필드 타입 모두에 오버헤드를 추가해요. 문서 수가 늘어나면 비율은 약간 감소하므로, 작은 샘플은 약간 보수적인 추정치를 산출해요.
저장 공간을 줄이려면 ignore_above를 사용해 지정된 길이보다 긴 값이 인덱싱되지 않게 하세요. 필드에 대해 집계, 정렬, 스크립팅이 필요하지 않다면 doc_values를 false로 설정해 doc values를 비활성화할 수도 있어요. 필드에서 doc values가 비활성화되면 _source에서 필드를 제외할 수 없고, 필드에 대한 쿼리가 상당히 느려질 수 있어요. 두 설정 모두 필드를 파생 소스(derived source)에 부적격하게 만들어요.
제한 사항 (Limitations)
wildcard 필드에서는 다음 쿼리가 지원되지 않아요.
- fuzzy 쿼리: 대신
keyword또는text필드에서 실행하세요. - range 쿼리: 이 필드 타입에는 범위 매칭이 적용되지 않아요.
파생 소스 (Derived source)
인덱스가 파생 소스를 사용할 때, OpenSearch는 소스 재구성 중에 wildcard 값을 정렬하고 다중 값 wildcard 필드에서 중복을 제거할 수 있어요.
파생 소스와 함께 wildcard 값을 사용하려면 wildcard 필드가 지원되도록 doc_values를 활성화해야 해요. ignore_above나 normalizer를 설정한 wildcard 필드는 인덱싱된 값에서 원래 값을 재구성할 수 없으므로 파생 소스를 지원하지 않아요.
파생 소스를 활성화하고 doc_values가 활성화된 name 필드를 구성하는 인덱스를 생성하세요.
PUT sample-index1
{
"settings": {
"index": {
"derived_source": {
"enabled": true
}
}
},
"mappings": {
"properties": {
"name": {
"type": "wildcard",
"doc_values": true
}
}
}
}
중복을 포함한 여러 wildcard 값이 있는 문서를 인덱스에 인덱싱하세요.
PUT sample-index1/_doc/1
{
"name": ["ba", "ab", "ac", "ba"]
}
OpenSearch가 _source를 재구성한 후, 파생 _source는 중복을 제거하고 값을 알파벳순으로 정렬해요.
{
"name": ["ab", "ac", "ba"]
}