다운타임 없이 인덱스 매핑(필드 타입, 분석기 등)을 변경하는 방법을 안내합니다.
소요 시간: 약 20-40분 (데이터 크기에 따라 추가 소요)
이 가이드의 범위다루는 내용: Reindex API를 활용한 무중단 매핑 변경, Alias 전환 전략, 검증 방법
다루지 않는 내용: 대규모 인덱스 재구축은 인덱스 재구축을, 클러스터 수준 변경은 클러스터 확장을 참조하세요.
TL;DR
- Alias 기반 운영: 애플리케이션은 Alias를 통해 인덱스에 접근
- 새 인덱스 생성: 변경된 매핑으로 새 인덱스를 만들고 Reindex
- Alias 전환: 검증 완료 후 Alias를 새 인덱스로 전환
- 롤백 가능: 이전 인덱스를 삭제하지 않으면 즉시 롤백 가능
시작하기 전에#
다음 조건을 확인하세요:
| 항목 | 요구사항 | 확인 방법 |
|---|---|---|
| Elasticsearch 버전 | 7.x 이상 | curl -X GET "localhost:9200" |
| 클러스터 상태 | green 또는 yellow | curl -X GET "localhost:9200/_cluster/health" |
| 인덱스 접근 권한 | 읽기 + 쓰기 + Alias 관리 | 아래 명령어로 테스트 |
# 현재 매핑 확인
curl -X GET "localhost:9200/products/_mapping?pretty"
# Alias 상태 확인
curl -X GET "localhost:9200/_cat/aliases?v"주의
Reindex는 소스 인덱스의 모든 문서를 복사합니다. 디스크 공간이 기존 인덱스 크기의 2배 이상 있는지 확인하세요.
증상#
다음과 같은 상황에서 매핑 마이그레이션이 필요합니다:
- 필드 타입을 변경해야 할 때 (예:
text→keyword,long→double) - 분석기를 교체해야 할 때 (예:
standard→nori한글 분석기) - 기존 필드에
multi-field를 추가해야 할 때
# 매핑 변경 시도 시 오류 예시
curl -X PUT "localhost:9200/products/_mapping" -H 'Content-Type: application/json' -d'
{
"properties": {
"price": { "type": "double" }
}
}'
# 응답: "mapper [price] cannot be changed from type [long] to [double]"마이그레이션 흐름#
flowchart TD
A["현재 상태<br>products-v1 인덱스<br>+ products Alias"] --> B["1. 새 매핑으로<br>products-v2 생성"]
B --> C["2. Reindex API로<br>데이터 복사"]
C --> D["3. 문서 수 비교<br>+ 샘플 쿼리 검증"]
D --> E{검증 통과?}
E -->|Yes| F["4. Alias를<br>products-v2로 전환"]
E -->|No| G["원인 분석 후<br>재시도"]
F --> H["5. 기존 인덱스<br>products-v1 정리"]
G --> B1단계: 현재 인덱스 상태 확인#
1.1 기존 매핑 백업#
# 현재 매핑을 파일로 저장
curl -s -X GET "localhost:9200/products/_mapping?pretty" > products_mapping_backup.json
# 현재 설정도 저장
curl -s -X GET "localhost:9200/products/_settings?pretty" > products_settings_backup.json
# 문서 수 기록 (나중에 검증용)
curl -s -X GET "localhost:9200/products/_count?pretty"1.2 Alias 확인#
애플리케이션이 Alias를 사용하고 있는지 확인합니다:
# Alias 목록 확인
curl -X GET "localhost:9200/_cat/aliases/products?v"
# 결과 예시:
# alias index filter routing.index routing.search
# products products-v1 - - -주의
Alias 없이 인덱스 이름을 직접 사용하고 있다면, 먼저 Alias를 설정하세요. 그래야 무중단 전환이 가능합니다.
# Alias가 없는 경우: 기존 인덱스에 Alias 추가
curl -X POST "localhost:9200/_aliases" -H 'Content-Type: application/json' -d'
{
"actions": [
{ "add": { "index": "products-v1", "alias": "products" } }
]
}'2단계: 새 인덱스 생성#
변경된 매핑으로 새 인덱스를 만듭니다:
# 예시: price를 long → double로, name에 nori 분석기 적용
curl -X PUT "localhost:9200/products-v2" -H 'Content-Type: application/json' -d'
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0,
"refresh_interval": "-1",
"analysis": {
"analyzer": {
"korean": {
"type": "custom",
"tokenizer": "nori_tokenizer"
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "korean",
"fields": {
"keyword": { "type": "keyword" }
}
},
"price": { "type": "double" },
"category": { "type": "keyword" },
"created_at": { "type": "date" }
}
}
}'팁
Reindex 성능을 위해 새 인덱스 생성 시refresh_interval: "-1"과number_of_replicas: 0으로 설정합니다. 마이그레이션 완료 후 원래 값으로 복구합니다.
3단계: Reindex로 데이터 복사#
3.1 기본 Reindex#
curl -X POST "localhost:9200/_reindex" -H 'Content-Type: application/json' -d'
{
"source": {
"index": "products-v1"
},
"dest": {
"index": "products-v2"
}
}'3.2 필드 변환이 필요한 경우#
Script를 사용하여 데이터를 변환하면서 복사할 수 있습니다:
curl -X POST "localhost:9200/_reindex" -H 'Content-Type: application/json' -d'
{
"source": {
"index": "products-v1"
},
"dest": {
"index": "products-v2"
},
"script": {
"source": "ctx._source.price = (double) ctx._source.price",
"lang": "painless"
}
}'3.3 대용량 인덱스의 경우#
비동기로 실행하여 타임아웃을 피합니다:
# 비동기 실행
curl -X POST "localhost:9200/_reindex?wait_for_completion=false" -H 'Content-Type: application/json' -d'
{
"source": {
"index": "products-v1",
"size": 5000
},
"dest": {
"index": "products-v2"
}
}'
# 응답: {"task": "node-1:12345"}
# 진행 상황 확인
curl -X GET "localhost:9200/_tasks/node-1:12345?pretty"4단계: 검증#
4.1 문서 수 비교#
# 원본 문서 수
curl -s -X GET "localhost:9200/products-v1/_count" | python3 -m json.tool
# 새 인덱스 문서 수
curl -s -X GET "localhost:9200/products-v2/_count" | python3 -m json.tool
# 두 값이 동일해야 합니다4.2 샘플 쿼리 테스트#
# 새 인덱스에서 refresh 실행 (검색 가능하도록)
curl -X POST "localhost:9200/products-v2/_refresh"
# 샘플 검색 테스트
curl -X GET "localhost:9200/products-v2/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match": { "name": "갤럭시" }
}
}'
# 집계 테스트
curl -X GET "localhost:9200/products-v2/_search?pretty" -H 'Content-Type: application/json' -d'
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}'4.3 매핑 변경 확인#
# 새 매핑이 올바르게 적용되었는지 확인
curl -X GET "localhost:9200/products-v2/_mapping?pretty"5단계: Alias 전환#
검증이 완료되면 Alias를 원자적으로 전환합니다:
# 원자적 Alias 전환 (다운타임 없음)
curl -X POST "localhost:9200/_aliases" -H 'Content-Type: application/json' -d'
{
"actions": [
{ "remove": { "index": "products-v1", "alias": "products" } },
{ "add": { "index": "products-v2", "alias": "products" } }
]
}'참고_aliasesAPI의 actions 배열은 원자적으로 실행됩니다. 두 동작 사이에 검색 요청이 실패하는 일은 없습니다.
6단계: 마이그레이션 후 설정 복구#
# refresh_interval 복구
curl -X PUT "localhost:9200/products-v2/_settings" -H 'Content-Type: application/json' -d'
{ "refresh_interval": "1s" }'
# replica 수 복구
curl -X PUT "localhost:9200/products-v2/_settings" -H 'Content-Type: application/json' -d'
{ "number_of_replicas": 1 }'체크리스트#
매핑 마이그레이션 시 확인사항:
- 디스크 공간이 충분한가? - 기존 인덱스 크기의 2배 이상
- 새 매핑을 테스트했는가? - 소규모 데이터로 먼저 검증
- Alias를 사용하고 있는가? - 무중단 전환의 전제 조건
- 문서 수가 일치하는가? - 원본과 새 인덱스 비교
- 샘플 쿼리가 정상인가? - 검색, 집계 모두 테스트
- 설정을 복구했는가? - refresh_interval, replica 수
성공 확인#
마이그레이션이 성공했는지 다음 방법으로 확인하세요:
Alias 확인: Alias가 새 인덱스를 가리키는지 확인
curl -X GET "localhost:9200/_cat/aliases/products?v" # products products-v2 가 출력되어야 함애플리케이션 동작 확인: 기존 API가 정상적으로 응답하는지 확인
성능 확인: 변경된 매핑으로 인한 성능 차이가 없는지 확인
curl -X GET "localhost:9200/products/_search?pretty" -H 'Content-Type: application/json' -d' { "query": { "match": { "name": "테스트" } } }' | grep took
성공 기준
- Alias가 새 인덱스(products-v2)를 가리킴
- 문서 수가 원본과 동일
- 애플리케이션 쿼리가 정상 동작
- 롤백이 필요하면 Alias만 되돌리면 됨
자주 발생하는 오류#
“mapper_parsing_exception”#
{
"error": {
"type": "mapper_parsing_exception",
"reason": "failed to parse field [price] of type [double]"
}
}원인: 원본 데이터가 새 매핑의 타입으로 변환할 수 없음
해결: Reindex 시 Script로 데이터를 변환하세요:
"script": {
"source": "ctx._source.price = Double.parseDouble(ctx._source.price.toString())"
}Reindex 속도가 느림#
원인: 기본 설정으로는 throttle이 걸릴 수 있음
해결: requests_per_second를 조정하세요:
curl -X POST "localhost:9200/_reindex?requests_per_second=-1" -H 'Content-Type: application/json' -d'
{
"source": { "index": "products-v1" },
"dest": { "index": "products-v2" }
}'“index_already_exists_exception”#
원인: products-v2 인덱스가 이미 존재
해결: 기존 실패한 인덱스를 삭제하고 재시도하세요:
curl -X DELETE "localhost:9200/products-v2"