1장. Elasticsearch 개요 — 검색 엔진이라는 다른 세계
"MySQL 비슷한데 검색이 빠른 것"이라는 오해에서 벗어나는 것이 첫걸음입니다. 인덱스 자료구조가 다르면, 시스템의 모든 것이 달라집니다.
① Elasticsearch의 정체성 = 분산 Lucene + REST ② RDB의 LIKE 검색과 역색인 검색은 무엇이 다른가 ③ NRT(near real-time)가 "느린 것"이 아니라 "의도된 설계"인 이유 ④ Elastic Stack의 구성과 분산 시스템 핵심 원칙
1.1 Elasticsearch는 어떤 시스템인가
Elasticsearch는 Apache Lucene 기반의 분산 검색·분석 엔진(Distributed Search and Analytics Engine)입니다. 공식 문서의 정의를 풀어보면 네 가지 키워드가 나옵니다.
- Distributed — 처음부터 여러 대의 서버(클러스터)로 동작하도록 설계됐습니다. 단일 노드는 사실상 개발용입니다.
- RESTful — 모든 조작이 HTTP + JSON입니다. 전용 드라이버 없이
curl한 줄로 색인·검색·진단까지 가능합니다. - Search and analytics — 풀텍스트 검색뿐 아니라 집계(aggregation), 시계열 분석, 벡터 검색까지 한 엔진에서 처리합니다.
- Lucene-based — 실제 인덱스 자료구조(역색인, 세그먼트, 머지)는 Lucene이라는 자바 검색 라이브러리가 담당합니다.
마지막 항목이 핵심입니다. 한 줄로 요약하면 이렇게 이해해도 큰 무리가 없습니다.
Elasticsearch ≈ 분산 Lucene + REST API + 클러스터 관리. Lucene 인덱스를 여러 노드에 나눠 담고(샤딩), 복제하고, 요청을 라우팅해 주는 레이어가 Elasticsearch입니다. 그래서 3장에서 Lucene 내부를, 2·4장에서 분산 레이어를 다룹니다.
버전에 대한 기준도 정해둡시다. 이 가이드는 8.17 LTS를 baseline으로 하고, 9.x 고유 변경점은 박스로 따로 표기합니다. 9.x는 Lucene 10 기반(8.x는 Lucene 9)이며, SIMD 최적화·BBQ(Better Binary Quantization) GA 등 성능 향상 대부분이 Lucene 10에서 옵니다.
1.2 RDB의 LIKE 검색 vs 역색인 검색
RDBMS의 인덱스는 보통 B-Tree입니다. B-Tree는 WHERE name = 'Alice' 같은 정확한 값 매치에 최적화되어 있습니다. 하지만 "본문에 'fox'라는 단어가 포함된 모든 문서"를 찾으려면 어떻게 될까요? LIKE '%fox%'는 인덱스를 쓰지 못하고 모든 행을 처음부터 끝까지 읽는 풀스캔이 됩니다.
Elasticsearch(Lucene)는 역색인(Inverted Index)을 씁니다. "term → 그 term이 등장한 문서 목록(posting list)"을 미리 만들어 두는 것입니다. 책 뒤편의 '찾아보기' 페이지와 정확히 같은 원리입니다. 두 방식이 같은 검색을 처리하는 과정을 비교해 봅시다.
양쪽 모두 같은 질문을 받았습니다: "fox가 포함된 문서를 모두 찾아라." 왼쪽은 RDB 테이블, 오른쪽은 미리 만들어 둔 역색인입니다.
RDB의 LIKE '%fox%'는 첫 행부터 문자열을 훑기 시작합니다. 중간 어디에나 올 수 있는 패턴이라 B-Tree 인덱스를 탈 수 없기 때문입니다.
행이 천만 개면 천만 번의 문자열 스캔이 일어납니다. 데이터가 늘수록 검색은 선형으로 느려집니다.
Elasticsearch는 색인 시점에 이미 문서를 토큰으로 분해해 두었습니다. 검색은 정렬된 term 사전에서 fox를 바로 찾는 룩업일 뿐입니다.
posting list가 가리키는 doc1을 반환하면 끝. 원문은 한 글자도 읽지 않았습니다. 풀텍스트 검색이 곧 인덱스 룩업 — 이것이 검색 엔진의 본질입니다.
역색인이라는 자료구조에서 Elasticsearch의 성격이 그대로 따라 나옵니다.
- 풀텍스트 검색이 본업입니다. 토큰 단위 검색 = 인덱스 룩업.
- 세그먼트는 immutable(불변)입니다. B-Tree처럼 제자리 수정을 하지 않고, 변경 시 새 세그먼트를 만들어 백그라운드 머지로 정리합니다 (3장).
- 관계형 JOIN은 약점입니다. 역색인은 단일 인덱스 안에서만 효율적입니다. 9.x에서 ES|QL JOIN이 GA됐지만 RDBMS의 임의 JOIN을 대체하지는 못합니다.
| RDBMS (B-Tree) | Elasticsearch (역색인) | |
|---|---|---|
| 잘하는 것 | 정확 매치, 범위 검색, 트랜잭션, JOIN | 풀텍스트 검색, 집계, 대규모 분산 |
| 부분 문자열 검색 | LIKE '%..%' = 풀스캔 | 토큰 룩업 = ms 단위 |
| 쓰기 방식 | 제자리 갱신(in-place) | 불변 세그먼트 + 백그라운드 머지 |
| 검색 가시성 | 커밋 즉시 | 기본 1초 후 (NRT) |
| 스키마 | DDL로 엄격 | 매핑(mapping) — 유연하지만 함정 있음 (5장) |
1.3 Near-Real-Time — "1초 늦는 것"은 버그가 아니다
Elasticsearch에 문서를 색인하고 곧바로 검색하면, 그 문서가 안 보일 수 있습니다. 처음 겪으면 당황하는 현상이지만, 이는 NRT(near real-time) 검색이라는 의도된 설계입니다. 새 문서는 기본 1초 간격의 refresh_interval이 지나야 검색에 노출됩니다.
t=0: 색인 요청이 도착하면 문서는 일단 인메모리 버퍼에 쌓입니다. 유실 방지를 위해 translog라는 로그 파일에도 기록됩니다(3장).
이 순간 바로 검색하면? 결과 0건. 버퍼 속 문서는 아직 검색 가능한 세그먼트가 아니기 때문입니다.
t=1s: refresh가 일어나 버퍼 내용이 새 세그먼트로 만들어지고, 검색기가 갱신됩니다. 이제 검색됩니다.
왜 이렇게 할까요? 매 색인마다 디스크 동기화와 인덱스 갱신을 하는 대신 1초 단위로 묶어 처리하면 색인 처리량이 극적으로 높아지기 때문입니다. NRT는 성능을 위한 의도된 트레이드오프입니다.
테스트 코드나 write-then-read 시나리오에서는 ?refresh=wait_for 옵션으로 refresh를 기다리게 할 수 있습니다. 단, 운영 트래픽에서 남용하면 색인 성능이 크게 떨어집니다. Elasticsearch의 검색 경로는 기본적으로 strong consistency를 보장하지 않는다는 점을 기억하세요.
직접 확인해 봅시다. Elasticsearch는 RESTful이므로 curl만 있으면 됩니다.
# 문서 색인 (인덱스가 없으면 자동 생성)
POST /products/_doc
{
"name": "quick brown fox 인형",
"price": 15000
}
# 풀텍스트 검색 — 1초 후에는 검색됨
GET /products/_search
{
"query": { "match": { "name": "fox" } }
}
# 테스트용: refresh를 기다렸다가 응답 받기
POST /products/_doc?refresh=wait_for
{ "name": "바로 검색돼야 하는 문서" }
1.4 무엇에 쓰는 물건인가 — 4대 사용 사례
🔎 Full-Text Search
상품 검색, 문서 검색, 사이트 내 검색. BM25 스코어링, 한국어 형태소 분석(nori), 자동완성(suggest)이 주무기입니다.
📈 Observability (로그·메트릭·트레이스)
로그 분석의 사실상 표준. Beats/Elastic Agent로 수집해 시간 기반 인덱스(logs-app-2026.07.11)에 색인하고 Kibana로 시각화합니다.
🛡️ Security Analytics (SIEM)
보안 이벤트를 색인하고 룰 기반 탐지(EQL), 행위 분석, 위협 헌팅을 수행하는 Elastic Security의 백엔드입니다.
🧠 Vector Search (시맨틱 검색)
8.x부터 dense_vector + kNN 검색 지원. 9.x에서 BBQ가 GA되어 메모리 32배 절감 + 8~30배 처리량. RAG·추천에 활용됩니다.
1.5 Elastic Stack — 혼자 다니지 않는다
Elasticsearch는 보통 수집기·시각화 도구와 한 세트로 움직입니다. 데이터가 흐르는 경로를 봅시다.
애플리케이션이 로그를 남기면, 서버에 설치된 Elastic Agent(또는 Filebeat 같은 Beats)가 이를 실시간으로 수집합니다.
복잡한 파싱·가공이 필요하면 Logstash를 중간에 둡니다. grok 파싱, 외부 데이터 enrichment 같은 무거운 ETL 담당입니다. 가벼운 전처리는 Elasticsearch의 ingest pipeline으로도 충분합니다.
데이터는 Elasticsearch의 시간 기반 인덱스에 색인됩니다. 여기가 저장·검색·집계의 중심입니다.
마지막으로 Kibana가 이 데이터를 대시보드로 시각화합니다. Kibana는 시각화뿐 아니라 클러스터 관리 UI와 Dev Tools 콘솔도 제공합니다.
| 컴포넌트 | 역할 |
|---|---|
| Elasticsearch | 데이터 저장, 검색, 집계. 모든 것의 중심 |
| Kibana | 시각화, 대시보드, 클러스터 관리, Dev Tools |
| Logstash | 무거운 ETL — grok 파싱, enrichment, 다중 출력 |
| Beats | 경량 수집기 (Filebeat, Metricbeat 등) |
| Elastic Agent + Fleet | Beats를 하나로 묶은 차세대 수집기 + 중앙 정책 관리. 신규 도입 표준 |
1.6 분산 시스템 핵심 원칙 — 미리보기
Elasticsearch가 Lucene 위에 얹는 분산 레이어의 핵심 개념 세 가지만 미리 잡아둡시다. 자세한 내용은 2장과 4장에서 다룹니다.
- 샤드(shard): 하나의 인덱스는 여러 샤드로 분할됩니다. 각 샤드는 독립적인 Lucene 인덱스이며 어느 노드에든 배치될 수 있습니다. 인덱스는 논리, 샤드는 물리입니다.
- 레플리카(replica): 각 primary 샤드의 복제본. 노드가 죽으면 replica가 primary로 승격(고가용성)되고, 평소에는 검색 요청을 분담(읽기 처리량)합니다. primary와 replica는 절대 같은 노드에 배치되지 않습니다.
- 일관성은 eventual: 검색 요청은 primary/replica 어느 쪽으로든 라우팅되며, refresh 지연과 복제 지연이 있어 방금 쓴 문서가 즉시 안 보일 수 있습니다.
# 클러스터가 살아있는지, 샤드 상태는 어떤지 — 운영자의 첫 명령
GET /_cluster/health
{
"cluster_name": "prod-search",
"status": "green", ← green/yellow/red
"number_of_nodes": 6,
"active_primary_shards": 120,
"active_shards": 240 ← primary + replica
}
① primary 샤드 수는 생성 후 바꾸기 어렵습니다 (4장). ② 샤드가 너무 많으면 클러스터가 통째로 흔들립니다(oversharding). ③ 대량 색인은 반드시 bulk API로. ④ 운영 환경은 마스터 전용 노드 3대 분리(2장). ⑤ 업그레이드는 N → N+1 단위 — 8.x → 9.x는 8.19 경유 필수.
✍️ 이해도 체크
LIKE '%fox%'가 느리고 Elasticsearch의 같은 검색이 빠른 근본적인 이유는?'%..%' 패턴에서 인덱스를 쓰지 못해 풀스캔이 되지만, 역색인은 "term → 문서 목록"을 미리 만들어 두므로 검색이 정렬된 사전에서의 룩업 + posting list 조회로 끝납니다.?refresh=wait_for를 사용하세요.이 장의 원문 문서: chapters/ch01_elasticsearch_overview.md — 9.x 변경점 전체 목록, eventual consistency의 상세 조건 등이 담겨 있습니다.