MySQL FullText에서
Elasticsearch로
키워드 검색 개선기
기존 MySQL FullText 검색의 한계를 넘어, Elasticsearch를 도입해 검색 속도를 4~5배 개선하고 한국어 형태소 분석 기반의 정확한 검색 품질을 확보하기까지의 과정을 정리합니다.
(기존 ~0.8s)
배경 — 왜 바꾸게 됐나
서비스에서 키워드 자동완성 검색을 제공하고 있었는데, 기존 구현은 MySQL의 FullText 검색을 사용하고 있었습니다. 운영하면서 두 가지 문제가 뚜렷하게 드러났습니다.
MySQL FullText는 기본적으로 최소 토큰 길이(ft_min_word_len) 제한이 있어, "금", "주", "암" 같은 한 글자 키워드를 검색하면 결과가 아예 반환되지 않았습니다. 자동완성 기능 특성상 사용자가 첫 글자만 입력한 시점에도 결과를 보여줘야 하는데, 이게 원천적으로 막혀 있었습니다.
데이터가 늘어나면서 "대통령" 같이 결과 수가 1,000개를 넘는 키워드를 검색할 때 응답이 눈에 띄게 느려졌습니다. 실제로 API를 반복 호출해보니 응답 시간이 700~1,200ms 수준이었습니다.
두 문제를 한꺼번에 해결하기 위해 Elasticsearch 도입을 검토하게 됐습니다.
1단계 — Elasticsearch 연동 및 성능 비교
Elasticsearch 검색 확인
먼저 Elasticsearch에 키워드 인덱스를 구성하고, 같은 쿼리를 날려 결과를 확인했습니다.
# "대통령" 키워드로 match 검색, 상위 3개 반환 curl -s 'http://localhost:9200/keywords/_search' \ -H 'Content-Type: application/json' \ -d '{ "query": {"match": {"keyword": "대통령"}}, "size": 3 }'
결과는 총 1,049개로 MySQL과 동일하게 나왔고, 상위 결과도 "대통령 대통령실 공사", "대통령 폴란드 대통령과" 등 관련도 높은 키워드들이 잘 잡혔습니다.
실제 서비스 API와 연동 후 성능 측정
Spring Boot 서비스에서 같은 요청을 MySQL과 ES 양쪽 모두 실행하고 시간을 로깅해 비교했습니다.
2026-03-26T22:48:33 INFO KeywordService : [MySQL FullText] 1049개 결과, 1193ms
2026-03-26T22:48:33 INFO KeywordService : [Elasticsearch] 1049개 결과, 310ms
2026-03-26T22:48:33 INFO KeywordService : 속도 차이: ES가 883ms 빠름 (MySQL 1193ms vs ES 310ms)
2026-03-26T22:49:01 INFO KeywordService : [Elasticsearch] 1049개 결과, 138ms
2026-03-26T22:49:01 INFO KeywordService : 속도 차이: ES가 638ms 빠름 (MySQL 776ms vs ES 138ms)
반복 요청할수록 MySQL은 600~1,200ms를 오가는 반면, ES는 꾸준히 130~310ms 대를 유지했습니다. 전체적으로 약 0.8초 → 0.17초로, 4~5배 수준의 개선이 확인됐습니다.
대통령 키워드 검색 — 응답시간 비교 (ms)
2단계 — 결과 수가 적을 때는 ES가 느리다?
"주식", "아이폰", "부동산" 등 결과 수가 적은 키워드(10~20개)도 테스트해봤더니 예상 외의 결과가 나왔습니다.
2026-03-26T22:35:39 INFO KeywordService : [MySQL FullText] 18개 결과, 77ms
2026-03-26T22:35:39 INFO KeywordService : [Elasticsearch] 18개 결과, 85ms
2026-03-26T22:35:39 INFO KeywordService : 속도 차이: ES가 8ms 느림
MySQL FullText는 같은 프로세스 내 직접 DB 호출이라 오버헤드가 없지만, ES는 HTTP 통신 (localhost라도 네트워크 왕복) + JSON 직렬화/역직렬화 고정 비용이 30~50ms 붙습니다. 결과가 18개처럼 소량이면 MySQL이 쿼리를 금방 끝내지만, ES는 이 고정비용 때문에 오히려 느려집니다.
| 결과 수 | 유리한 쪽 | 이유 |
|---|---|---|
| 소량 (~수십 개) | MySQL | HTTP 오버헤드 없음, 직접 DB 호출 |
| 대량 (수백~수천 개) | Elasticsearch | 역인덱스 기반 검색이 압도적으로 빠름 |
3단계 — 갯수 차이가 발생하는 이유 (반도체 케이스)
"삼성" (366개 vs 355개), "반도체" (49개 vs 46개)처럼 MySQL과 ES의 결과 수가 미묘하게 달랐습니다. 왜 그런지 파고들었습니다.
원인: 한국어 토크나이저의 차이
| 엔진 | 방식 | 설명 |
|---|---|---|
| MySQL FullText | BOOLEAN MODE | 단순 문자열 포함 여부 매칭 |
| Elasticsearch | nori 형태소 분석 | 형태소 분해 후 토큰 단위로 매칭 |
직접 Elasticsearch의 analyze API로 확인해봤습니다.
curl -s 'http://localhost:9200/keywords/_analyze' \
-H 'Content-Type: application/json' \
-d '{"analyzer":"korean_analyzer","text":"반도체법"}'
# 결과:
# tokens: ["반", "도", "체법"] ← "반도체" 토큰이 없음!
| 분석 대상 | nori 토큰 결과 | "반도체" 매칭? |
|---|---|---|
| 반도체 | 반도체, 반, 도체 | ✅ 매칭 |
| 반도체법 | 반, 도, 체법 | ❌ 미매칭 |
4단계 — 해결방법: ngram 서브필드 추가
"반도체법"에서 "반도체"를 찾으려면 부분 문자열 매칭이 필요합니다. nori 분석기는 그대로 두되, ngram 서브필드를 추가해 두 가지를 함께 활용하는 방식을 선택했습니다.
쿼리 전략: nori에 높은 가중치, ngram에 낮은 가중치
{
"query": {
"bool": {
"should": [
{
"match": {
"keyword": { "query": "반도체", "boost": 3 }
}
},
{
"match": {
"keyword.ngram": { "query": "반도체", "boost": 1 }
}
}
],
"minimum_should_match": 1
}
}
}
- nori로 매칭된 결과 → boost 3 → 상위 노출 (정확 매칭 우선)
- ngram으로만 매칭된 결과 ("한반도", "도체") → boost 1 → 하위 노출
- min_score 설정 시 점수 낮은 과매칭 결과 자동 필터링 가능
인덱스 크기 영향 측정
| 인덱스 | 크기 | 비율 |
|---|---|---|
| nori만 (기존) | 106.9 MB | 1x |
| nori + ngram | 188.0 MB | 1.76x |
ngram 추가 후 결과
2026-03-26T23:48:09 INFO KeywordService : [MySQL FullText] 49개 결과, 141ms
2026-03-26T23:48:09 INFO KeywordService : [Elasticsearch] 74개 결과, 290ms
최종 결론 및 의사결정
🎯 정리
| 항목 | 결과 |
|---|---|
| 대량 키워드 검색 속도 | ES 4~5배 빠름 |
| 소량 키워드 검색 속도 | MySQL이 오히려 빠름 |
| 한국어 형태소 매칭 정확도 | ES (nori) 우위 |
| 복합어 부분 매칭 | ngram 서브필드로 보완 (과매칭 주의) |
| 인덱스 크기 증가 | nori+ngram 기준 약 76% 증가 |
권장 적용 전략
- 결과 수가 많은 대형 키워드 검색 → Elasticsearch 사용
- 결과 수가 적은 정확 매칭 검색 → MySQL FullText도 충분
- 복합어 포함 포괄적 검색이 필요하면 → nori + ngram + boost 조합
- ngram 과매칭 제어 → min_score 또는 상위 N개만 반환
- nori의 decompound_mode를 none으로 바꾸면 갯수는 맞출 수 있지만 검색 품질(복합어 분해) 저하