선수 개념

이 문서를 읽기 전에 다음 개념을 먼저 이해하세요:

소요 시간: 약 25-30분

전체 비유: 도서관의 신간 입고 프로세스#

인덱싱 전략을 도서관의 신간 입고 프로세스에 비유하면 이해하기 쉽습니다:

도서관 비유Elasticsearch역할
한 권씩 등록단건 인덱싱느리지만 즉시 확인 가능
박스 단위 대량 입고Bulk 인덱싱훨씬 빠름 (10배 이상)
입고 후 서가 배치Refresh검색 가능하게 만듦 (기본 1초)
영구 장서 등록Flush디스크에 영구 저장
입고 일지Translog장애 시 복구용 기록
신간 분류 규칙 템플릿Index Template새 인덱스 자동 설정
오래된 책 → 창고 → 폐기ILM (Index Lifecycle)Hot → Warm → Cold → Delete
구판 → 신판 교체Reindex데이터 마이그레이션
도서 별칭 (시리즈명)Alias인덱스 별명으로 무중단 교체

이처럼 인덱싱 전략은 “도서관에 새 책을 효율적으로 입고하고 관리"하는 프로세스와 같습니다.

대용량 데이터를 효율적으로 저장하기 위한 Bulk 인덱싱, Refresh, Index Lifecycle Management를 배웁니다.

Elasticsearch의 인덱싱은 단순히 데이터를 저장하는 것 이상의 의미를 가집니다. 문서가 인덱싱되면 텍스트 분석, 역색인 생성, 세그먼트 관리 등 복잡한 과정을 거치게 됩니다. 이 과정을 이해하면 왜 Bulk 인덱싱이 단건보다 10배 이상 빠른지, 왜 인덱싱 직후 검색이 안 되는지, 왜 대량 인덱싱 시 Refresh를 비활성화해야 하는지 알 수 있습니다. 올바른 인덱싱 전략은 시스템 성능과 운영 비용에 직접적인 영향을 미칩니다.

인덱싱 기본 개념#

인덱싱 과정#

flowchart LR
    A[문서 수신] --> B[분석 Analyze]
    B --> C[역색인 생성]
    C --> D[Memory Buffer]
    D --> E[Refresh]
    E --> F[Segment]
    F --> G[Flush]
    G --> H[Disk]

문서가 수신되어 분석, 역색인 생성, 메모리 버퍼 저장, Refresh, Segment 생성, Flush를 거쳐 디스크에 영구 저장되는 인덱싱 과정입니다.

단계설명
분석텍스트를 토큰으로 분리
Memory Buffer메모리에 임시 저장
Refresh검색 가능하게 만듦 (기본 1초)
Segment불변의 인덱스 조각
Flush디스크에 영구 저장

단건 vs Bulk 인덱싱#

1만 건의 데이터를 하나씩 인덱싱하면 네트워크 왕복이 10,000번 발생하여 약 30초가 걸립니다. 같은 데이터를 1,000건씩 묶어서 보내면 10번의 요청으로 약 3초 만에 완료됩니다. Bulk 인덱싱은 네트워크 오버헤드를 대폭 줄여 대량 데이터 처리 속도를 10배 이상 향상시킵니다.

단건 인덱싱#

PUT /products/_doc/1
{
  "name": "맥북 프로",
  "price": 2390000
}

Bulk 인덱싱#

여러 문서를 한 번에 처리:

POST /_bulk
{"index": {"_index": "products", "_id": "1"}}
{"name": "맥북 프로", "price": 2390000}
{"index": {"_index": "products", "_id": "2"}}
{"name": "맥북 에어", "price": 1390000}
{"index": {"_index": "products", "_id": "3"}}
{"name": "아이패드", "price": 1499000}

NDJSON 형식: 각 줄은 개행문자(\n)로 구분, 마지막 줄도 개행 필요

성능 비교#

방식1만 건 처리 시간네트워크 요청
단건~30초10,000회
Bulk (1000건씩)~3초10회

Bulk 권장 설정#

POST /_bulk
// 권장 크기: 5-15MB per request
// 권장 문서 수: 1,000-5,000

Spring에서 Bulk 인덱싱#

@Service
public class ProductBulkService {

    private final ElasticsearchOperations operations;

    public void bulkIndex(List<Product> products) {
        List<IndexQuery> queries = products.stream()
            .map(product -> new IndexQueryBuilder()
                .withId(product.getId())
                .withObject(product)
                .build())
            .toList();

        operations.bulkIndex(queries, Product.class);
    }
}

Refresh#

Refresh란?#

Memory Buffer의 데이터를 검색 가능하게 만드는 작업입니다.

flowchart LR
    A[Memory Buffer] -->|Refresh| B[Segment<br>검색 가능]

메모리 버퍼의 데이터가 Refresh를 통해 검색 가능한 Segment로 변환되는 과정을 보여줍니다.

Refresh Interval#

PUT /products/_settings
{
  "index": {
    "refresh_interval": "30s"    // 기본값: 1s
  }
}
설정의미용도
1s1초마다 (기본)실시간 검색
30s30초마다일반적인 서비스
-1비활성화대량 인덱싱 중

대량 인덱싱 시 최적화#

// 1. Refresh 비활성화
PUT /products/_settings
{ "refresh_interval": "-1" }

// 2. Bulk 인덱싱 수행
POST /_bulk
...

// 3. 수동 Refresh
POST /products/_refresh

// 4. Refresh 복원
PUT /products/_settings
{ "refresh_interval": "1s" }

Flush와 Translog#

Translog#

데이터 유실 방지를 위한 Write-Ahead Log입니다. Lucene 내부 구조에서 중요한 역할을 합니다. → Lucene 내부 구조 상세

flowchart LR
    A[문서] --> B[Translog]
    A --> C[Memory Buffer]
    B -->|장애 복구| D[데이터 복원]
    C -->|Flush| E[Disk Segment]

문서가 Translog와 메모리 버퍼에 동시에 기록되어, 장애 시 Translog로 복구하고 Flush로 디스크에 영구 저장하는 구조입니다.

Flush#

Memory Buffer + Translog → Disk Segment 영구화:

POST /products/_flush

주의: 일반적으로 수동 Flush 불필요. Elasticsearch가 자동 관리.

Flush 설정#

PUT /products/_settings
{
  "index": {
    "translog": {
      "durability": "async",        // async: 성능, request: 안정성
      "sync_interval": "5s",
      "flush_threshold_size": "512mb"
    }
  }
}

Index Template#

매일 logs-2024-01-01, logs-2024-01-02 같은 인덱스를 생성해야 한다면, 매번 샤드 수, Replica, Mapping을 수동으로 정의해야 할까요? 실수로 설정을 빠뜨리면 인덱스마다 구조가 달라져 검색과 운영에 문제가 생깁니다. Index Template은 패턴에 맞는 인덱스가 생성될 때 자동으로 설정을 적용합니다.

새 인덱스 생성 시 자동 적용되는 설정:

PUT /_index_template/products_template
{
  "index_patterns": ["products-*"],
  "priority": 1,
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "refresh_interval": "5s"
    },
    "mappings": {
      "properties": {
        "name": { "type": "text" },
        "price": { "type": "integer" },
        "created_at": { "type": "date" }
      }
    }
  }
}

이제 products-2024, products-2025 등 생성 시 자동 적용.


Index Lifecycle Management (ILM)#

로그 데이터가 매일 수십 GB씩 쌓이는데 수동으로 오래된 인덱스를 삭제하고, 접근 빈도가 낮은 인덱스를 저비용 노드로 옮기는 작업을 반복해야 한다면 어떻게 될까요? 운영자의 실수로 디스크가 꽉 차거나 중요한 데이터가 삭제될 수 있습니다. ILM은 Hot → Warm → Cold → Delete 정책을 자동으로 실행하여 이런 운영 부담과 위험을 제거합니다.

시계열 데이터의 수명주기를 자동 관리합니다. 로그 데이터 관리에 특히 유용합니다. → ILM 실전 적용 예제

수명주기 단계#

flowchart LR
    A[Hot<br>활발한 쓰기/읽기] --> B[Warm<br>읽기 위주]
    B --> C[Cold<br>가끔 읽기]
    C --> D[Frozen<br>거의 안 읽음]
    D --> E[Delete<br>삭제]

인덱스 수명주기의 다섯 단계를 보여줍니다. Hot에서 활발히 사용되다가 Warm, Cold, Frozen으로 점차 접근 빈도가 줄고, 최종적으로 Delete됩니다.

ILM 정책 생성#

PUT /_ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50gb",
            "max_age": "7d"
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },
          "set_priority": { "priority": 50 }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "set_priority": { "priority": 0 }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

ILM 정책 적용#

PUT /_index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "logs_policy",
      "index.lifecycle.rollover_alias": "logs"
    }
  }
}

Reindex#

기존 인덱스를 새 인덱스로 복사/변환:

기본 Reindex#

POST /_reindex
{
  "source": { "index": "products-old" },
  "dest": { "index": "products-new" }
}

필터링 Reindex#

POST /_reindex
{
  "source": {
    "index": "products-old",
    "query": {
      "term": { "in_stock": true }
    }
  },
  "dest": { "index": "products-active" }
}

필드 변환#

POST /_reindex
{
  "source": { "index": "products-old" },
  "dest": { "index": "products-new" },
  "script": {
    "source": "ctx._source.price_krw = ctx._source.price * 1000"
  }
}

비동기 Reindex#

POST /_reindex?wait_for_completion=false
{
  "source": { "index": "large-index" },
  "dest": { "index": "large-index-new" }
}

진행 상황 확인:

GET /_tasks?actions=*reindex&detailed

Alias#

애플리케이션 코드에 인덱스명 products-v1을 직접 하드코딩했는데, 매핑을 변경하여 products-v2로 전환해야 한다면? 모든 코드를 수정하고 배포해야 하며, 전환 시점에 다운타임이 발생합니다. Alias는 인덱스에 별명을 붙여 애플리케이션은 별명만 바라보게 하고, 실제 인덱스를 무중단으로 교체할 수 있게 합니다.

인덱스에 별명을 붙여 유연하게 관리:

Alias 생성#

POST /_aliases
{
  "actions": [
    { "add": { "index": "products-v1", "alias": "products" } }
  ]
}

Zero Downtime 재인덱싱#

// 1. 새 인덱스 생성 및 데이터 복사
PUT /products-v2
POST /_reindex
{
  "source": { "index": "products-v1" },
  "dest": { "index": "products-v2" }
}

// 2. Alias 전환 (원자적)
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products-v1", "alias": "products" } },
    { "add": { "index": "products-v2", "alias": "products" } }
  ]
}

애플리케이션은 products alias만 사용 → 무중단 전환


인덱싱 성능 최적화#

대량 인덱싱 체크리스트#

// 1. Replica 비활성화
PUT /products/_settings
{ "number_of_replicas": 0 }

// 2. Refresh 비활성화
PUT /products/_settings
{ "refresh_interval": "-1" }

// 3. Bulk 인덱싱 수행
POST /_bulk
...

// 4. Refresh
POST /products/_refresh

// 5. 설정 복원
PUT /products/_settings
{
  "number_of_replicas": 1,
  "refresh_interval": "1s"
}

최적 Bulk 크기#

항목권장 값
요청 크기5-15 MB
문서 수1,000-5,000
동시 요청2-3 (노드당)

인덱싱 스레드#

PUT /products/_settings
{
  "index": {
    "indexing": {
      "slowlog": {
        "threshold": {
          "index": {
            "warn": "10s",
            "info": "5s"
          }
        }
      }
    }
  }
}

다음 단계#

목표추천 문서
클러스터 구성클러스터 관리
검색 최적화성능 튜닝
장애 대응고가용성