~/tech-blog/posts/es-inverted-index — zsh

단어 검색은 어떻게 동작할까 — 역인덱스

저희는 어떠한 물건을 검색할 때 단어의 순서를 정확하게 기억하고 검색하지는 않습니다.

예를 들면 맥북을 검색하고 싶을 때 맥북 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에 저장한다고 가정하면, 내부적으로 아래와 같이 데이터를 저장합니다.

  1. 단어 단위로 자르기
"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]
  1. 역인덱스로 저장
  • 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 계산 로직은 아래와 같으며, 추후에 상세하게 알아두면 좋을 것 같습니다.

  1. Term Frequency (TF)

    문서 내에서 검색어가 얼마나 자주 등장하냐

    → 많이 등장할수록 점수↑

  2. Inverse Document Frequency (IDF)

    검색어가 전체 문서 중 얼마나 희귀하냐

    → 희귀할수록 점수↑ (흔한 단어일수록 점수↓)

  3. Field Length Normalization

    문서(필드)가 짧을수록 점수↑

    → 검색어가 짧은 문서에서 등장하면 더 관련성 높다고 판단

이러한 동작 구조는 데이터 타입이 Text일 경우에만 적용됩니다.

역인덱스 조회와 MySQL 인덱스 조회의 Trade-off 관계

  1. 쓰기 증폭

    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이라는 또 다른 대가가 됩니다.

  2. 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번 죽음" 비트만 세움  ← 여기가 전부
    
  3. 스키마 변경이 힘듦

    B-Tree 인덱스는 ALTER TABLE ... ADD INDEX로 나중에 추가·삭제가 자유롭습니다. 역인덱스는 분석기가 색인 시점에 이미 적용되어 굳어버립니다.

    동의어 사전을 고치거나, tokenizer를 standard에서 nori로 바꾸거나, 필드 타입을 text에서 keyword로 바꾸는 순간 → 전체 reindex입니다. 수억 건이면 몇 시간짜리 작업이고, 무중단으로 하려면 alias 스위칭 전략이 필요합니다.

MySQL 관계형 데이터베이스에서의 한계점을 역인덱스를 통해 해결한 것을 알 수 있었고, 이를 통해 각각의 장단점을 잘 알고 적용해야 될 것으로 생각되었습니다.

댓글

GitHub 계정으로 로그인하시면 댓글과 질문을 남기실 수 있습니다. 남겨 주신 글은 이 저장소의 Discussions에 쌓입니다.

● main 214 posts UTF-8 LF © 2026 코징 RSS · GitHub