Elasticsearch 핵심 정리 — 검색은 어떻게 빨라지는가
RDB의 LIKE '%키워드%'로는 한계가 명확하다. 풀스캔, 형태소 무시, 오타 취약, 연관도 없음. Elasticsearch가 이 문제를 어떻게 푸는지 — 클러스터 구조부터 역인덱스, 애널라이저, 한글 검색, 쿼리 DSL, 실전 프로젝트까지 핵심만 정리한다.
Search Engine · Elasticsearch. RDB의 LIKE '%키워드%'로는 한계가 명확하다. 풀스캔, 형태소 무시, 오타 취약, 연관도 없음. Elasticsearch가 이 문제를 어떻게 푸는지 — 클러스터 구조부터 역인덱스, 애널라이저, 한글 검색, 쿼리 DSL, 실전 프로젝트까지 핵심만 정리한다.
기준 — Elasticsearch 7.x · Spring Data Elasticsearch · 읽는 데 약 18분.
목차
- Elasticsearch란 — 어디에 쓰나
- 클러스터 구조 — 노드 · 샤드 · 레플리카
- 데이터 구조 — 인덱스 / 도큐먼트 / 필드
- 역인덱스 — 검색이 빠른 진짜 이유
- 애널라이저 — 텍스트가 토큰이 되는 과정
- 한글 검색 — Nori Analyzer
- 매핑 — 데이터 타입과 text / keyword
- 쿼리 DSL — 자주 쓰는 검색들
- 실무 부가 기능 — 하이라이팅 · 정렬 · 자동완성
- 프로젝트 — 상품 검색 기능 구현 흐름
- 운영 — Spring Boot 연동과 Elastic Cloud
01 · Elasticsearch란 — 어디에 쓰나
Elasticsearch는 Apache Lucene 위에 만들어진 분산 검색·분석 엔진이다. JSON 도큐먼트를 저장하고, REST API로 검색한다. 단순 저장소가 아니라 "대량 텍스트를 빠르게 검색하고 집계하는 것"에 특화돼 있다.
왜 RDB로는 부족한가
- 풀스캔 —
LIKE '%키워드%'는 인덱스를 못 타고 모든 행을 훑는다. 데이터가 늘수록 느려진다. - 형태소 무시 — "가을이"로 색인된 값을 "가을"로 못 찾는다. 단어를 쪼개는 개념이 없다.
- 연관도 없음 — "얼마나 잘 맞는지" 점수가 없다. 검색 결과를 관련도순으로 줄 수 없다.
- 확장 한계 — 대량 텍스트 검색을 수평 확장으로 분산하기 어렵다.
주요 활용 사례
- 전문 검색 — 상품 검색, 문서·게시글 검색, 자동완성. 쿠팡·깃허브 코드 검색 같은 곳.
- 로그·메트릭 분석 — 서버 로그를 쌓고 실시간으로 집계·모니터링. 가장 흔한 쓰임새.
- ELK 스택 — Elasticsearch(저장·검색) + Logstash / Beats(수집) + Kibana(시각화). 로그 분석의 사실상 표준 조합.
실습·관리 도구로는 Kibana의 Dev Tools를 쓴다. REST API를 GUI에서 바로 날려보고 결과를 확인할 수 있어 학습·디버깅이 편하다.
02 · 클러스터 구조 — 노드 · 샤드 · 레플리카
Elasticsearch가 "분산" 엔진인 이유가 여기 있다. 데이터를 조각내 여러 서버에 흩어 두고, 검색을 병렬로 처리한다. 네 가지 용어만 잡으면 된다.
- 클러스터(Cluster) — 함께 동작하는 노드들의 묶음. 하나의 검색 시스템 전체.
- 노드(Node) — Elasticsearch 인스턴스 하나. 보통 서버 한 대.
- 샤드(Shard) — 인덱스를 쪼갠 조각. 실제로는 Lucene 인덱스 하나다. 데이터를 여러 노드에 나눠 담아 수평 확장과 병렬 검색을 가능하게 한다.
- 레플리카(Replica) — 샤드의 복제본. 원본(프라이머리) 장애에 대비하고, 읽기 부하를 분산한다. 프라이머리와 다른 노드에 둬야 의미가 있다.
핵심 — 샤드 = 확장과 병렬 처리, 레플리카 = 가용성과 읽기 분산. 검색 요청이 오면 여러 샤드에서 동시에 찾고 결과를 합친다. 이게 대용량에서도 빠른 두 번째 이유다(첫 번째는 다음 섹션의 역인덱스).
03 · 데이터 구조 — 인덱스 / 도큐먼트 / 필드
Elasticsearch는 JSON 도큐먼트를 저장하고 검색한다. RDB와 1:1로 대응시키면 구조가 빨리 잡힌다.
- 인덱스(Index) — 같은 성격의 도큐먼트를 모은 단위. RDB의 테이블에 해당한다.
- 도큐먼트(Document) — 저장되는 데이터 한 건. RDB의 행(row), JSON 객체 하나다.
- 필드(Field) — 도큐먼트 안의 key-value. RDB의 컬럼에 해당한다.
- 매핑(Mapping) — 각 필드의 데이터 타입과 분석 방식을 정의한 스키마.
도큐먼트 CRUD는 전부 REST API다. Kibana Dev Tools에서 바로 실행한다.
PUT products/_doc/1
{
"name": "갤럭시 버즈",
"price": 189000,
"tags": ["이어폰", "무선"]
}
// GET products/_doc/1 → 조회
// POST products/_update/1 → 수정
// DELETE products/_doc/1 → 삭제주의 — 매핑은 한 번 정의되면 기존 필드 타입을 변경할 수 없다. 타입을 바꾸려면 새 인덱스를 만들고
_reindex로 옮겨야 한다. 운영 인덱스는 처음 설계가 중요하다.
04 · 역인덱스 — 검색이 빠른 진짜 이유
RDB는 WHERE content LIKE '%검색%'에서 본문을 처음부터 끝까지 훑는다. Elasticsearch는 반대다. 저장 시점에 텍스트를 단어(term)로 쪼개, "이 단어가 어느 도큐먼트에 있는가"를 거꾸로 색인해 둔다. 이게 역인덱스(Inverted Index)다.
- RDB — 검색마다 본문 스캔. 데이터 양에 비례해 느려진다.
- Elasticsearch — 색인 때 미리 단어 분해. 검색은 단어 → 도큐먼트 lookup이라 빠르다.
- 트레이드오프 — 색인 시점에 분석 비용·저장 공간이 든다. "쓰기를 미리 치르고 읽기를 싸게 만든다".
단어 순서가 바뀌어도 검색된다
역인덱스는 단어별로 색인하므로 어순을 따지지 않는다. "맑은 가을"로 검색하든 "가을 맑은"으로 검색하든, 두 단어가 든 도큐먼트를 똑같이 찾아낸다. RDB의 LIKE로는 불가능한 동작이다.
연관도 점수 (_score)
match 검색 결과는 그냥 일치/불일치가 아니라 얼마나 잘 맞는지를 점수(_score)로 매겨 정렬한다. 기본 알고리즘은 BM25다.
- TF — 해당 단어가 도큐먼트에 많이 나올수록 점수↑.
- IDF — 그 단어가 전체 도큐먼트에서 희귀할수록 점수↑ (흔한 단어는 변별력이 낮으므로).
- 결과 — "검색어와 가장 관련 높은 순"으로 정렬돼 나온다. 이 점수를 의도적으로 조정하는 게 뒤의
should·boost다.
05 · 애널라이저 — 텍스트가 토큰이 되는 과정
애널라이저(Analyzer)는 원본 텍스트를 역인덱스에 들어갈 토큰으로 가공하는 파이프라인이다. 세 단계를 순서대로 거친다.
- Character Filter — 토큰화 전에 원문을 손본다.
html_strip으로<b>태그 제거. - Tokenizer — 텍스트를 토큰으로 자르는 핵심 단계. 정확히 하나만 지정.
- Token Filter — 잘린 토큰을 후처리. 여러 개를 순서대로 적용.
기본 제공 애널라이저
| 애널라이저 | 동작 |
|---|---|
| standard | 기본값. 공백·문장부호로 자르고 소문자화. 대부분의 영문에 무난 |
| simple | 문자가 아닌 것 모두 기준으로 자르고 소문자화 (숫자도 분리) |
| whitespace | 공백으로만 자른다. 소문자화·기호 제거 안 함 |
| stop | standard + 불용어(a, the, or…) 제거 |
| keyword | 자르지 않고 통째로 하나의 토큰. 분석 없이 원문 그대로 |
자주 쓰는 토큰 필터
| 필터 | 역할 | 예시 |
|---|---|---|
| lowercase | 대소문자 통일. "Apple"과 "apple"이 같은 검색어로 | Apple → apple |
| stop | 불용어 제거. 색인 크기↓, 노이즈↓ | a, an, the 제거 |
| stemmer | 어형을 어간으로 환원. 시제·복수형 흡수 | running → run |
| synonym | 동의어를 같은 토큰으로 묶음 | laptop ↔ notebook |
중요 — 색인 때 쓴 애널라이저와 검색 때 쓰는 애널라이저가 같아야 결과가 맞는다. 둘 다
lowercase를 거쳐야 "Apple" 검색이 "apple" 색인어와 매칭된다.
토큰이 실제로 어떻게 쪼개지는지는 _analyze API로 확인한다. 매핑 잡기 전에 반드시 찍어보는 습관이 좋다.
POST _analyze
{
"analyzer": "standard",
"text": "The Quick Brown Foxes"
}
// → [the, quick, brown, foxes] (소문자화 + 공백 분리)06 · 한글 검색 — Nori Analyzer
standard 애널라이저는 공백 기준으로만 자른다. 영어는 단어가 공백으로 나뉘니 잘 되지만, 한글은 조사가 단어에 붙어 다녀서 제대로 검색되지 않는다. "가을이"를 통째로 색인하면 "가을"로 검색해도 안 잡힌다.
- Nori — Elasticsearch 공식 한글 형태소 분석 플러그인. 명사·동사 어간을 뽑고 조사·어미를 분리.
- 설치 —
analysis-nori플러그인을 별도 설치. (Elastic Cloud·AWS OpenSearch는 기본 제공) - 한·영 혼용 — 한 필드에 한글·영어가 섞이면, 멀티 필드나 적절한 애널라이저 조합으로 양쪽 다 검색되게 설계.
07 · 매핑 — 데이터 타입과 text / keyword
매핑은 필드별 데이터 타입을 정의한다. 타입에 따라 분석 여부·검색 방식·정렬 가능 여부가 갈린다.
주요 데이터 타입
| 타입 | 용도 |
|---|---|
| text | 전문 검색용 문자열. 애널라이저로 분석됨 (본문·상품명) |
| keyword | 정확 일치·정렬·집계용 문자열. 분석 안 함 (상태·카테고리·이메일) |
| long / integer | 정수 |
| double / float | 실수 |
| date | 날짜·시간. range 쿼리·정렬에 사용 |
| boolean | true / false |
| object | 중첩 JSON 객체 (내부적으로 평탄화) |
| nested | 배열 안 객체를 독립적으로 검색해야 할 때 |
text와 keyword는 다르다
가장 자주 하는 실수가 이 둘의 혼동이다. 같은 문자열도 타입에 따라 토큰이 완전히 달라진다.
- text — 분석한다. 일부 단어로 찾는 전문 검색용.
- keyword — 분석 안 한다. 정확 일치·정렬·집계·필터용.
- Multi Field — 한 필드에 두 타입 동시.
name은 text로 검색,name.keyword로 정렬·집계. - 매핑 특징 —
null허용, 별도 array 타입 없이 모든 필드가 배열을 받는다.
PUT products/_mapping — multi field
{
"properties": {
"name": {
"type": "text",
"analyzer": "nori",
"fields": {
"keyword": { "type": "keyword" } // name.keyword
}
}
}
}08 · 쿼리 DSL — 자주 쓰는 검색들
실무 검색의 90%는 아래 조합으로 끝난다. 먼저 "분석 여부"로 두 갈래를 나누는 게 출발점이다.
- match — 검색어를 애널라이저로 분석한 뒤 매칭. 전문 검색 기본. text 필드용.
- term / terms — 검색어를 분석 없이 정확히 일치. keyword·숫자·상태값용.
흔한 함정 — text 필드에
term으로 "Samsung Galaxy"를 넣으면 결과가 안 나온다. text는 소문자 토큰(samsung,galaxy)으로 색인됐는데 term은 원문 그대로 비교하기 때문. text엔 match, keyword엔 term이 기본 원칙.
bool — 조건 조합의 핵심
GET products/_search — bool 조합
{ "query": { "bool": {
"must": [{ "match": { "name": "무선 이어폰" } }],
"filter": [{ "range": { "price": { "lte": 200000 } } }],
"must_not":[{ "term": { "status": "품절" } }],
"should": [{ "term": { "brand": "삼성" } }]
} } }
// 무선 이어폰 + 20만원 이하 + 품절 제외 + 삼성이면 상위로나머지 자주 쓰는 쿼리
| 쿼리 | 쓰는 상황 |
|---|---|
| range | 숫자·날짜 범위. gte / lte / gt / lt로 가격·기간 필터링 |
| fuzziness | 오타 허용. 편집 거리 기준으로 "samsng"도 "samsung"으로 매칭 |
| multi_match | 여러 필드를 한 번에. 제목·본문에서 동시에 키워드 검색 |
특정 조건을 상위로 — 점수 튜닝
"평점 높고 좋아요 많은 글을 위로" 같은 요구는 점수를 직접 조정해 푼다.
- should + boost — 특정 조건에 가중치를 줘서 매칭되면
_score를 올린다. 인기 브랜드·카테고리를 띄울 때. - function_score — 평점·좋아요 같은 숫자 필드를 점수에 곱한다. "관련도 × 인기도"로 정렬할 때.
09 · 실무 부가 기능 — 하이라이팅 · 정렬 · 자동완성
- highlight — 매칭된 부분을
<em>태그로 감싸 반환. 결과에서 키워드 강조 표시. - pagination —
from/size로 페이지 분할. 깊은 페이지는search_after가 안전. - sorting —
sort로 정렬. text는 정렬 불가 →.keyword나 숫자 필드 기준. - 자동완성 — 일부만 입력해도 추천.
edge_ngram으로 접두어를 미리 색인하거나search_as_you_type타입 사용.
자동완성 — edge_ngram의 원리
입력 도중에도 결과가 나오게 하려면, 단어를 앞에서부터 잘라 여러 접두어로 미리 색인한다. "갤럭시"를 색인하면 갤, 갤럭, 갤럭시가 토큰으로 들어가, "갤"만 쳐도 매칭된다.
10 · 프로젝트 — 상품 검색 기능 구현 흐름
지금까지의 개념을 쿠팡 같은 상품 검색에 합치면 이렇게 된다. 핵심은 기능 하나하나가 앞서 본 쿼리·매핑의 조립이라는 점이다.
구현 순서
- ① 요구사항 — 상품명 검색 + 필터(가격·카테고리) + 자동완성 + 결과 하이라이팅·정렬.
- ② 인프라 설계 — 원본은 MySQL, 검색은 Elasticsearch, 그 사이를 Spring Boot가 연결.
- ③ 매핑 정의 — 상품명은 nori text + keyword 멀티필드, 가격은 long, 카테고리·상태는 keyword, 자동완성용 필드는 edge_ngram.
- ④ 분석 검증 — 색인 전에
_analyze로 한글이 의도대로 쪼개지는지 확인. - ⑤ 검색 쿼리 조립 — 아래처럼 한 요청에 기능을 합친다.
GET products/_search — 상품 검색 종합
{
"query": { "bool": {
"must": [{ "multi_match": {
"query": "무선 이어폰",
"fields": ["name", "desc"] } }],
"filter": [{ "term": { "category": "음향기기" } },
{ "range": { "price": { "lte": 300000 } } }],
"must_not":[{ "term": { "status": "품절" } }]
} },
"highlight": { "fields": { "name": {} } },
"sort": [{ "_score": "desc" }, { "price": "asc" }],
"from": 0, "size": 20
}적용 전략
- 점수 vs 필터 — 검색어 관련도는
must(점수 O), 가격·카테고리 같은 단순 조건은filter(점수 X, 캐싱)로 분리. - 정렬 우선순위 — 1차
_score, 동점이면 가격순. 인기도까지 넣으려면 function_score. - 자동완성은 별도 인덱스/필드 — 검색 본 쿼리와 분리해 edge_ngram 필드로 가볍게 처리.
11 · 운영 — Spring Boot 연동과 Elastic Cloud
Spring Data Elasticsearch를 쓰면 인덱스를 도큐먼트 클래스로 매핑하고 리포지토리로 CRUD를 한다. JPA를 써봤다면 구조가 익숙하다.
Product.java
@Document(indexName = "products")
public class Product {
@Id private String id;
@Field(type = FieldType.Text, analyzer = "nori")
private String name;
@Field(type = FieldType.Long)
private Long price;
}데이터 동기화 — 가장 중요한 부분
Elasticsearch는 원본 저장소가 아니라 읽기 전용 검색 인덱스로 두는 게 정석이다. 원본은 MySQL에 두고, 데이터가 바뀔 때 ES에도 반영한다.
직접 구축 vs 매니지드
- Elastic Cloud — Elastic 본사 매니지드 서비스. 최신 기능·Nori 플러그인을 바로 사용.
- AWS OpenSearch — Elasticsearch에서 갈라진 AWS 포크. AWS 생태계 통합에 유리, Nori도 제공.
- 현업 — 직접 클러스터를 운영하기보다 매니지드를 쓰는 경우가 많다. 노드 관리·스케일링 부담을 줄이려고.
비용 주의 — Elastic Cloud는 클러스터가 떠 있는 동안 과금된다. 실습·테스트 후에는 디플로이먼트를 반드시 삭제(리소스 정리)해야 불필요한 요금이 안 나간다.
— 검색은 결국 "어떻게 쪼개서 색인하느냐"의 문제다.
클러스터가 데이터를 분산하고, 역인덱스가 단어로 찾고, 애널라이저가 쪼개는 규칙을 정한다. text/keyword 구분과 분석기 선택부터 정확히 잡으면 나머지 쿼리는 조립일 뿐이다.
주간 기술 뉴스레터
Backend · AI · Java 핵심 내용을 매주 이메일로 보내드립니다.