Elasticsearch

Elasticsearch는 대량의 데이터를 실시간으로 검색하고 분석할 수 있는 분산형 검색 엔진으로, 역색인(Inverted Index) 구조를 기반으로 빠른 전문 검색(Full-Text Search)을 제공합니다.

왜 필요한가

[일반 DB 검색의 한계]
SELECT * FROM products WHERE name LIKE '%무선 블루투스 이어폰%';

→ LIKE '%...%' 는 인덱스를 활용하지 못함 → Full Table Scan
→ 데이터가 수백만 건이면 수 초~수십 초 소요
→ "블루투스 무선 이어폰" (어순 변경) → 검색 안 됨 X
→ "이어폰 블루투스" (부분 단어) → 검색 안 됨 X
→ 오타 보정, 유의어 검색 불가

[Elasticsearch]
GET /products/_search
{"query": {"match": {"name": "블루투스 무선 이어폰"}}}

→ 역색인 기반 → 밀리초 단위 응답
→ 어순 무관하게 검색 가능 O
→ 유사 단어, 오타 보정 가능 O

핵심 개념 — 역색인 (Inverted Index)

일반 DB는 문서 → 단어 방향으로 저장하지만, Elasticsearch는 단어 → 문서 방향으로 저장합니다.

[일반 DB — 정방향 인덱스]
문서1: "무선 블루투스 이어폰"
문서2: "블루투스 스피커"
문서3: "무선 마우스"

→ "블루투스" 검색 시 모든 문서를 하나씩 확인해야 함

[Elasticsearch — 역색인]
"무선"     → [문서1, 문서3]
"블루투스"  → [문서1, 문서2]
"이어폰"   → [문서1]
"스피커"   → [문서2]
"마우스"   → [문서3]

→ "블루투스" 검색 시 → 즉시 [문서1, 문서2] 반환
→ "무선 블루투스" 검색 시 → 교집합 [문서1] 반환
→ 데이터가 아무리 많아도 빠름

기본 구조 — RDBMS와 비교

RDBMSElasticsearch설명
DatabaseIndex데이터 저장 단위
TableType (7.x 이후 폐지)문서 분류
RowDocument하나의 데이터 레코드
ColumnField데이터 항목
SchemaMapping필드 타입 정의
SQLQuery DSL검색 문법
// Elasticsearch Document 예시
{
    "_index": "products",
    "_id": "1",
    "_source": {
        "name": "무선 블루투스 이어폰",
        "price": 45000,
        "category": "전자기기",
        "description": "고품질 무선 블루투스 이어폰입니다"
    }
}

기본 CRUD 예시

// 문서 생성 (인덱싱)
PUT /products/_doc/1
{
    "name": "무선 블루투스 이어폰",
    "price": 45000,
    "category": "전자기기"
}

// 문서 조회
GET /products/_doc/1

// 문서 검색
GET /products/_search
{
    "query": {
        "match": {
            "name": "블루투스 이어폰"
        }
    }
}

// 문서 수정
POST /products/_update/1
{
    "doc": {
        "price": 39000
    }
}

// 문서 삭제
DELETE /products/_doc/1

검색 기능

전문 검색 (Full-Text Search)

// match — 형태소 분석 후 검색 (기본)
GET /products/_search
{
    "query": {
        "match": {
            "name": "무선 이어폰"
        }
    }
}
// → "무선", "이어폰" 각각으로 분리 → 하나라도 포함되면 검색

// match_phrase — 어순 일치 검색
GET /products/_search
{
    "query": {
        "match_phrase": {
            "name": "무선 블루투스"
        }
    }
}
// → "무선 블루투스"가 연속으로 나오는 문서만 검색

// fuzzy — 오타 보정 검색
GET /products/_search
{
    "query": {
        "fuzzy": {
            "name": {
                "value": "블루투쓰",
                "fuzziness": 1
            }
        }
    }
}
// → "블루투스"도 검색됨 (편집 거리 1 이내)

복합 검색

GET /products/_search
{
    "query": {
        "bool": {
            "must": [
                {"match": {"name": "블루투스 이어폰"}}
            ],
            "filter": [
                {"range": {"price": {"lte": 50000}}},
                {"term": {"category": "전자기기"}}
            ]
        }
    },
    "sort": [
        {"price": "asc"}
    ]
}
// → 이름에 "블루투스 이어폰" 포함
// → 가격 5만원 이하
// → 카테고리가 "전자기기"
// → 가격 오름차순 정렬

분산 구조

                    ┌─────────────────────┐
                    │   Elasticsearch     │
                    │     Cluster         │
                    └────────┬────────────┘
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
         ┌─────────┐   ┌─────────┐   ┌─────────┐
         │ Node 1  │   │ Node 2  │   │ Node 3  │
         │(Master) │   │         │   │         │
         ├─────────┤   ├─────────┤   ├─────────┤
         │ Shard 0 │   │ Shard 1 │   │ Shard 2 │
         │(Primary)│   │(Primary)│   │(Primary)│
         │ Shard 1 │   │ Shard 2 │   │ Shard 0 │
         │(Replica)│   │(Replica)│   │(Replica)│
         └─────────┘   └─────────┘   └─────────┘

- 데이터를 Shard 단위로 분할하여 분산 저장
- 각 Shard의 Replica를 다른 노드에 배치 → 장애 대응
- 노드 추가만으로 수평 확장(Scale-Out) 가능

Java(Spring Boot) 연동 예시

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
// Document 정의
@Document(indexName = "products")
public class ProductDocument {

    @Id
    private String id;

    @Field(type = FieldType.Text, analyzer = "nori")  // 한국어 형태소 분석기
    private String name;

    @Field(type = FieldType.Integer)
    private int price;

    @Field(type = FieldType.Keyword)
    private String category;
}
// Repository
public interface ProductSearchRepository
        extends ElasticsearchRepository<ProductDocument, String> {

    List<ProductDocument> findByName(String name);
    List<ProductDocument> findByPriceBetween(int min, int max);
}
// Service — 복잡한 검색
@Service
@RequiredArgsConstructor
public class ProductSearchService {

    private final ElasticsearchOperations operations;

    public List<ProductDocument> search(String keyword, int maxPrice) {
        NativeQuery query = NativeQuery.builder()
                .withQuery(q -> q
                    .bool(b -> b
                        .must(m -> m.match(mt -> mt.field("name").query(keyword)))
                        .filter(f -> f.range(r -> r.field("price").lte(JsonData.of(maxPrice))))
                    )
                )
                .build();

        SearchHits<ProductDocument> hits = operations.search(query, ProductDocument.class);
        return hits.getSearchHits().stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
}

Analyzer — 텍스트 분석 파이프라인

역색인을 만들기 위해 텍스트를 토큰으로 분해하는 과정입니다.

[Analyzer 처리 흐름]
원문: "무선 블루투스 이어폰입니다!"
        │
        ▼
  [Character Filter]  → 불필요한 문자 제거/변환 (HTML 태그 제거 등)
        │
        ▼
  [Tokenizer]         → 공백, 형태소 등으로 단어 분리
        │              "무선", "블루투스", "이어폰입니다", "!"
        ▼
  [Token Filter]      → 소문자화, 불용어 제거, 동의어 처리
                       "무선", "블루투스", "이어폰"
Analyzer언어특징
standard범용공백/특수문자 기준 분리
nori한국어형태소 분석 (은/는/이/가 등 조사 제거)
english영어어간 추출 (running → run)
whitespace범용공백 기준만 분리
// 한국어 인덱스 매핑 — nori 분석기 지정
PUT /products
{
    "settings": {
        "analysis": {
            "analyzer": {
                "korean": {
                    "type": "nori"
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "nori"
            }
        }
    }
}

Aggregation — 집계/분석

검색과 함께 통계를 내는 기능입니다. SQL의 GROUP BY + 집계 함수와 유사합니다.

// 카테고리별 평균 가격 집계
GET /products/_search
{
    "size": 0,
    "aggs": {
        "by_category": {
            "terms": {
                "field": "category"
            },
            "aggs": {
                "avg_price": {
                    "avg": {
                        "field": "price"
                    }
                }
            }
        }
    }
}

// 응답
{
    "aggregations": {
        "by_category": {
            "buckets": [
                {"key": "전자기기", "doc_count": 150, "avg_price": {"value": 45000}},
                {"key": "의류",    "doc_count": 300, "avg_price": {"value": 32000}}
            ]
        }
    }
}

주요 집계 타입:

타입예시설명
termsGROUP BY category값별 버킷 분류
range가격 구간별범위별 버킷
date_histogram월별 판매량시간 단위 버킷
avg / sum / max / min평균 가격숫자 통계
cardinality고유 사용자 수중복 제거 카운트

RDBMS + Elasticsearch 병행 구조

실무에서는 Elasticsearch를 DB 대체가 아닌 검색 전용 계층으로 함께 사용합니다.

[일반적인 아키텍처]
                  쓰기                     읽기(검색)
클라이언트 ──→ API 서버 ──→ MySQL        클라이언트 ──→ API 서버 ──→ Elasticsearch
                   │
                   └──→ Elasticsearch (동기화)

- MySQL: 원본 데이터 저장 (트랜잭션, 정합성)
- Elasticsearch: 검색용 인덱스 (빠른 전문 검색)
- 데이터 동기화: 이벤트 기반 또는 배치로 MySQL → Elasticsearch 반영

RDBMS vs Elasticsearch 비교

항목RDBMSElasticsearch
검색 속도LIKE 검색 느림역색인 기반 밀리초 응답
전문 검색제한적형태소 분석, 유의어, 오타 보정
트랜잭션ACID 보장 O미지원 X
정합성강한 정합성근실시간 (Near Real-Time)
JOIN지원 O미지원 (비정규화로 해결)
용도데이터 저장/관리검색/분석

면접 포인트

CSLOG에서 보기