이 글에서 얻는 것

  • **역인덱스(Inverted Index)**의 내부 구조를 이해하고, RDBMS 인덱스와의 차이를 설명할 수 있습니다.
  • 분석기(Analyzer) 파이프라인을 이해하고, 한국어(Nori) 분석기를 설정할 수 있습니다.
  • 매핑(Mapping) 설계 원칙을 익혀 text/keyword/nested 타입을 적절히 선택할 수 있습니다.
  • 쿼리 유형(match/term/bool/range)과 BM25 스코어링 원리를 이해할 수 있습니다.
  • **집계(Aggregation)**로 실시간 분석 쿼리를 작성할 수 있습니다.
  • 클러스터/Shard 설계 원칙과 운영 체크리스트를 확보합니다.

0) Elasticsearch는 “검색 특화 데이터베이스"다

왜 Elasticsearch인가?

RDBMS의 LIKE 검색:

-- ❌ 느린 검색
SELECT * FROM products
WHERE name LIKE '%맥북%'
   OR description LIKE '%맥북%';

-- 문제:
-- - Full Table Scan (인덱스 사용 불가)
-- - 대용량 데이터에서 매우 느림
-- - 형태소 분석 불가 ("맥북프로" 검색 시 "맥북 프로" 못 찾음)

Elasticsearch:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "맥북",
      "fields": ["name", "description"]
    }
  }
}
// 장점:
// - 역인덱스로 빠른 검색 (ms 단위)
// - 형태소 분석으로 유연한 검색
// - 관련도 점수 (relevance score)

사용 사례

유형예시ES 장점
전문 검색상품 검색, 문서 검색역인덱스 + 형태소 분석
로그 분석ELK Stack시계열 인덱싱 + 빠른 집계
실시간 분석대시보드, 모니터링Near Real-Time 집계
자동완성검색어 추천Edge N-gram + Completion Suggester
지리 검색근처 매장 찾기Geo-point/Geo-shape 타입

1) 핵심 개념

1-1) 인덱스 vs 도큐먼트

Elasticsearch      RDBMS             차이점
─────────────────  ──────────────    ──────────────────
Index              Database/Table    스키마리스(동적 매핑)
Document           Row               JSON 구조
Field              Column            중첩/배열 자유
Mapping            Schema            런타임 변경 불가(재인덱싱)
Shard              Partition          수평 분산 저장 단위

예시:

// Index: products
{
  "_index": "products",
  "_id": "1",
  "_source": {
    "name": "맥북 프로 M3",
    "price": 2500000,
    "category": "노트북",
    "tags": ["Apple", "고성능"],
    "specs": {
      "cpu": "M3 Pro",
      "ram_gb": 18
    },
    "created_at": "2025-12-16"
  }
}

1-2) 역인덱스 (Inverted Index) 심층

역인덱스는 “단어(Term) → 문서(DocID) 목록” 매핑입니다. 일반 DB 인덱스(B-tree)는 “문서 → 값"이지만, 역인덱스는 반대입니다.

내부 구조:

분석 전 원문:
  Doc 1: "맥북 프로 M3 출시"
  Doc 2: "맥북 에어 경량 노트북"
  Doc 3: "아이패드 프로 M3"

분석 후 Term Dictionary + Posting List:
┌──────────┬──────────────────────────────────────┐
│ Term     │ Posting List                         │
├──────────┼──────────────────────────────────────┤
│ 맥북     │ [Doc1(pos:0, freq:1), Doc2(pos:0, freq:1)] │
│ 프로     │ [Doc1(pos:1, freq:1), Doc3(pos:1, freq:1)] │
│ m3       │ [Doc1(pos:2, freq:1), Doc3(pos:2, freq:1)] │
│ 출시     │ [Doc1(pos:3, freq:1)]                │
│ 에어     │ [Doc2(pos:1, freq:1)]                │
│ 경량     │ [Doc2(pos:2, freq:1)]                │
│ 노트북   │ [Doc2(pos:3, freq:1)]                │
│ 아이패드 │ [Doc3(pos:0, freq:1)]                │
└──────────┴──────────────────────────────────────┘

Posting List 저장 정보:

  • DocID: 문서 식별자
  • Term Frequency (TF): 해당 문서에서 Term 출현 횟수
  • Position: Term 위치 (Phrase 검색용)
  • Offset: 원문 내 시작/끝 위치 (Highlight용)

검색 동작 예시: “맥북 프로” 검색

1. "맥북 프로" → 분석 → [맥북, 프로]
2. 맥북 → [Doc1, Doc2]
3. 프로 → [Doc1, Doc3]
4. Bool Query에 따라:
   - must(AND): Doc1 (교집합)
   - should(OR): Doc1, Doc2, Doc3 (합집합, Doc1 점수 최고)

2) 분석기(Analyzer) 파이프라인

분석기는 텍스트를 Term으로 변환하는 파이프라인입니다. 인덱싱과 검색 모두에서 같은 분석기가 적용되어야 정확히 매칭됩니다.

2-1) 분석기 구조: 3단계

원문 텍스트
[Character Filter] → HTML 태그 제거, 특수문자 변환
[Tokenizer] → 텍스트를 토큰(단어)으로 분리
[Token Filter] → 소문자 변환, 불용어 제거, 동의어 처리
최종 Term 목록 → 역인덱스에 저장

2-2) 내장 분석기 비교

분석기입력: “The Quick Brown Fox”출력 Term
standard기본값[the, quick, brown, fox]
simple비문자 기준 분리[the, quick, brown, fox]
whitespace공백 기준 분리[The, Quick, Brown, Fox]
keyword분석 안 함[The Quick Brown Fox]
stop+ 불용어 제거[quick, brown, fox]

2-3) 한국어 분석기: Nori

한국어는 교착어(조사/어미 결합)라서 기본 분석기로는 제대로 검색이 안 됩니다.

Nori 플러그인 설치:

# Elasticsearch에 Nori 플러그인 설치
bin/elasticsearch-plugin install analysis-nori

Nori 분석기 설정:

PUT /products
{
  "settings": {
    "analysis": {
      "analyzer": {
        "korean_analyzer": {
          "type": "custom",
          "tokenizer": "nori_tokenizer",
          "filter": [
            "nori_readingform",
            "lowercase",
            "nori_part_of_speech_filter",
            "synonym_filter"
          ]
        }
      },
      "tokenizer": {
        "nori_tokenizer": {
          "type": "nori_tokenizer",
          "decompound_mode": "mixed",
          "discard_punctuation": true,
          "user_dictionary": "userdict_ko.txt"
        }
      },
      "filter": {
        "nori_part_of_speech_filter": {
          "type": "nori_part_of_speech",
          "stoptags": ["E", "IC", "J", "MAG", "MAJ",
                       "MM", "SP", "SSC", "SSO", "SC",
                       "SE", "XPN", "XSA", "XSN", "XSV",
                       "UNA", "NA", "VSV"]
        },
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "삼성, 삼성전자, Samsung",
            "맥북, MacBook",
            "노트북, 랩탑, laptop"
          ]
        }
      }
    }
  }
}

Nori decompound_mode 비교:

모드입력: “삼성전자”결과사용 상황
none복합어 그대로[삼성전자]정확 매치만 원할 때
discard분해만[삼성, 전자]부분 매치
mixed (권장)원형 + 분해[삼성전자, 삼성, 전자]정확 + 부분 매치 모두

분석 결과 확인:

POST /products/_analyze
{
  "analyzer": "korean_analyzer",
  "text": "삼성전자 갤럭시 S25 울트라를 출시했습니다"
}
// 결과: [삼성전자, 삼성, 전자, 갤럭시, s25, 울트라, 출시]
// 조사('를'), 어미('했습니다')가 제거됨

3) 매핑(Mapping) 설계

매핑은 각 필드의 타입과 분석 방법을 정의합니다. 한번 생성된 매핑은 변경 불가 — 변경하려면 재인덱싱이 필요합니다.

3-1) 주요 필드 타입

타입분석용도예시
text전문 검색상품명, 설명
keyword정확 일치, 필터, 정렬, 집계카테고리, 상태, 이메일
long/integer숫자 범위 검색, 정렬가격, 수량
date날짜 범위, 히스토그램생성일, 주문일
boolean필터활성 여부
nested✅/❌객체 배열 독립 검색리뷰, 옵션
geo_point위치 기반 검색좌표(lat/lon)
completion-자동완성 Suggest검색어 추천

3-2) text + keyword 복합 매핑 (가장 흔한 패턴)

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "korean_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          },
          "suggest": {
            "type": "completion"
          }
        }
      },
      "category": {
        "type": "keyword"
      },
      "price": {
        "type": "long"
      },
      "description": {
        "type": "text",
        "analyzer": "korean_analyzer"
      },
      "tags": {
        "type": "keyword"
      },
      "reviews": {
        "type": "nested",
        "properties": {
          "author": { "type": "keyword" },
          "rating": { "type": "integer" },
          "content": { "type": "text", "analyzer": "korean_analyzer" }
        }
      },
      "location": {
        "type": "geo_point"
      },
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

name 필드 사용 방법:

  • name → text 타입, 전문 검색 (match 쿼리)
  • name.keyword → keyword 타입, 정확 일치/정렬/집계 (term 쿼리)
  • name.suggest → completion 타입, 자동완성

3-3) Dynamic Mapping vs Explicit Mapping

DynamicExplicit
정의ES가 필드 타입 자동 추론개발자가 명시적으로 정의
장점빠른 프로토타이핑정확한 타입 제어
단점의도와 다른 타입 매핑 (숫자 문자열 → text)매핑 작업 필요
운영 권장
// Dynamic Mapping 비활성화 (권장)
PUT /products
{
  "mappings": {
    "dynamic": "strict",
    "properties": { ... }
  }
}
// dynamic: strict → 매핑에 없는 필드가 들어오면 에러
// dynamic: false  → 매핑에 없는 필드 저장은 하지만 인덱싱 안 함
// dynamic: true   → 자동 매핑 (기본값, 운영 비권장)

4) 쿼리 유형별 실전 코드

4-1) 쿼리 유형 분류

Query DSL
├── Full-text Query (분석기 거침 → text 타입)
│   ├── match          : 기본 전문 검색
│   ├── multi_match    : 여러 필드 검색
│   ├── match_phrase   : 구문 검색 (순서 유지)
│   └── query_string   : Lucene 구문 직접 사용
├── Term-level Query (분석 안 함 → keyword/숫자/날짜)
│   ├── term           : 정확 일치
│   ├── terms          : 다중 값 일치 (IN)
│   ├── range          : 범위 검색
│   ├── exists         : 필드 존재 여부
│   └── prefix/wildcard: 접두사/와일드카드
└── Compound Query
    ├── bool           : must/should/must_not/filter 조합
    ├── function_score : 점수 커스터마이징
    └── nested         : nested 객체 검색

4-2) Bool Query (가장 많이 사용)

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": {
              "query": "삼성 노트북",
              "operator": "and"
            }
          }
        }
      ],
      "filter": [
        { "term": { "category": "노트북" } },
        { "range": { "price": { "gte": 500000, "lte": 2000000 } } },
        { "range": { "created_at": { "gte": "2025-01-01" } } }
      ],
      "should": [
        { "term": { "tags": { "value": "고성능", "boost": 2.0 } } },
        { "match": { "description": "가성비" } }
      ],
      "minimum_should_match": 1,
      "must_not": [
        { "term": { "status": "discontinued" } }
      ]
    }
  },
  "highlight": {
    "fields": {
      "name": { "pre_tags": ["<em>"], "post_tags": ["</em>"] },
      "description": { "fragment_size": 150, "number_of_fragments": 3 }
    }
  },
  "sort": [
    { "_score": "desc" },
    { "price": "asc" }
  ],
  "from": 0,
  "size": 20
}

Bool Query 절별 차이:

점수 계산캐시용도
must필수 조건 + 관련도에 영향
filter필수 조건 + 점수 무관 (가장 빠름)
should선호 조건 (점수 부스팅)
must_not제외 조건

💡 성능 팁: “점수가 필요 없는 조건"은 전부 filter로 넣으세요. 캐시되어 반복 쿼리가 빨라집니다.

4-3) Nested Query (객체 배열 독립 검색)

// ❌ 일반 object 배열: Cross-matching 문제
// reviews: [{author: "Kim", rating: 5}, {author: "Lee", rating: 1}]
// "Lee가 쓴 별점 5 리뷰" 검색 → 잘못된 매칭 발생!

// ✅ nested 타입 사용
GET /products/_search
{
  "query": {
    "nested": {
      "path": "reviews",
      "query": {
        "bool": {
          "must": [
            { "term": { "reviews.author": "Lee" } },
            { "range": { "reviews.rating": { "gte": 4 } } }
          ]
        }
      },
      "inner_hits": {}
    }
  }
}

5) BM25 스코어링

ES 5.0+의 기본 스코어링 알고리즘은 **BM25(Best Matching 25)**입니다.

5-1) BM25 공식 (직관적 이해)

score(D, Q) = Σ IDF(qi) × [ TF(qi, D) × (k1 + 1) ]
                            ──────────────────────────
                            TF(qi, D) + k1 × (1 - b + b × |D|/avgdl)
요소의미영향
TF (Term Frequency)문서 내 검색어 출현 횟수많을수록 높은 점수 (로그적 수렴)
IDF (Inverse Document Frequency)전체 문서 중 검색어 포함 비율의 역수희귀한 단어일수록 높은 점수
k1 (기본 1.2)TF 포화 속도높으면 TF가 더 오래 영향
b (기본 0.75)문서 길이 보정1이면 짧은 문서 유리, 0이면 길이 무시
avgdl전체 문서 평균 길이정규화 기준

핵심 직관:

  • “맥북"이라는 단어가 3번 나온 짧은 문서 > 3번 나온 긴 문서
  • “맥북”(흔한 단어)보다 “M3”(드문 단어)가 매칭 시 점수 기여가 큼

5-2) function_score로 비즈니스 로직 반영

GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "name": "노트북" } },
      "functions": [
        {
          "field_value_factor": {
            "field": "sales_count",
            "modifier": "log1p",
            "factor": 0.5
          },
          "weight": 2
        },
        {
          "gauss": {
            "created_at": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          },
          "weight": 1
        },
        {
          "filter": { "term": { "is_promoted": true } },
          "weight": 3
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}
함수역할효과
field_value_factor판매량으로 부스팅인기 상품 상위 노출
gauss (decay)최신 상품 우대30일 내 상품이 높은 점수
filter + weight프로모션 상품 부스팅광고 상품 상위

6) 집계(Aggregation)

집계는 ES의 “실시간 분석 엔진” 기능입니다. SQL의 GROUP BY + SUM/AVG/COUNT를 JSON 형태로 지원합니다.

6-1) 집계 유형

유형예시SQL 대응
Bucket카테고리별 그룹핑GROUP BY
Metric평균/최대/최소/합계AVG/MAX/MIN/SUM
Pipeline집계 결과에 대한 2차 집계서브쿼리

6-2) 실전 집계 예시

GET /products/_search
{
  "size": 0,
  "query": {
    "range": { "created_at": { "gte": "2025-01-01" } }
  },
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category",
        "size": 20,
        "order": { "avg_price": "desc" }
      },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } },
        "max_price": { "max": { "field": "price" } },
        "price_ranges": {
          "range": {
            "field": "price",
            "ranges": [
              { "to": 500000, "key": "저가" },
              { "from": 500000, "to": 1500000, "key": "중가" },
              { "from": 1500000, "key": "고가" }
            ]
          }
        },
        "monthly_trend": {
          "date_histogram": {
            "field": "created_at",
            "calendar_interval": "month",
            "format": "yyyy-MM"
          },
          "aggs": {
            "monthly_avg": { "avg": { "field": "price" } }
          }
        }
      }
    },
    "overall_stats": {
      "stats": { "field": "price" }
    }
  }
}

7) 클러스터 아키텍처와 Shard 설계

7-1) 노드 역할

역할설정주요 기능권장 스펙
Masternode.roles: [master]클러스터 상태 관리, Shard 할당CPU/메모리 적당, 디스크 적음
Datanode.roles: [data]인덱싱, 검색높은 디스크/메모리
Data Hotnode.roles: [data_hot]최신 데이터 (SSD)빠른 SSD
Data Warmnode.roles: [data_warm]오래된 데이터 (HDD)대용량 HDD
Coordinatingnode.roles: []요청 라우팅, 결과 병합CPU
Ingestnode.roles: [ingest]인덱싱 전 데이터 변환CPU

7-2) Shard 크기 가이드라인

항목권장이유
Shard 크기10~50GB너무 작으면 오버헤드, 너무 크면 복구 시간 증가
Shard 수/노드20개 이하/GB 힙힙 사용량(Shard 메타데이터)이 누적
Primary Shard생성 후 변경 불가_split/_shrink로만 조정
Replica최소 10이면 노드 장애 시 데이터 유실
// 인덱스 생성 시 Shard 설정
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "refresh_interval": "5s"
  }
}
// 3 Primary × 2(원본+복제) = 6 Shard → 최소 2노드 필요

7-3) Index Lifecycle Management (ILM)

시계열 데이터(로그, 메트릭)의 인덱스를 자동으로 관리합니다.

PUT _ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_age": "7d",
            "max_size": "50gb",
            "max_docs": 100000000
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "30d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },
          "set_priority": { "priority": 50 },
          "allocate": {
            "require": { "data": "warm" }
          }
        }
      },
      "cold": {
        "min_age": "90d",
        "actions": {
          "freeze": {},
          "allocate": {
            "require": { "data": "cold" }
          }
        }
      },
      "delete": {
        "min_age": "365d",
        "actions": { "delete": {} }
      }
    }
  }
}
Hot (SSD)          Warm (HDD)         Cold (Archive)     Delete
0~7일              30~90일             90~365일            365일+
활발한 쓰기/읽기    읽기만              거의 접근 없음       자동 삭제
높은 I/O           Shrink+ForceMerge  Freeze              

8) 성능 최적화 팁

인덱싱 성능

기법효과설정
Bulk API건별 요청 대비 10~20배 빠름_bulk, 배치 크기 5~15MB
refresh_interval 늘리기쓰기 부하 감소대량 인덱싱 시 "30s" 또는 "-1"
Replica 0으로 인덱싱복제 오버헤드 제거인덱싱 후 Replica 복구
불필요한 필드 비활성화인덱스 크기 감소"enabled": false, "index": false

검색 성능

기법효과설명
filter 활용캐시 + 점수 계산 생략점수 불필요 조건은 전부 filter
_source filtering네트워크 전송량 감소"_source": ["name", "price"]
Routing검색 Shard 수 감소"routing": "category_id"
Search AfterDeep paging 대안from: 10000은 성능 문제 → search_after 사용

안티패턴 6가지

#안티패턴문제올바른 방법
1Dynamic Mapping 운영 사용의도치 않은 타입 + Mapping Explosiondynamic: strict
2keyword 필드에 match 쿼리분석 안 되므로 정확 일치만 동작keyword → term, text → match
3from+size deep paging10000건+ 시 OOM 위험search_after + PIT
4무분별한 nested 사용문서당 Lucene 문서 수 폭증정말 독립 검색이 필요한 경우만
5Shard 과다(인덱스당 수십 개)힙/메타데이터 오버헤드Shard 10~50GB 기준 산정
6refresh_interval=1s + 대량 인덱싱Segment 과다 생성대량 인덱싱 시 30s 또는 -1

운영 체크리스트

  • 매핑을 Explicit으로 설계했는가? (dynamic: strict)
  • text/keyword 복합 매핑을 적절히 사용하고 있는가?
  • 한국어 필드에 Nori 분석기를 설정했는가?
  • Shard 크기가 10~50GB 범위인가?
  • Replica가 최소 1 이상인가?
  • ILM 정책이 설정되어 있는가? (시계열 데이터)
  • Bulk API를 사용하고 있는가? (건별 인덱싱 아님)
  • 점수 불필요 조건은 filter로 처리하고 있는가?
  • _cluster/health 상태가 Green인가?
  • 디스크 사용량 85% 이하인가? (watermark 기본값)

관련 글