본문 바로가기

SearchDeveloper/엘라스틱서치 바이블

[3] 인덱스 설계

23.11.6

[[대규모 음악 데이터 검색 기능을 위한 Elasticsearch 구성 및 속도 개선 방법]]

인덱스 설정

number_of_shards

-한 번 지정하면 reindex 하지 않는 한 설정 바꾸기 어렵기 때문에 신중하게 설계해야 한다. 

  • 클러스터에 샤드 수가 너무 많으면 → 색인 성능이 감소한다.
  • 인덱스 당 샤드 수를 너무 작게 하면 → 장애 상황 등에서 샤드 복구에 많은 시간이 소요되고 클러스터 안정성이 떨어진다.

-기본값이 7 버전 미만은 5였는데 7부터 1로 변경됐다.

number_of_replicas

-인덱스 생성 후에도 동적으로 변경 가능하다 

http://localhost:9200/_settings
{
  "index" : {
    "number_of_replicas" : 0
  }
}

리플리카 설정 안 하는 경우는 주로 대용량의 초기 데이터를 마이그레이션 하는 등의 쓰기 성능을 일시적으로 끌어올리기 위해 사용한다.

refresh_interval

-디폴트는 1초 이다. 30초(index.search.idle.after) 이상 검색 쿼리가 들어오지 않으면 다음 검색 쿼리가 들어오기 전까지 refresh 를 수행하지 않는다.

-null 로 지정하면 디폴트 값으로 되돌린다.

http://localhost:9200/_settings
{
  "index" : {
    "refresh_interval" : null
  }
}

인덱스 생성 후 response 의미

PUT my_index2

{
 "acknowledged": true,
 "shards_acknowledged": true,
 "index": "my_index2"
}

acknowledged : 클러스터 상태 업데이트가 타임아웃 나기 전에 성공했는지 여부

shards_acknowledged : 샤드가 지정한 개수(wait_for_active_shards , 디폴트 프라이머리 샤드 1개) 만큼 타임아웃 나기 전에 활성화가 되었는지 여부

동적 매핑 vs. 명시적 매핑

  • 동적 매핑(dynamic mapping): ES 가 자동으로 생성하는 매핑
  • 명시적 매핑(explicit mapping): 사용자가 직접 매핑
    • 이미 인덱스 생성된 후에서 신규 필드 추가할 때는 매핑 정보 추가할 수 있다.
    • PUT mapping_test/_mapping
      {
        "properties": {
          "longValue": {
            "type": "long"
          }
        }
      }

-타입 한 번 설정하면 변경이 불가능하다.

필드 타입

작은 비트 사용하는 자료형을 고르면 검색과 색인에는 이득이 있으나 저장할 때는 실제 값에 맞춰 최적화되기 때문에 디스크 사용량에는 이득이 없다.

scaled_float

  • 부동소수점 3.94 를 scaling_factor 가 100 이면 100을 곱해 394로 색인한다.
  • 이후 검색, 집계, 정렬에서는 3.94 처럼 동작한다.
  • 장점: 부동소수점 보다 정수 형태로 저장하는게 압축이 쉬워 디스크 공간을 아낄 수 있다.
  • 단점: 정확도는 떨어진다.

date

아래 처럼 format 지정 가능하다.

"mappings": {
  "properties": {
    "createdDate": {
      "type": "date",
      "format": "strict_date_time || epoch_millis"
    }
  }
}

어떤 형식으로 들어오더라도 내부적으로는 UTC 시간대로 변환하여 epoch milliseconds 형식의 long 숫자로 색인된다.

배열

배열을 표현하는 별도의 타입 구분이 없다. 색인 과정에서 단일 인지 배열인지에 상관없이 각 값마다 하나의 독립적인 역색인을 구성한다.

※ 계층 구조를 지원하는 타입: object, nested

object

put object_test/_doc/1
{
 "spec": {
  "cores": 12,
  "memory": 128,
  "storage": 8000
  }
}

/_mappings 를 날리면 필드 타입이 명시적으로 object 로 표현하지는 않는다. 기본값이라서.

내부적으로는 flat 하게 키-값 쌍으로 색인된다.

{
  "spec.cores": 12,
  "spec.memory": 128,
  "spec.storage": 8000
}

object 랑 nested 랑 검색 결과 다른 경우

object 로 색인했을 때

PUT object_test/_doc/1
{
 "spec": [
    {"cores": 12,"memory": 128,"storage": 8000},
    {"cores": 6,"memory": 64,"storage": 8000},
    {"cores": 6,"memory": 32,"storage": 4000}
  ]
}

cores 가 6, memory 가 12 로 검색하면 결과가 없어야 하는데 나온다.

GET object_test/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {"spec.cores": {"value": "6"}}},
        {"term": {"spec.memory": {"value": "128"}}}
      ]
    }
  }
}

왜? 문서가 아래처럼 평탄화돼서 색인되기 때문

{
  "spec.cores": [12, 6, 6],
  "spec.memory": [128, 64, 32],
  "spec.storage": [8000, 8000, 4000]
}

6과 128이 있으니까

근데 우리가 원하는 결과는 이게 아님! 그래서 나온게 nested 이다

nested

mappings에 nested type 을 명시해주어야 한다.

PUT nested_test
{
  "mappings": {
    "properties": {
      "spec": {
        "type": "nested",
        "properties": {
          "cores": {"type": "long"},
          "memory": {"type": "long"},
          "storage": {"type": "long"}
        }
      }
    }
  }
}

nested type 으로 검색 쿼리를 나오면 원하는 대로 결과가 안 나온다.

GET nested_test/_search
{
  "query": {
    "nested": {
      "path": "spec",
       "query": {
          "bool": {
            "must": [
              {"term": {"spec.cores": {"value": "6"}}},
              {"term": {"spec.memory": {"value": "128"}}}
            ]
          }
        }
    }
  }
}

정확히 cores:12, memory 128 로 해야 결과가 나온다.

이유: nested 는 각 객체를 내부적으로 별도의 루씬 문서로 분리해 저장하기 때문이다. 그래서 object 에 비해 무겁다.

그래서 너무 사이즈가 크면 OOM 이 날 수도 있기 때문에 제한할 수 있는 설정이 있다.

  • index.mapping.nested_fields.limit 한 인덱스에 nested 타입을 몇 개까지 지정할 수 있는지
  • index.mapping.nested_objects.limit 한 문서가 nested 객체를 몇 개까지 가질 수 있는지

애널라이저와 토크나이저

keyword

-analzer 대신 normalizer 를 사용한다. 노멀라이저는 간단한 전처리만 하고 커다란 단일 텀으로 역색인을 구성한다.

match 쿼리

match 는 검색어도 text 타입일 때는 analyze 를 한다. keyword 타입이면 검색어 analyze 안 하는듯!

keyword vs. text

  • 정렬, 집계, 스크립트 작업 수행할 때는 keyword 타입이 적합하다.
  • keyword 는 doc_values 캐시, text 는 fielddata 캐시를 사용한다.

doc_values

  • 정렬, 집계, 스크립트는 역색인이 아닌 문서를 보고 필드 내의 텀을 찾는다. (그래도 keyword 는 역색인을 구성함! 정렬 등 3개 작업할 때 역색인을 사용하지 않는다는 뜻인듯)
  • doc_values 는 디스크를 기반으로 한 자료구조로 파일시스템 캐시를 사용한다.
  • text, annotated_text 타입 빼고 나머지 타입은 doc_values 를 지원한다.
  • 기본값: true

fielddata

  • text 타입에서 사용한다.
  • 정렬, 집계, 스크립트 작업 하려면 역색인 전체를 읽어 힙 메모리에 올린 후 연산한다.
  • 그래서 OOM 이 발생할 수 있다.
  • 기본값: 비활성화

_source

  • 원본 JSON 문서를 저장하는 메타데이터 필드
  • _source.enabled: false 로 설정할 수 있지만,
    • update, update_by_query 를 쓸 수 없다. 업데이트 요청이 들어오면 기존 문서 내용와 변경된 문서 내용을 병합해서 새 문서를 색인해야 하는데, 기존 문서를 읽을 _source 가 없기 때문이다.
    • reindex 를 쓸 수 없다. _source 의 원본 데이터를 읽어 재색인을 수행하기 때문이다. 그러므로 _source 는 웬만하면 enabled:true 로 놓기!
  • 디스크 공간을 절약해야 한다면 _source 비활성화보다는 데이터의 압축률을 높이는 편이 낫다 (공식 가이드)
    • PUT codec
      {
      "settings": {
      "index": {
      "codec": "best_compression"
      }
      } 
      } 

synthetic source (ES 8.4~)

(synthetic: 통합적인)

PUT synthetic
{
  "mappings": {
    "_source": {
      "mode": "synthetic"
    }
  } 
} 

-_source 에 원문을 저장하지 않는다. 대신 source 를 읽어야하면 문서 내 각 필드의 doc_values 를 모아 _source 를 재조립한다.

-제약조건

  • 모든 필드가 doc_values 를 사용해야 한다.
  • 재조립된 _source 도 원문 JSON 과 조금 다를 수 있다.
    • 필드명이나 배열 내 값은 오름차순 정렬되고,
    • . 를 포함한 필드명은 최대한 object 계층 구조로 조립하려한다.

-장점

  • 인덱스 크기를 많이 줄여준다.
  • reindex 도 가능하다.

index

-해당 필드의 역색인을 만들 것인지 결정. 기본값: true

-false 로 하면 검색 결과에는 포함되나 검색 대상이 되지 않는다. (역색인이 없기 때문에)

-doc_values 사용하는 필드면 정렬, 집계 대상 으로는 사용 가능하다.

-ES 8.1 부터는 index:false 여도 keyword 타입이면 doc_values 를 사용해서 검색 대상으로는 만들 수 있다. 대신 검색용으로 설계된 자료형이 아니라 검색 성능은 떨어진다. 대신 색인 성능을 얻는다.

index:false 로 색인하기

PUT string_test
{
  "mappings": {
    "properties": {
      "textString": {
        "type": "text",
        "index": false
      }
    }
  }
}

textString 으로 검색 쿼리 날리면 에러남: failed to create query: Cannot search on field [textString] since it is not indexed.

enabled

-obeject 타입에만 설정할 수 있다.

-false이면 _source 에만 저장되고 그 외 역색인 생성 안 하고(검색 안 됨), doc_values 생성 안 하고(정렬/집계/스크립트 안 됨), 파싱도 안 하기 때문에 배열에 여러 가지 타입이 동시에 들어올 수 있다. → 성능 상의 이득은 있음

-_source 로 확인만 할 데이터라면 고려해볼만 한다.

ngram

GET string_test/_analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 2,
    "max_gram": 3,
    "token_chars": ["letter"]
  },
  "text": ab, cd!
}

token_chars: 후처리 필터

  • token_charsletter (언어만 분류) 이면, 먼저 letter 전 문자 “ab, cd!” 를 ngram 으로 쪼갠 다음에 후처리로 언어인 것만 발라낸다. 전처리가 아님!

분석 결과: ab / cd (bc 는 없다 후처리라)

edge_ngram

GET string_test/_analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 3,
    "max_gram": 4,
    "token_chars": ["letter"]
  },
  "text": "Hello, World!"
}

단어 단위로 쪼갠다음에 edge_ngram 을 시행한다.

분석 결과: Hel / Hell / Wor / Worl

default analyzer 변경하기

PUT keyword_analyzer_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "default": {
          "type": "keyword"
        }
      }
    }
  }
}

keyword analyzer 로 디폴트 변경함

normalizer

-keyword 타입에 지정하고, 단일 토큰을 생성한다.

-토크나이저 없이 char_filter, filter 만 있다.

-디폴트는 아무 normalizer 도 적용하지 않는다.

인덱스 생성하기

PUT normalizer_test
{
  "settings": {
    "analysis": {
      "normalizer": {
        "my_n": {
          "type": "custom",
          "char_filter": [],
          "filter": ["asciifolding", "uppercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "n_keyword": {
        "type": "keyword",
        "normalizer": "my_n"
      }
    }
  }
}

문서 색인하고 검색하기

PUT normalizer_test/_doc/1
{
  "n_keyword": "hello world"
}


GET normalizer_test/_search
{
  "query": {
    "match": {
      "n_keyword": "HELLO WORLD"
    }
  }
}

대문자든, 소문자든, 대문자 소문자 섞어서 검색해도 검색 결과 나온다.

_analyze 로 확인

GET normalizer_test/_analyze
{
  "field": "n_keyword",
  "text": ["hello world"]
}

분석 결과: HELLO WORLD

템플릿

인덱스 템플릿 (ES 7.8~)

PUT _index_template

인덱스에 템플릿 지정

priority 값이 높을수록 우선순위가 높다.

컴포넌트 템플릿 (ES 7.8~)

PUT _component_template

인덱스 템플릿 안에서 쓸 부분 템플릿 지정

레거시 템플릿 (ES 7.8 미만)

PUT _template

우선순위가 낮다.

동적 템플릿

“mappings”: {”dynamic_templates”: [{...}]}

조건에 맞게 동적으로 필드명에 템플릿을 적용할 수 있다.

mappings 안에서 설정한다.

빌트인 인덱스 템플릿 (ES 7.9~)

로그나 메트릭 편리하게 수집하기 위한 X-Pack 전용 추가 기능 Elastic Agent 에서 사용하기 위해 내장된 템플릿이다.

metrics-*-*, logs-*-*

라우팅

  • 몇 번 샤드를 대상으로 작업을 수행할 지 지정할 수 있다. (단일 샤드로 검색 가능)
  • 샤드 번호 구하기: 라우팅값(routing=)을 해시한 후 프라이머리 샤드 개수로 나머지 연상 수행한 값
    • 라우팅값 지정하지 않으면 기본값은 _id 이다.
  • 색인할 때 라우팅 값을 지정했다면 조회, 변경, 삭제, 검색할 때도 똑같은 라우팅 값을 지정해줘야한다.

인덱스 생성 및 라우팅 값 사용하여 색인

PUT routing_test
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1
  }
}

PUT routing_test/_doc/1?routing=myid
{
  "login_id": "myid"
}

routing=myid

**<routing=myid 로 검색하기>
**

GET routing_test/_search?routing=my_id

검색 결과 나온다.

샤드 1개만 검색함. "_shards" : { "total" : 1, "successful" : 1}

<routing 없이 검색하기>

GET routing_test/_search

검색 결과 나온다.

샤드 5개 전체 검색함. "_shards" : { "total" : 5, "successful" : 5}

*→ 라우팅 있는 거랑 없는거랑 성능차이가 매우 크다!
*

<없는 routing= 로 검색하기>

GET routing_test/_search?routing=2

결과 없음.

샤드 1개만 검색함. "_shards" : { "total" : 1, "successful" : 1}

[주의] 고유 _id 강제하기

인데스 내에서 _id 의 고유성 검증은 샤드 단위로 보장된다. 즉, 같은 _id 인데도 routing 값이 다르면 다른 샤드로 가버려서 여러 샤드에 같은 _id 를 가진 문서가 중복으로 존재할 수도 있단 뜻이다.

고유한 _id 를 보장하는 것은 사용자의 책임이다!

※ routing 값 필수로 지정하도록 강제할 수는 있다.

PUT routing_test
{
  "mappings": {
    "_routing": {"required": true}
  }
}

 

레퍼런스

엘라스틱서치 바이블 3장