저희는 어떠한 물건을 검색할 때 단어의 순서를 정확하게 기억하고 검색하지는 않습니다.
예를 들면 맥북을 검색하고 싶을 때 맥북 14 에어, 맥북 에어 14, 맥북 에어 14 M4 등 다양한 형태로 검색을 진행합니다.
이를 MySQL로 진행할 시 SELECT * FROM products WHERE name = "%맥북 14 에어%";라고 검색을 한다면, 정확히 일치하는 단어가 없을 시 실패할 것입니다.
하지만 Elasticsearch의 검색은 역인덱스를 활용하여 원하는 데이터를 추출합니다.
역인덱스
- 필드 값을 단어마다 쪼개 찾기 쉽게 미리 정리해놓은 목록
POST /products/_create/1
{
"name": "Apple 2025 맥북 에어 13 M4 10코어"
}
POST /products/_create/2
{
"name": "Apple 2024 에어팟 4세대"
}
POST /products/_create/3
{
"name": "Apple 2024 아이패드 mini A17 Pro"
}
위와 같은 3개의 도큐먼트를 Elasticsearch에 저장한다고 가정하면, 내부적으로 아래와 같이 데이터를 저장합니다.
- 단어 단위로 자르기
"name": "Apple 2025 맥북 에어 13 M4 10코어"
→ [Apple, 2025, 맥북, 에어, 13, M4, 10코어]
"name": "Apple 2024 에어팟 4세대"
→ [Apple, 2024, 에어팟, 4세대]
"name": "Apple 2024 아이패드 mini A17 Pro"
→ [Apple, 2024, 아이패드, mini, A17, Pro]
- 역인덱스로 저장
- products의 name 필드에 대한 역인덱스
| 토큰 | 도큐먼트 |
|---|---|
| Apple | [1, 2, 3] |
| 2025 | [1] |
| 맥북 | [1] |
| 에어 | [1] |
| 13 | [1] |
| M4 | [1] |
| 10코어 | [1] |
| 2024 | [2, 3] |
| 에어팟 | [2] |
| 4세대 | [2] |
| 아이패드 | [3] |
| mini | [3] |
| A17 | [3] |
| Pro | [3] |
Apple 2024 아이패드라고 검색하면 역인덱스를 활용해 일치하는 단어가 많은 도큐먼트를 우선적으로 조회합니다. Score가 높은 순입니다.
- id가 1인 도큐먼트 : 단어 1개 일치
- id가 2인 도큐먼트 : 단어 2개 일치
- id가 3인 도큐먼트 : 단어 3개 일치
score 계산 로직은 아래와 같으며, 추후에 상세하게 알아두면 좋을 것 같습니다.
Term Frequency (TF)
문서 내에서 검색어가 얼마나 자주 등장하냐
→ 많이 등장할수록 점수↑
Inverse Document Frequency (IDF)
검색어가 전체 문서 중 얼마나 희귀하냐
→ 희귀할수록 점수↑ (흔한 단어일수록 점수↓)
Field Length Normalization
문서(필드)가 짧을수록 점수↑
→ 검색어가 짧은 문서에서 등장하면 더 관련성 높다고 판단
이러한 동작 구조는 데이터 타입이 Text일 경우에만 적용됩니다.
역인덱스 조회와 MySQL 인덱스 조회의 Trade-off 관계
쓰기 증폭
B-Tree는 문서 하나를 INSERT할 때 인덱스가 걸린 컬럼 개수만큼만 갱신합니다.
인덱스 3개면 트리 3개에 각각 1건씩입니다.
역인덱스는 문서 하나에 토큰 개수만큼 posting list를 건드립니다. 500단어짜리 문서에서 중복 제거 후 200개의 고유 term이 나오면, 200개의 posting list에 doc id를 추가해야 합니다.
MySQL: INSERT 1건 → 인덱스 3개 갱신 ES: INSERT 1건 → term 200개 × (posting list + term frequency + position) 갱신여기에 색인 시점 분석(형태소 분석, nori 같은 건 특히 무겁습니다) 비용까지 붙습니다. 읽기를 빠르게 하려고 쓰기 비용을 앞당겨 지불하는 구조입니다.
그래서 ES는 이걸 배치로 상쇄합니다 →
메모리 버퍼에 모았다가 1초에 한 번 세그먼트로 떨굽니다. 대신 그 1초가 near real-time이라는 또 다른 대가가 됩니다.UPDATE/DELETE 비용
필드 하나만 바꿔도 문서 전체가 다시 색인됩니다. 200개 term을 전부 다시 씁니다. MySQL이 해당 컬럼의 인덱스 페이지만 건드리는 것과 대비됩니다.
term이란?
{ "name": "카카오프렌즈 라이언 쿠션", "description": "부드러운 극세사 소재로 제작된 대형 쿠션입니다. 거실 소파나 ... (200자)", "stock": 100 } 이 문서 1건을 분석기에 통과시키면: [부드럽다, 극세사, 소재, 제작, 대형, 쿠션, 거실, 소파, 침대, 사용, 커버, 분리, 세탁, 가능, ...] 으로 분리되서 저장하는 것을 말합니다.재색인 시 실제로 일어나는 일
새 세그먼트 _1 을 만들면서 ├─ "부드럽다" → posting list에 [새 doc id] 추가 ├─ "극세사" → posting list에 [새 doc id] 추가 ├─ "소재" → posting list에 [새 doc id] 추가 ├─ ... (이 문서의 term 200개) └─ 각 term마다 term frequency, position 정보도 함께 기록 기존 세그먼트 _0 은 └─ .del 파일에 "doc 0번 죽음" 비트만 세움 ← 여기가 전부스키마 변경이 힘듦
B-Tree 인덱스는
ALTER TABLE ... ADD INDEX로 나중에 추가·삭제가 자유롭습니다. 역인덱스는 분석기가 색인 시점에 이미 적용되어 굳어버립니다.동의어 사전을 고치거나, tokenizer를 standard에서 nori로 바꾸거나, 필드 타입을
text에서keyword로 바꾸는 순간 → 전체 reindex입니다. 수억 건이면 몇 시간짜리 작업이고, 무중단으로 하려면 alias 스위칭 전략이 필요합니다.
MySQL 관계형 데이터베이스에서의 한계점을 역인덱스를 통해 해결한 것을 알 수 있었고, 이를 통해 각각의 장단점을 잘 알고 적용해야 될 것으로 생각되었습니다.
댓글
GitHub 계정으로 로그인하시면 댓글과 질문을 남기실 수 있습니다. 남겨 주신 글은 이 저장소의 Discussions에 쌓입니다.