Elasticsearch 매핑과 분석기
필드 타입과 분석기 파이프라인이 검색 정확도와 색인 비용을 어떻게 가르는지 정리합니다.
Contents
매핑은 필드를 어떤 타입으로 저장할지 정하고, 분석기는 텍스트를 어떤 토큰으로 쪼갤지 정합니다. 이 두 설계가 검색 정확도와 색인 비용을 거의 결정합니다.
매핑이 검색 품질을 먼저 정한다
Elasticsearch에서 매핑(Mapping)은 JSON 문서의 각 필드를 어떤 타입으로 역색인에 저장할지 정의하는 스키마입니다. 관계형 데이터베이스의 테이블 스키마와 비슷하지만, 내부의 Lucene 엔진이 각 필드를 어떻게 분석하고 색인할지까지 제어합니다. 같은 문자열이라도 매핑에 따라 전문 검색 대상이 되기도 하고, 정확 매칭 전용이 되기도 합니다.
필드 타입을 잘못 잡으면 원하는 쿼리가 아예 동작하지 않거나, 불필요한 메모리를 소모합니다. 쿼리를 아무리 다듬어도 매핑이 어긋나면 메우기 어렵습니다. 그래서 설계 단계에서 검색어 패턴을 먼저 정리하는 편이 안전합니다.
동적 매핑과 정적 매핑
매핑을 만드는 방법은 두 가지입니다. 동적 매핑(Dynamic Mapping)은 사전에 정의하지 않은 문서가 들어올 때, 값의 형태를 보고 타입을 자동으로 추론하는 방식입니다. 데이터 모델이 확정되지 않은 초기 단계에서 적재 속도를 높여 줍니다.
문제는 운영 환경에서 드러납니다. 숫자로만 이루어진 문자열이 정수형으로 잡히는 식의 타입 오판이 생기면, 데이터 무결성이 깨지고 쿼리가 실패할 수 있습니다. 그래서 프로덕션에서는 정적 매핑(Static Mapping)으로 인덱스를 명시적으로 선언하는 편을 모범 사례로 봅니다.
| 전략 | 특징 | 주의점 |
|---|---|---|
| 동적 매핑 | 초기 적재가 빠릅니다 | 의도하지 않은 필드 증가와 타입 오판 가능 |
| 정적 매핑 | 필드 제어가 쉽습니다 | 설계 시간이 더 필요합니다 |
| 멀티 필드 | text와 keyword를 함께 둘 수 있습니다 | 저장 비용이 증가합니다 |
정적 매핑은 인덱스를 만들 때 필드 타입과 분석기, 멀티 필드 여부를 한 번에 선언합니다. 표의 세 전략은 양자택일이 아니라, 핵심 필드만 정적으로 고정하고 나머지는 동적 매핑을 제한적으로 허용하는 식으로 섞어 씁니다.
자주 쓰는 필드 타입
문자열 외에도 검색 의도에 맞춰 타입을 나눕니다.
- text: 전문 검색 대상입니다. 분석기를 거쳐 토큰으로 쪼개집니다.
- keyword: 정확한 매칭, 집계, 정렬에 씁니다. 값을 통째로 저장합니다.
- date: 시간 기반 필터와 분석에 씁니다.
- numeric(integer, long 등): 범위 필터와 수치 집계에 씁니다.
- dense_vector: 임베딩 기반 벡터 검색에 씁니다.
타입 선택의 핵심은 그 필드로 무엇을 할지입니다. 본문처럼 '안에 든 단어'를 찾아야 하면 text가 맞고, 상태값처럼 '값 자체'로 거르거나 묶어야 하면 keyword가 맞습니다.
text와 keyword, 그리고 멀티 필드
text와 keyword의 차이는 분석기를 거치느냐입니다. text 필드는 분석기 파이프라인을 통과해 여러 토큰으로 나뉘므로, 문장 속 단어를 찾는 match 쿼리에 적합합니다. keyword 필드는 값을 쪼개지 않고 통째로 색인하므로, 정확 매칭과 집계, 정렬에 적합합니다.
한 필드를 두 용도로 모두 써야 할 때 멀티 필드(Multi-field)를 씁니다. 같은 문자열을 text로도, 하위 필드 keyword로도 동시에 색인하는 패턴입니다.
PUT /products
{
"mappings": {
"properties": {
"name": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
},
"price": { "type": "integer" },
"created_at": { "type": "date" }
}
}
}이렇게 두면 name으로는 전문 검색을, name.keyword로는 정확 매칭과 집계를 합니다. 다만 같은 값을 두 번 색인하므로 저장 비용이 늘어, 두 용도가 모두 필요한 필드에만 적용합니다.
분석기 파이프라인
분석기(Analyzer)는 원문 텍스트를 검색 가능한 토큰으로 가공하는 단계입니다. 세 구성 요소를 순서대로 거칩니다.
- Character Filter: 토크나이저에 닿기 전 문자열을 다듬습니다. HTML 태그 제거나 문자 치환 같은 작업을 합니다.
- Tokenizer: 문자열을 의미 단위인 토큰으로 분해합니다. 기본값 Standard Tokenizer는 공백과 구두점을 기준으로 나눕니다.
- Token Filter: 쪼개진 토큰을 가공합니다. 소문자 변환(Lowercase), 불용어 제거, 동의어 병합, 어근 추출(Stemming)이 여기서 일어납니다.
이 파이프라인을 통과한 토큰만 역색인(Inverted Index)에 등록됩니다. 역색인은 각 토큰이 어느 문서에 등장하는지 미리 매핑해 둔 구조라, 전체 문서를 훑지 않고 토큰에 걸린 문서 목록을 바로 돌려줍니다. 전문 검색이 대용량에서도 빠른 이유가 이 구조입니다.
핵심은 색인 시점과 검색 시점에 같은 규칙으로 텍스트를 해석하는지입니다. 두 시점의 분석기가 어긋나면, 분명히 있는 문서가 검색되지 않습니다.
한국어 검색과 nori
Standard Tokenizer는 공백과 구두점으로 나누므로 한국어에 약합니다. '삼성전자의'와 '삼성전자'를 다른 토큰으로 봐서, 조사가 붙은 단어를 놓칩니다. 그래서 한국어는 형태소 분석기를 씁니다.
nori는 Elasticsearch 공식 한국어 형태소 분석 플러그인(analysis-nori)입니다. nori_tokenizer가 문장을 형태소 단위로 쪼개고, nori_part_of_speech 필터로 조사와 어미 같은 품사를 걸러냅니다. 플러그인을 따로 설치해야 쓸 수 있습니다.
PUT /stocks
{
"settings": {
"analysis": {
"analyzer": {
"korean": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"filter": ["lowercase", "nori_part_of_speech"]
}
}
}
},
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "korean" }
}
}
}자동 완성은 본문 검색과 토큰화 규칙이 다릅니다. 입력 중간 글자로도 매칭해야 하므로, 전용 필드와 전용 분석기를 분리해 설계합니다. 본문용 분석기에 자동 완성까지 떠넘기면 둘 다 어정쩡해집니다.
실무 설계 기준
필드 타입은 검색어 패턴에서 역산합니다. 어떤 값으로 무엇을 할지부터 정하면 타입이 따라옵니다.
- 사용자 이름, 상태값, 태그: keyword
- 본문, 설명, 리뷰: text
- 검색과 집계를 둘 다 해야 하는 문자열: 멀티 필드
- 자동 완성: 전용 필드와 전용 분석기 분리
가장 비싼 실수는 분석기를 나중에 바꾸는 일입니다. 분석기를 바꾸면 이미 저장된 토큰과 규칙이 달라지므로, 전체 재색인(Reindex)이 필요합니다. 운영 인덱스에서 재색인은 시간과 자원이 들기 때문에, 매핑은 적재 전에 검색어 패턴을 기준으로 확정하는 편이 안전합니다.
정리
매핑은 필드 타입을, 분석기는 토큰화 규칙을 정합니다. text는 분석기를 거쳐 전문 검색에, keyword는 통째로 저장해 정확 매칭과 집계에 쓰고, 둘 다 필요하면 멀티 필드로 묶습니다. 분석기는 Character Filter, Tokenizer, Token Filter를 거쳐 역색인을 만듭니다. 색인과 검색 시점의 규칙이 같아야 빠지는 결과가 없습니다.
한국어는 nori 같은 형태소 분석기로 조사를 걸러야 검색이 정확해집니다. 자동 완성은 본문 검색과 토큰화가 다르므로 전용 필드와 전용 분석기로 분리합니다. 분석기를 바꾸면 전체 재색인이 필요하니, 매핑은 적재 전에 검색어 패턴을 기준으로 확정하는 편이 좋습니다.