검색 엔진에 데이터를 넣기 전에 결정해야 할 것이 있다. 이 데이터를 어떤 구조로 저장할 것인가? 관계형 데이터베이스에서 테이블 스키마를 정의하듯, Elasticsearch에서는 매핑(Mapping) 을 정의한다. 매핑은 문서의 각 필드가 어떤 타입이고, 어떻게 인덱싱되며, 어떻게 검색 가능한지를 결정한다.
매핑을 제대로 설계하지 않으면 검색 품질이 떨어지고, 디스크를 낭비하며, 나중에 변경하기도 어렵다. 이 글에서는 매핑의 핵심 개념부터 실전 인덱싱 전략까지 다룬다.
매핑(Mapping)이란?
매핑은 인덱스에 저장되는 문서의 스키마 정의다. 각 필드의 데이터 타입, 분석 방법, 저장 방식 등을 지정한다.
PUT /products
{
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "standard" },
"price": { "type": "integer" },
"category": { "type": "keyword" },
"description": { "type": "text" },
"created_at": { "type": "date" },
"in_stock": { "type": "boolean" },
"location": { "type": "geo_point" }
}
}
}
매핑에서 가장 중요한 결정은 필드 타입 선택이다. 같은 문자열이라도 text와 keyword는 완전히 다르게 동작한다.
핵심 필드 타입
text vs keyword
이 두 타입의 차이를 이해하는 것이 Elasticsearch 매핑의 출발점이다.
text 타입은 전문 검색(Full-text Search) 을 위한 타입이다. 저장 시 Analyzer를 통해 토큰으로 분해된다. "Elasticsearch is powerful"이라는 문자열은 ["elasticsearch", "is", "powerful"]로 분리되어 역색인에 저장된다. 이후 "powerful"로 검색하면 이 문서가 매칭된다.
keyword 타입은 정확한 값 매칭을 위한 타입이다. 문자열을 분석하지 않고 그대로 저장한다. "Electronics"라는 카테고리를 keyword로 저장하면, 정확히 "Electronics"로 검색해야만 매칭된다. 필터링, 정렬, 집계에 사용한다.
| 특성 | text | keyword |
|---|---|---|
| 분석(Analyze) | O | X |
| 전문 검색 | O | X |
| 정확한 매칭 | X | O |
| 정렬/집계 | X | O |
| 적합한 데이터 | 제목, 본문, 설명 | 카테고리, 상태, 이메일, ID |
실무에서는 하나의 필드에 두 타입을 모두 적용하는 Multi-field 패턴이 흔하다.
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
이렇게 하면 title로 전문 검색하고, title.keyword로 정렬이나 집계가 가능하다.
숫자 타입
| 타입 | 범위 | 용도 |
|---|---|---|
byte | -128 ~ 127 | 매우 작은 정수 |
short | -32,768 ~ 32,767 | 작은 정수 |
integer | -2^31 ~ 2^31-1 | 일반 정수 |
long | -2^63 ~ 2^63-1 | 큰 정수 |
float | 32bit 부동소수 | 일반 실수 |
double | 64bit 부동소수 | 정밀한 실수 |
scaled_float | 고정소수점 | 가격 등 (scaling_factor 지정) |
가격처럼 정밀도가 중요한 데이터에는 scaled_float이 유용하다. scaling_factor: 100으로 설정하면 19.99를 내부적으로 1999로 저장하여 부동소수점 오차를 피한다.
날짜 타입
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
date 타입은 내부적으로 epoch_millis(long) 로 저장된다. format으로 입력 가능한 형식을 지정하며, ||로 여러 형식을 허용할 수 있다.
기타 주요 타입
boolean: true/falsegeo_point: 위도/경도 좌표. 거리 기반 검색이 가능하다nested: 객체 배열에서 각 객체의 독립성을 유지한다. 일반object타입은 배열 내 객체의 필드가 섞여버린다join: 부모-자식 관계를 같은 인덱스 내에서 정의한다
동적 매핑(Dynamic Mapping)
매핑을 미리 정의하지 않고 문서를 넣으면, Elasticsearch가 자동으로 매핑을 추론한다. 이를 동적 매핑이라 한다.
// 매핑 없이 문서 인덱싱
POST /my-index/_doc
{
"name": "iPhone 15",
"price": 1299,
"available": true
}
Elasticsearch는 name을 text(+ keyword sub-field), price를 long, available을 boolean으로 자동 매핑한다.
동적 매핑의 문제점
편리하지만 위험하다. 몇 가지 문제가 있다.
타입 추론 실패: 숫자 문자열 "12345"가 처음 들어오면 text로 매핑된다. 이후 실제 숫자 12345를 넣으면 타입 충돌이 발생한다.
불필요한 인덱싱: 로그 메시지의 모든 필드가 text로 인덱싱되면, 검색할 필요 없는 필드까지 역색인이 생성되어 디스크와 메모리를 낭비한다.
매핑 폭발(Mapping Explosion): 동적 필드가 무한히 늘어나면 클러스터 성능이 저하된다.
동적 매핑 제어
PUT /strict-index
{
"mappings": {
"dynamic": "strict",
"properties": {
"name": { "type": "text" },
"price": { "type": "integer" }
}
}
}
| dynamic 값 | 동작 |
|---|---|
true (기본값) | 새 필드 자동 추가 |
false | 새 필드 저장은 되지만 인덱싱/검색 불가 |
strict | 매핑에 없는 필드가 오면 에러 |
runtime | 런타임 필드로 자동 추가 |
프로덕션에서는 strict 또는 false 를 권장한다. 스키마가 명확한 비즈니스 데이터에는 strict, 로그처럼 유연성이 필요한 데이터에는 false가 적합하다.
인덱스 템플릿(Index Template)
동일한 패턴의 인덱스를 반복 생성할 때, 매번 매핑을 지정하는 것은 비효율적이다. 인덱스 템플릿은 인덱스 이름 패턴에 맞는 인덱스가 생성될 때 자동으로 매핑과 설정을 적용한다.
PUT _index_template/logs-template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"dynamic": "false",
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"message": { "type": "text" },
"service": { "type": "keyword" }
}
}
},
"priority": 100
}
이후 logs-2026-02-12라는 인덱스가 생성되면 이 템플릿의 매핑과 설정이 자동 적용된다. 시계열 데이터를 날짜별 인덱스로 관리할 때 필수적이다.
인덱싱 전략
적절한 샤드 수 결정
인덱스를 생성할 때 샤드 수를 결정해야 한다. 샤드는 인덱스의 물리적 분할 단위이며, 한 번 설정하면 변경할 수 없다(reindex 필요).
경험적 가이드라인:
- 샤드 하나당 10~50GB 가 적절하다
- 샤드가 너무 적으면 단일 샤드가 비대해지고, 너무 많으면 오버헤드가 커진다
- 노드당 샤드 수는 20개 이내가 안정적이다 (힙 메모리 1GB당 약 20개)
PUT /large-data
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
시계열 데이터 인덱싱
로그, 메트릭 같은 시계열 데이터는 날짜 기반 인덱스 패턴이 표준이다.
logs-2026-02-10
logs-2026-02-11
logs-2026-02-12
장점은 세 가지이다.
- 검색 최적화: 특정 기간만 검색할 때 해당 인덱스만 조회한다
- 데이터 관리: 오래된 인덱스를 통째로 삭제할 수 있다
- ILM 적용: Index Lifecycle Management로 Hot → Warm → Cold → Delete 자동 관리
ILM(Index Lifecycle Management)
ILM은 인덱스의 생명주기를 자동으로 관리한다.
PUT _ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "1d"
}
}
},
"warm": {
"min_age": "7d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"freeze": {}
}
},
"delete": {
"min_age": "90d",
"actions": {
"delete": {}
}
}
}
}
}
- Hot: 활발히 쓰기/읽기. 고성능 SSD에 저장
- Warm: 읽기 전용. 세그먼트 병합으로 최적화
- Cold: 거의 조회하지 않는 데이터. 저비용 스토리지
- Delete: 보존 기간 만료 시 자동 삭제
매핑 변경의 한계
한 번 생성된 매핑의 기존 필드 타입은 변경할 수 없다. text로 매핑된 필드를 keyword로 바꾸는 것은 불가능하다. 이미 역색인이 해당 타입 기준으로 구축되었기 때문이다.
매핑을 변경해야 한다면 Reindex가 필요하다.
POST _reindex
{
"source": { "index": "old-products" },
"dest": { "index": "new-products" }
}
새 매핑으로 인덱스를 생성하고, 기존 데이터를 복사한 뒤, Alias를 전환하는 패턴이다.
Alias 활용
Alias는 인덱스에 대한 별칭이다. Reindex나 인덱스 교체 시 애플리케이션 코드를 변경하지 않아도 된다.
POST _aliases
{
"actions": [
{ "remove": { "index": "products-v1", "alias": "products" } },
{ "add": { "index": "products-v2", "alias": "products" } }
]
}
애플리케이션은 항상 products라는 Alias를 통해 접근하므로, 뒤에서 인덱스가 교체되어도 영향을 받지 않는다. 프로덕션에서는 인덱스에 직접 접근하지 말고 반드시 Alias를 사용하는 것이 모범 사례이다.
정리
- 매핑은 Elasticsearch의 스키마 정의이며, 필드 타입 선택이 검색 품질과 성능을 좌우한다
- text vs keyword: 전문 검색은 text, 정확한 매칭/정렬/집계는 keyword. Multi-field로 둘 다 적용하는 것이 일반적이다
- 동적 매핑은 편리하지만 프로덕션에서는
strict또는false로 제어해야 한다 - 인덱스 템플릿으로 반복적인 매핑 정의를 자동화한다
- 시계열 데이터는 날짜 기반 인덱스 + ILM으로 생명주기를 관리한다
- 매핑 변경은 Reindex가 필요하며, Alias를 통해 무중단 교체가 가능하다