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_chars
가letter
(언어만 분류) 이면, 먼저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장
'SearchDeveloper > 엘라스틱서치 바이블' 카테고리의 다른 글
[7] 운영 도중 발생하는 장애 대응 (0) | 2024.03.31 |
---|---|
[6] 클러스터 운영 (0) | 2024.03.31 |
[5] 서비스 환경에 클러스터 구성 (0) | 2024.03.31 |
[4] 데이터 다루기 (0) | 2024.03.31 |
[2] 엘라스틱서치 기본 동작과 구조 (0) | 2024.01.14 |