본문 바로가기
Research Notes (개인)

OpenSearch 완전 가이드: Reindex(무중단)부터 Analyzer/Tokenizer/Normalizer, 인덱스 설계, Keyword·Term 개념까지

OpenSearch를 운영하다 보면 결국 Reindex로 돌아옵니다.

  • 매핑을 바꿔야 한다(필드 타입 변경/추가)
  • analyzer를 바꿔야 한다(토큰화/동의어/정규화)
  • 정렬/집계를 더 빠르게 해야 한다(doc_values, keyword)
  • 검색 품질을 올려야 한다(BM25 튜닝, ngram, synonym)
  • 벡터/하이브리드를 붙이고 싶다(k-NN, RRF)

문제는 OpenSearch에서 **많은 변경이 "기존 인덱스에 바로 적용되지 않는다"**는 점입니다.

그래서 운영에서는 "수정"이 아니라 새 인덱스를 만들고 Reindex한 뒤 Alias를 스왑하는 방식(블루/그린)이 표준입니다.

이 글은 다음을 예시와 함께 한 번에 정리합니다.

  • Reindex가 왜 필요한지 / 언제 필요한지
  • 무중단 Reindex(blue/green + alias) 운영 절차
  • analyzer / tokenizer / token filter / char filter / normalizer 차이와 예시
  • 인덱스 설계 전략(샤드/레플리카/refresh/merge/ILM/alias)
  • keyword vs text, term의 의미, query 유형별 올바른 사용법
  • 실전에서 자주 터지는 함정과 체크리스트

참고: OpenSearch는 Elasticsearch 계열이므로 개념/DSL이 매우 유사합니다. (버전/플러그인 차이는 운영 환경에 맞게 확인)


TL;DR

  • 매핑/분석기(analyzer)는 "데이터가 인덱싱되는 방식" 이라서, 바꾸려면 대부분 Reindex가 필요합니다.
  • 운영 표준은 index_vN 새로 만들고 -> Reindex -> 검증 -> alias swap 입니다.
  • text는 "검색용(분석됨)", keyword는 "정확일치/정렬/집계용(분석 안 됨)"입니다.
  • term query분석을 안 하는 정확일치입니다. text 필드에 쓰면 흔히 "왜 검색이 안 되지?"가 발생합니다.
  • analyzer/normalizer는 품질에 직결되지만, 바꾸는 순간 Reindex 비용이 생기므로 버저닝/alias 전략이 필수입니다.

1) Reindex가 필요한 이유: "인덱싱은 과거를 바꾸지 않는다"

OpenSearch에서 검색은 크게 두 단계를 가집니다.

  1. Index-time(색인 시점): 문서를 저장하면서 텍스트를 토큰화/정규화하고 역색인을 만든다
  2. Search-time(검색 시점): 쿼리를 분석해서 역색인에서 매칭한다

여기서 중요한 점:

  • analyzer/tokenizer/필드 타입 같은 설정은 Index-time에 적용됩니다.
  • 이미 인덱싱된 문서의 토큰/필드 구조는 자동으로 다시 만들어지지 않습니다.

즉, 아래 변경은 대부분 "기존 인덱스에 적용 불가" -> 새 인덱스 + Reindex로 갑니다.

Reindex가 거의 필수인 변경들

  • 필드 타입 변경: text -> keyword, integer -> long, date format 변경
  • analyzer 변경(동의어/토크나이저/필터 변경)
  • normalizer 변경(keyword 정규화 방식 변경)
  • index_options, norms, doc_values, fielddata 같은 인덱싱/저장 방식 변경
  • copy_to 전략 변경, multi-field 설계 변경

Reindex 없이 가능한 변경(대표)

  • 새 필드 추가(단, 기존 문서에 값이 없을 뿐)
  • 일부 settings(예: number_of_replicas) 조정
  • alias 추가/변경(인덱스 데이터 자체는 그대로)

2) Reindex의 3가지 방식: API / Update-by-query / 재색인 파이프라인

2.1 _reindex API (가장 표준)

  • 소스 인덱스 -> 목적 인덱스로 문서 복사
  • (옵션) 스크립트로 문서 변형 가능

2.2 _update_by_query

  • 같은 인덱스 내에서 문서를 다시 써서(업데이트) 일부 필드를 재계산
  • analyzer 변경 같은 "토큰 재생성"에는 한계가 큼(근본적 구조는 안 바뀜)

2.3 애플리케이션/ETL 기반 재색인

  • DB/원천에서 다시 읽어서 새 인덱스에 인덱싱
  • 가장 통제 가능하지만 구현 비용이 큼
  • 대규모 운영에선 결국 이 방식이 더 안정적인 경우도 많습니다(정합성/검증/재시도).

3) 무중단 Reindex 표준: Blue/Green + Alias Swap

운영에서 가장 안전한 패턴은 다음입니다.

  • 읽기 alias: books_read
  • 쓰기 alias: books_write
  • 실제 인덱스: books_v1, books_v2, ...

3.1 목표 상태

  • 검색 서비스는 항상 books_read만 조회
  • 인덱싱 파이프라인은 항상 books_write만 인덱싱
  • reindex 완료 후 books_read/books_write가 새로운 인덱스를 가리키도록 스왑

3.2 예시: v1 -> v2 무중단 절차

(1) 새 인덱스 생성: books_v2

PUT books_v2
{
  "settings": {
    "number_of_shards": 6,
    "number_of_replicas": 1,
    "analysis": {
      "char_filter": {
        "strip_html": { "type": "html_strip" }
      },
      "filter": {
        "ko_syn": {
          "type": "synonym_graph",
          "synonyms": [
            "해리포터, harry potter",
            "자바, java"
          ]
        }
      },
      "tokenizer": {
        "edge_ngram_tok": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 20,
          "token_chars": [ "letter", "digit" ]
        }
      },
      "analyzer": {
        "ko_search": {
          "type": "custom",
          "char_filter": [ "strip_html" ],
          "tokenizer": "standard",
          "filter": [ "lowercase", "ko_syn" ]
        },
        "ac_index": {
          "type": "custom",
          "tokenizer": "edge_ngram_tok",
          "filter": [ "lowercase" ]
        }
      },
      "normalizer": {
        "kw_norm": {
          "type": "custom",
          "filter": [ "lowercase", "asciifolding" ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "book_id": { "type": "keyword" },
      "title": {
        "type": "text",
        "analyzer": "ko_search",
        "fields": {
          "raw": { "type": "keyword", "normalizer": "kw_norm" }
        }
      },
      "author": {
        "type": "text",
        "analyzer": "ko_search",
        "fields": {
          "raw": { "type": "keyword", "normalizer": "kw_norm" }
        }
      },
      "published_at": { "type": "date" },
      "price": { "type": "integer" }
    }
  }
}

(2) Reindex 실행: v1 -> v2

POST _reindex?wait_for_completion=false
{
  "source": { "index": "books_v1" },
  "dest": { "index": "books_v2", "op_type": "create" }
}
  • wait_for_completion=false로 비동기 실행 후 task를 추적합니다.
  • op_type=create는 "이미 있는 문서는 덮어쓰지 않음"이라 재실행에 조금 더 안전합니다.

(3) Reindex 중 성능/부하 제어

POST _reindex?wait_for_completion=false
{
  "source": {
    "index": "books_v1",
    "size": 1000
  },
  "dest": { "index": "books_v2" },
  "conflicts": "proceed"
}
  • size로 배치 크기 조절
  • requests_per_second(지원 시)로 스로틀링
  • 운영에서는 리프레시/레플리카를 조절해 속도를 올렸다가 끝나면 원복하는 패턴을 씁니다.

(4) 검증(필수)

  • 문서 수 비교
  • 샘플 쿼리 결과 비교(스냅샷 테스트)
  • 집계/정렬 결과 비교
  • 특정 키워드(동의어/정규화) 케이스 회귀 테스트

예: 문서 수

GET books_v1/_count
GET books_v2/_count

예: 샘플 검색 품질 비교

GET books_v2/_search
{
  "query": { "match": { "title": "해리 포터" } },
  "size": 5
}

(5) Alias Swap (원자적으로)

POST _aliases
{
  "actions": [
    { "remove": { "alias": "books_read",  "index": "books_v1" } },
    { "add":    { "alias": "books_read",  "index": "books_v2" } },
    { "remove": { "alias": "books_write", "index": "books_v1" } },
    { "add":    { "alias": "books_write", "index": "books_v2", "is_write_index": true } }
  ]
}
  • _aliases는 액션을 묶어 "사실상 원자적"으로 바꿉니다.
  • 이 순간부터 서비스는 무중단으로 v2를 사용합니다.

(6) 구 인덱스 보관/삭제

  • 즉시 삭제하지 말고 일정 기간 보관(롤백/감사/회귀 대응)
  • 보관 기간이 끝나면 삭제

4) Analyzer / Tokenizer / Normalizer: 무엇이 어떻게 다른가?

4.1 Tokenizer

  • 텍스트를 토큰으로 쪼개는 규칙
  • 예: 공백/구두점 기준, ngram, edge_ngram 등

예: "Harry Potter" -> ["harry", "potter"] (standard tokenizer)

4.2 Analyzer

  • (char_filter) -> tokenizer -> token_filter
  • text 필드에 적용되는 "검색 가능한 형태로 만드는 파이프라인"

예시:

  • 소문자화, 동의어 확장, stopword 제거, stemming 등

4.3 Normalizer

  • keyword 필드용 "분석기"
  • 토큰화(tokenizer)가 없다 (keyword는 쪼개면 안 되니까)
  • 주로 lowercase, asciifolding 같은 정규화만 수행

예: Keyword: "Kim""kim"으로 통일 -> 집계/정렬/정확일치 안정화


5) 인덱스 설계 전략(운영 관점): 샤드/레플리카/refresh/alias

5.1 샤드 수

  • 샤드는 "병렬 처리 + 분산"의 단위
  • 너무 많으면 오버헤드(파일/세그먼트/메모리) 증가
  • 너무 적으면 확장성/처리량 제한

원칙

  • "지금 데이터 크기 + 성장률 + 쿼리 패턴"으로 결정
  • reindex로만 바꿀 수 있는 경우가 많으니(버전/환경에 따라) 초기부터 과도한 샤드는 피하는 편이 낫습니다.

5.2 레플리카 수

  • 검색 가용성/성능(읽기 분산) vs 인덱싱 비용(쓰기 2배)
  • ingest/reindex 동안 replicas=0으로 속도 올리고 끝나면 1로 올리는 패턴이 흔함(운영 정책에 따라)

5.3 refresh_interval

  • refresh는 "검색 가능해지는 주기"
  • 짧으면 실시간성이 좋지만 인덱싱 비용 증가
  • 대량 적재/리인덱스 동안 refresh를 늘리거나 끄고, 끝나면 원복하면 성능이 크게 좋아집니다.

5.4 alias는 운영의 핵심

  • 인덱스 이름은 버전으로 고정(_vN)
  • 서비스는 alias만 바라본다(_read, _write)
  • 무중단 전환/롤백/실험이 쉬워집니다.

6) text vs keyword: 검색/집계/정렬의 분기점

6.1 text

  • analyzer로 분석됨
  • match, multi_match 같은 "풀텍스트 검색"에 적합

6.2 keyword

  • 분석하지 않는 "그대로의 값"
  • term, terms, prefix, wildcard(주의), 정렬, 집계에 적합

6.3 실무 표준: multi-field

같은 원문을 두 관점으로 쓰고 싶으면:

  • titletext (검색)
  • title.rawkeyword (정렬/집계/정확일치)

이게 가장 흔한 패턴입니다.


7) "term"은 무엇인가? (그리고 왜 term query가 자주 실패하나)

7.1 term(개념)

  • 역색인에 들어가는 토큰(또는 keyword 값) 을 term이라고 생각하면 됩니다.
  • keyword 필드의 term은 "원문(정규화 후)" 그 자체
  • text 필드의 term은 analyzer를 거쳐 쪼개진 토큰들

7.2 term query는 "분석을 안 하는 정확일치"

그래서 아래 실수가 매우 흔합니다.

  • titletext인데
  • term으로 "Harry Potter"를 찾으려 한다
  • 하지만 title의 term은 보통 "harry", "potter"로 나뉘어 있음
  • 결과: 매칭이 안 됨

정확일치는 보통 title.raw(keyword)에 term을 씁니다.

예시:

GET books_v2/_search
{
  "query": {
    "term": { "title.raw": "harry potter" }
  }
}

반대로 text에는 match:

GET books_v2/_search
{
  "query": {
    "match": { "title": "Harry Potter" }
  }
}

8) 쿼리 유형별 올바른 사용 가이드(짧고 실전적으로)

  • 정확일치(필터): term/terms + keyword
  • 범위 필터: range + numeric/date
  • 풀텍스트 검색: match/multi_match + text
  • 부분일치(자동완성): edge_ngram(index analyzer) + match(search analyzer) 또는 completion 설계
  • 정렬: keyword(또는 numeric/date), text 정렬은 거의 금지(필요하면 .raw)
  • 집계(aggregation): keyword 또는 numeric/date (text는 fielddata 필요 -> 메모리 위험)

9) Analyzer 설계 예시 3종: 검색/정렬/자동완성

9.1 검색용 analyzer(동의어 + 소문자)

  • match 품질 개선
  • 동의어는 운영에서 변경이 잦아 "reindex 비용"과 직결됩니다.
    • 파일 기반 synonyms/검색 시 확장 등 전략을 분리할 가치가 큼

9.2 정렬/집계용 normalizer(keyword)

  • "Kim", "KIM", "kim"을 같은 버킷으로 묶고 싶을 때
  • lowercase + asciifolding이 흔한 기본값

9.3 자동완성(edge_ngram)

  • 인덱싱 시 edge_ngram으로 접두어 토큰을 만들어 두면 빠른 prefix 매칭이 됩니다.
  • 단점: 인덱스 크기 증가, 품질(노이즈) 튜닝 필요

10) Reindex 운영에서 자주 터지는 함정(그리고 피하는 법)

함정 A) alias 없이 인덱스 이름을 서비스에 박아둠

  • 리인덱스/롤백이 "배포"가 되어버림 -> 장애 시 복구가 느림 -> 처음부터 read/write alias 고정

함정 B) analyzer 변경을 가볍게 봄

  • analyzer 변경 = 대부분 재색인
  • 동의어/토큰화는 품질에 큰 영향 + 회귀가 잦음 -> "동의어 버전/롤백/검증 세트"를 운영 도구로 갖추는 게 좋습니다.

함정 C) text에 집계/정렬 걸어서 메모리 폭발

  • text는 doc_values가 아니라 fielddata로 올라가서 무거움 -> multi-field로 .raw 키워드를 항상 같이 둔다

함정 D) reindex 중 클러스터가 느려짐

  • reindex는 대규모 I/O + merge를 유발 -> 스로틀링, refresh/replica 조절, 운영 윈도우 분리

11) "운영형 Reindex" 체크리스트(실무용)

  • 새 인덱스 이름은 버전으로(index_vN)
  • read/write alias 분리(_read, _write)
  • 매핑/analysis는 템플릿화(복붙 금지)
  • reindex 중 부하 제어(스로틀/배치/refresh/replica)
  • 검증: count / 샘플 쿼리 / 집계 / 정렬 / 회귀 키워드 세트
  • alias swap은 _aliases로 묶어서 실행
  • 롤백 플랜: alias를 v1로 되돌릴 수 있는가?
  • 구 인덱스 삭제는 "검증 후 + 보관 기간 후"

12) 마무리: OpenSearch 운영의 핵심은 "Reindex를 전제로 한 설계"다

OpenSearch에서 성능/품질을 개선하는 대부분의 작업은 결국:

  • 분석기 변경
  • 매핑 개선
  • 인덱스 구조 튜닝

으로 이어지고, 이는 Reindex를 요구합니다.

그래서 운영에서 중요한 건 "Reindex를 어떻게 하는가?"가 아니라:

Reindex가 언제든 가능하게 인덱스를 설계했는가?