데이터베이스에 수억 건의 상품 정보가 있다고 하자. 사용자가 "빨간색 나이키 운동화"를 검색하면, 관계형 데이터베이스는 LIKE '%빨간색%' AND LIKE '%나이키%'처럼 전체 레코드를 하나하나 훑어야 한다. 데이터가 늘어날수록 검색은 느려진다.
Elasticsearch는 이 문제를 해결하기 위해 탄생한 분산 검색 및 분석 엔진이다. Apache Lucene을 기반으로 만들어졌으며, RESTful API를 통해 데이터를 색인하고 검색할 수 있다. 위키백과, GitHub, Netflix 등 대규모 서비스의 검색 기능 뒤에는 Elasticsearch가 있다.
이 글에서는 Elasticsearch의 핵심 구조인 인덱스, 매핑, 샤드, 역색인을 다루며, 왜 이 엔진이 빠른지 그 원리를 살펴본다.
Elasticsearch란?
Elasticsearch는 Java로 작성된 오픈소스 분산 검색 엔진이다. 내부적으로 Apache Lucene 라이브러리를 사용하여 텍스트를 색인(indexing)하고 검색(searching)한다. Lucene 자체는 단일 머신에서 동작하는 라이브러리에 불과하지만, Elasticsearch는 이를 분산 시스템으로 확장하여 수십, 수백 대의 노드에서 데이터를 처리할 수 있게 만들었다.
핵심 특징을 정리하면 다음과 같다.
- RESTful API: 모든 작업을 HTTP 요청으로 수행한다.
PUT,GET,POST,DELETE만으로 데이터를 다룰 수 있어 언어에 구애받지 않는다. - JSON 기반: 데이터를 JSON 문서 형태로 저장하고, 검색 결과도 JSON으로 반환한다.
- Near Real-Time(NRT): 문서를 색인한 후 약 1초 이내에 검색 가능한 상태가 된다. 완전한 실시간은 아니지만, 거의 즉시 반영된다.
- 분산 아키텍처: 데이터를 여러 노드에 분산 저장하고, 노드 장애 시에도 자동으로 복구한다.
관계형 데이터베이스와의 가장 큰 차이는 역색인(Inverted Index) 이라는 자료 구조에 있다. 이 구조 덕분에 전문 검색(Full-Text Search)에서 압도적인 속도를 보인다. 역색인의 원리는 뒤에서 자세히 다룬다.
Index & Document
Document: 데이터의 기본 단위
Elasticsearch에서 데이터의 최소 단위는 문서(Document) 이다. 하나의 문서는 JSON 객체로 표현되며, 관계형 데이터베이스의 행(row)에 해당한다.
{
"title": "Elasticsearch 기초",
"author": "홍길동",
"published_date": "2026-02-11",
"content": "Elasticsearch는 분산 검색 엔진이다...",
"tags": ["search", "database"]
}
각 문서는 고유한 _id 를 가진다. 직접 지정할 수도 있고, Elasticsearch가 자동으로 생성하기도 한다.
Index: 문서의 논리적 그룹
인덱스(Index) 는 유사한 특성을 가진 문서들의 논리적 모음이다. 관계형 데이터베이스의 테이블에 비유할 수 있다. 예를 들어, 블로그 포스트를 저장하는 blog-posts 인덱스, 사용자 정보를 저장하는 users 인덱스를 별도로 만들 수 있다.
| 관계형 DB | Elasticsearch |
|---|---|
| Database | Cluster |
| Table | Index |
| Row | Document |
| Column | Field |
| Schema | Mapping |
CRUD 기본 작업
Elasticsearch의 모든 작업은 REST API로 수행된다.
문서 생성 (Indexing)
PUT /blog-posts/_doc/1
{
"title": "Elasticsearch 기초",
"author": "홍길동",
"published_date": "2026-02-11"
}
문서 조회
GET /blog-posts/_doc/1
문서 검색
GET /blog-posts/_search
{
"query": {
"match": {
"title": "Elasticsearch"
}
}
}
문서 수정
POST /blog-posts/_update/1
{
"doc": {
"author": "김철수"
}
}
문서 삭제
DELETE /blog-posts/_doc/1
PUT으로 문서를 넣고, GET으로 가져오고, DELETE로 지우는 구조가 직관적이다. 별도의 클라이언트 라이브러리 없이 curl 명령어만으로도 Elasticsearch를 완전히 다룰 수 있다.
Mapping & Fields
Mapping이란?
매핑(Mapping) 은 인덱스에 저장되는 문서의 구조를 정의하는 것이다. 관계형 데이터베이스의 스키마(Schema)에 해당하며, 각 필드가 어떤 데이터 타입을 가지고, 어떻게 색인될지를 결정한다.
매핑이 중요한 이유는 필드 타입에 따라 색인 방식이 완전히 달라지기 때문이다. 예를 들어, text 타입 필드는 분석기(Analyzer)를 거쳐 역색인에 토큰 단위로 저장되지만, keyword 타입 필드는 분석 없이 원본 그대로 저장된다.
주요 필드 타입
| 타입 | 설명 | 사용 예 |
|---|---|---|
| text | 전문 검색용. 분석기가 토큰으로 분리 | 본문, 설명, 제목 |
| keyword | 정확한 값 매칭. 분석 없이 저장 | 상태값, 태그, 이메일 |
| integer / long | 정수형 | 조회수, 나이 |
| float / double | 부동소수점 | 가격, 평점 |
| date | 날짜/시간 | 생성일, 수정일 |
| boolean | true/false | 공개 여부 |
| object | 중첩된 JSON 객체 | 주소, 메타데이터 |
가장 혼동하기 쉬운 것이 text 와 keyword 의 차이다.
text: "Elasticsearch is great"를["elasticsearch", "is", "great"]로 분리하여 색인한다. 전문 검색(Full-Text Search)에 사용한다.keyword: "Elasticsearch is great"를 있는 그대로 하나의 값으로 색인한다. 정렬, 집계, 정확한 값 필터링에 사용한다.
같은 필드를 두 가지 방식으로 모두 사용해야 한다면, multi-field 기능으로 하나의 필드에 text와 keyword 타입을 동시에 지정할 수 있다.
Dynamic Mapping vs Explicit Mapping
Dynamic Mapping(동적 매핑) 은 문서를 색인할 때 Elasticsearch가 자동으로 필드 타입을 추론하는 방식이다. 별도의 매핑을 정의하지 않아도, 첫 번째 문서가 들어오면 각 필드의 값을 보고 타입을 결정한다.
JSON 데이터 타입 → Elasticsearch 필드 타입
──────────────────────────────────────────
"hello" → text (+ keyword 서브필드)
123 → long
12.5 → float
true → boolean
"2026-02-11" → date
{ "key": "val" } → object
편리하지만, 의도하지 않은 타입이 지정될 수 있다. 예를 들어, 우편번호 "06234"를 text로 색인하면 불필요한 토큰 분석이 이루어진다. 실제로는 keyword가 적합하다.
Explicit Mapping(명시적 매핑) 은 인덱스 생성 시 직접 매핑을 정의하는 방식이다. 프로덕션 환경에서는 대부분 이 방식을 사용한다.
PUT /blog-posts
{
"mappings": {
"properties": {
"title": { "type": "text" },
"author": { "type": "keyword" },
"published_date": { "type": "date" },
"view_count": { "type": "integer" },
"content": { "type": "text" },
"tags": { "type": "keyword" }
}
}
}
한 가지 주의할 점이 있다. 이미 생성된 필드의 매핑은 변경할 수 없다. 기존 데이터의 역색인 구조가 깨질 수 있기 때문이다. 매핑을 변경하려면 인덱스를 새로 만들고 데이터를 재색인(reindex)해야 한다. 이 때문에 초기 매핑 설계가 매우 중요하다.
dynamic 파라미터를 사용하면 동적 매핑의 동작을 제어할 수 있다.
true(기본값): 새로운 필드를 자동으로 매핑에 추가false: 새로운 필드를 무시 (저장은 되지만 색인/검색 불가)strict: 매핑에 없는 필드가 들어오면 문서 자체를 거부
역색인(Inverted Index): 검색이 빠른 이유
Elasticsearch가 빠른 근본적인 이유는 역색인(Inverted Index) 자료 구조에 있다. 이 개념을 이해하면 Elasticsearch의 검색 원리가 명확해진다.
정방향 색인 vs 역색인
일반적인 데이터베이스는 정방향 색인(Forward Index) 을 사용한다. 문서 ID를 기준으로 해당 문서의 내용을 찾는 구조이다.
정방향 색인 (Forward Index)
──────────────────────────────
Doc 1 → "Elasticsearch is fast"
Doc 2 → "Elasticsearch uses Lucene"
Doc 3 → "Lucene is a search library"
"Lucene"이라는 단어가 어떤 문서에 포함되어 있는지 알려면, 모든 문서를 처음부터 끝까지 스캔해야 한다.
역색인(Inverted Index) 은 이를 뒤집는다. 단어를 기준으로 해당 단어가 포함된 문서 목록을 저장한다.
역색인 (Inverted Index)
──────────────────────────────
"elasticsearch" → [Doc 1, Doc 2]
"fast" → [Doc 1]
"is" → [Doc 1, Doc 3]
"library" → [Doc 3]
"lucene" → [Doc 2, Doc 3]
"search" → [Doc 3]
"uses" → [Doc 2]
이제 "Lucene"을 검색하면, 역색인에서 "lucene" 항목을 한 번만 조회하여 [Doc 2, Doc 3]이라는 결과를 즉시 얻는다. 문서가 수억 건이든 수십억 건이든, 조회 속도는 거의 동일하다. 마치 책의 맨 뒤에 있는 색인(찾아보기) 과 같다. 특정 키워드가 몇 페이지에 나오는지 알고 싶을 때, 책을 처음부터 읽지 않고 색인을 펼치면 된다.
분석(Analysis) 과정
문서가 색인될 때, 텍스트는 분석기(Analyzer) 를 거쳐 토큰으로 변환된 후 역색인에 저장된다. 분석 과정은 세 단계로 나뉜다.
- Character Filter: 원본 텍스트를 전처리한다. HTML 태그 제거, 특수문자 변환 등을 수행한다.
- Tokenizer: 텍스트를 개별 토큰(token) 으로 분리한다. 기본
standard토크나이저는 공백과 구두점을 기준으로 분리한다. - Token Filter: 토큰을 변환한다. 소문자화(lowercase), 불용어(stop words) 제거, 어간 추출(stemming) 등을 수행한다.
예시로, "The Quick Brown Fox!"라는 텍스트의 분석 과정을 보자.
원본 텍스트: "The Quick Brown Fox!"
↓ Character Filter
전처리: "The Quick Brown Fox!" (변경 없음)
↓ Tokenizer
토큰 분리: ["The", "Quick", "Brown", "Fox"]
↓ Token Filter (lowercase)
최종 토큰: ["the", "quick", "brown", "fox"]
검색 시에도 동일한 분석기가 검색어에 적용된다. 사용자가 "quick fox"를 검색하면 ["quick", "fox"]로 분석된 후, 역색인에서 해당 토큰을 포함하는 문서를 찾는다. 이 때문에 색인 시와 검색 시 동일한 분석기를 사용하는 것이 중요하다.
Shard & Replication
왜 샤드가 필요한가?
하나의 인덱스에 수TB의 데이터가 쌓인다면 어떻게 될까? 단일 노드의 디스크 용량으로는 감당할 수 없고, 검색 요청도 하나의 노드에 집중되어 병목이 발생한다.
Elasticsearch는 이 문제를 샤드(Shard) 로 해결한다. 인덱스를 여러 개의 조각으로 나누어 여러 노드에 분산 저장하는 것이다.
Primary Shard와 Replica Shard
Primary Shard(프라이머리 샤드) 는 데이터가 실제로 저장되는 원본 조각이다. 인덱스 생성 시 프라이머리 샤드 수를 지정하며, 한 번 설정하면 변경할 수 없다. 이 점이 매우 중요하다. 프라이머리 샤드 수를 변경하려면 인덱스를 새로 만들어야 한다.
Replica Shard(레플리카 샤드) 는 프라이머리 샤드의 복제본이다. 두 가지 목적으로 사용된다.
- 고가용성(High Availability): 프라이머리 샤드가 있는 노드에 장애가 발생하면, 레플리카가 프라이머리로 승격되어 서비스가 중단되지 않는다.
- 검색 성능 향상: 검색 요청을 프라이머리와 레플리카가 나누어 처리하므로, 읽기 처리량(throughput)이 증가한다.
레플리카 샤드는 반드시 프라이머리 샤드와 다른 노드에 배치된다. 같은 노드에 놓으면 그 노드가 죽었을 때 원본과 복제본을 동시에 잃기 때문이다.
PUT /blog-posts
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
}
}
위 설정은 프라이머리 3개, 각 프라이머리마다 레플리카 1개씩, 총 6개의 샤드를 생성한다.
Node 1 Node 2 Node 3
────── ────── ──────
[P0] [P1] [P2]
[R1] [R2] [R0]
P는 프라이머리, R은 레플리카, 숫자는 샤드 번호이다. 각 레플리카가 자신의 프라이머리와 다른 노드에 배치된 것을 확인할 수 있다.
샤드 라우팅
새로운 문서가 들어오면, 어떤 샤드에 저장할지 결정해야 한다. 이것을 라우팅(Routing) 이라 한다. 기본 공식은 다음과 같다.
shard_number = hash(_routing) % number_of_primary_shards
기본적으로 _routing 값은 문서의 _id이다. 문서 ID의 해시값을 프라이머리 샤드 수로 나눈 나머지가 해당 문서가 저장될 샤드 번호가 된다.
이 공식이 바로 프라이머리 샤드 수를 변경할 수 없는 이유이다. 샤드 수가 바뀌면 나머지 연산의 결과가 달라져, 기존 문서를 찾을 수 없게 된다.
적정 샤드 수 결정
샤드 수를 결정할 때 고려해야 할 기본 가이드라인이 있다.
- 샤드 하나의 크기: 10GB ~ 50GB가 적정 범위이다.
- 샤드 당 문서 수: 2억 건 이하를 권장한다.
- 샤드가 너무 적으면: 단일 샤드가 지나치게 커져 장애 복구 시간이 길어진다.
- 샤드가 너무 많으면: 각 샤드마다 메모리와 CPU 오버헤드가 발생하여 클러스터 전체 성능이 저하된다. 이를 오버샤딩(Over-Sharding) 이라 하며, 흔히 저지르는 실수 중 하나이다.
예를 들어, 총 100GB의 데이터가 예상된다면 프라이머리 샤드 35개 정도가 적절하다. 레플리카 수는 가용성 요구 수준에 따라 12개를 설정한다.
정리
Elasticsearch의 핵심 구조를 다시 정리하면 다음과 같다.
- Document: JSON 형태의 데이터 기본 단위. 관계형 DB의 행에 해당
- Index: 문서들의 논리적 그룹. 관계형 DB의 테이블에 해당
- Mapping: 필드 타입과 색인 방식을 정의하는 스키마.
text와keyword의 차이를 이해하는 것이 핵심 - Inverted Index: 단어 기준으로 문서 목록을 저장하는 자료 구조. Elasticsearch가 빠른 근본적 이유
- Shard: 인덱스를 분산 저장하는 물리적 단위. Primary로 쓰기, Replica로 읽기 분산과 장애 복구를 수행
- Analyzer: 텍스트를 토큰으로 분리하여 역색인에 저장하는 파이프라인
이러한 구조들이 유기적으로 결합되어, Elasticsearch는 수십억 건의 데이터에서도 밀리초 단위의 검색 성능을 제공한다. 다음 글에서는 Elasticsearch의 검색 쿼리(Query DSL) 를 다루며, match, term, bool 등 실전에서 자주 사용하는 쿼리 패턴을 살펴본다.