17 6월 2013 엔지니어링

중단 시간 없이 매핑 변경

By Clinton Gormley

2015년 11월 2일 추가: Elasticsearch 2.0 버전에서의 매핑 소개 내용을 반드시 확인하세요.

제가 아는 개발자가 이런 트윗을 보냈습니다.

제가 Elasticsearch 모델을 사용할 때 느끼는 가장 큰 문제점은 스키마를 바꿀 때마다 새로 인덱싱을 해야 한다는 점입니다. 데이터 세트의 크기 때문에 시간이 오래 걸려 중단 시간이 너무 길어집니다. 대부분의 애플리케이션에서 지나치게 많은 시간이 소요됩니다.

중단 시간 없이 스키마와 매핑의 변경이 가능합니다. 트윗으로 설명하기에는 너무 많은 옵션이 있기 때문에 블로그 포스트로 대신 합니다.

문제 — 매핑을 변경할 수 없는 이유

사용자는 인덱스에 저장된 것만 찾을 수 있습니다. 데이터가 검색이 가능하도록 하려면 데이터베이스의 각 필드에 포함되어 있는 데이터의 타입과 어떤 형태로 인덱싱이 되는 지를 알아야 합니다. 예를 들어 필드 타입을 문자열에서 날짜 형태로 전환하면 이미 인덱싱되어 있는 해당 필드의 모든 데이터가 무용지물이 됩니다. 어찌 되었든 간에 해당 필드는 다시 인덱싱을 해야 합니다.

이는 Elasticsearch 뿐만 아니라 검색을 위해 인덱스를 사용하는 모든 데이터베이스에 적용됩니다. 그리고 만약에 인덱스를 사용하지 않는 경우, 유연성은 높아지겠지만 속도가 떨어지는 문제가있습니다.

Elasticsearch(및 Lucene)는 자신의 인덱스를 변경이 불가능한 세그먼트에 저장합니다 — 각각의 세그먼트는 "미니" 역 인덱스입니다. 이 세그먼트들은 적소에 있는 동안에는 절대 업데이트되지 않습니다. 도큐먼트를 업데이트하는 것은 실제로는 새로운 도큐먼트를 생성한 뒤 기존 도큐먼트는 삭제된 것으로 표시하는 작업입니다. 더 많은 도큐먼트를 추가(또는 기존 도큐먼트를 업데이트) 하게 되면 새 세그먼트가 생성됩니다. 백그라운드에서 실행되는 병합 프로세스에서 여러 개의 작은 세그먼트가 새로운 큰 세그먼트로 병합된 후 기존 세그먼트가 완전히 삭제됩니다.

일반적으로 Elasticsearch의 인덱스는 서로 다른 타입의 도큐먼트들을 포함합니다. 모든 타입은 각자의 스키마나 매핑을 가집니다. 하나의 세그먼트에는 여러 타입의 도큐머트들이 포함되었을 수 있습니다. 따라서 한 유형의 단일 필드에 대한 필드 정의를 변경하려는 경우, 옵션은 거의 없지만 인덱스에 포함된 모든 도큐먼트들을 다시 인덱싱해야 합니다.

자유로운 필드 추가

세그먼트에는 해당 세그먼트에 대해 문서에 실제로 존재하는 필드에 대한 인덱스만 포함되어 있습니다. 즉, _mapping API를 put 하여 자유롭게 새 필드를 추가할 수 있습니다. 이 작업에는 다시 인덱싱할 필요가 없습니다.

데이터 재인덱싱

데이터 재인덱싱 프로세스는 상당히 단순합니다. 먼저 새 인덱스를 만들고 매핑과 기타 설정을 합니다.

curl -XPUT localhost:9200/new_index -d '
{
    "mappings": {
        "my_type": { ... new mapping definition ...}
    }
}
'

그런 다음, scrolled search 를 사용하여 기존 인덱스에서 도큐먼트들을 끌어오고 벌크 API를 사용하여 새로 인덱싱합니다. 다수의 클라이언트 API는 모든 프로세스를 알아서 처리하는 reindex() 메서드를 제공합니다. 이 작업이 완료되면 기존 인덱스를 삭제할 수 있습니다.

<참고: 검색 요청에 search_type=scan 이 포함되었는지 확인하십시오. 이렇게 하면 정렬이 비활성화되고 "딥 페이징" 효율이 높아집니다.

이 방법의 문제는 인덱스 이름이 변경되기 때문에 새로운 인덱스 이름을 사용하도록 애플리케이션을 변경해야 한다는 점입니다.

중단 시간 없이 데이터 재인덱싱

인덱스 별칭(aliases)을 활용하면 유연하게 백그라운드에서 데이터를 다시 인덱싱할 수 있어, 애플리케이션의 변경을 투명하게 할 수 있습니다. 별칭(alias) 은 하나 이상의 실제 인덱스를 가리킬 수 있는 바로가기 링크 같은 것입니다.

일반적은 작업 흐름은 다음과 같습니다. 먼저, 인덱스를 만들고 이름 뒤에 버전이나 타임스탬프를 붙입니다.

curl -XPUT localhost:9200/my_index_v1 -d '
{ ... mappings ... }
'

이제 애플리케이션이 my_index를 실제 인덱스처럼 읽을 수 있습니다.

데이터를 다시 인덱싱해야 하는 경우에는 끝에 새 버전 번호를 붙여서 새 인덱스를 만들 수 있습니다.

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index",
            "index": "my_index_v1"
        }}
    ]
}
'

데이터를 my_index_v1에서 새로운 my_index_v2로 다시 인덱싱한 다음, 새 인덱스를 가리키기 위해 간단한 단계를 거쳐 myindex 별칭을 변경합니다.

그리고 마지막으로 기존 인덱스를 삭제합니다.

curl -XPUT localhost:9200/my_index_v2 -d '
{ ... mappings ... }
'

그리고 마지막으로 기존 인덱스를 삭제합니다.

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index",
            "index": "my_index_v1"
        }},
        { "add": {
            "alias": "my_index",
            "index": "my_index_v2"
        }}
    ]
}
'

이제 중단 시간 없이 백그라운드에서 모든 데이터가 다시 인덱싱되었습니다. 다행히 애플리케이션은 인덱스가 변경되었다는 사실을 인식하지 못합니다.

이 방법은 스키마 변경을 제어하는 일반적인 방법이지만 그 외에 다른 많은 옵션들도 제공되며, 아래에서 그에 대해 알아볼 것입니다.

기존 데이터는 신경쓰지 않는 경우

단일 필드에 대한 데이터 타입을 변경하려고 할 때 기존 데이터가 검색되지 않아도 괜찮다면 어떻게 해야 할까요? 이 경우, 몇 가지 옵션이 있습니다.

매핑 삭제

Update November 2, 2015: Please note that delete mappings are not supported in Elasticsearch 2.0+.

특정 타입에 대한 매핑 을 삭제한 경우에는 _mapping API를 put 하여 기존 인덱스에서 해당 타입에 대한 새 매핑을 만들 수 있습니다.

참고: 한 타입에 대한 매핑을 삭제하면 인덱스에서 해당 타입의 도큐먼트들도 모두 삭제됩니다.

이는 특히 소수의 도큐먼트가 포함된 타입에 대한 매핑을 변경하고자 할 때 유용합니다.

필드 이름 바꾸기

새 필드를 자유롭게 추가할 수 있으므로 나중에 모든 도큐먼트에 사용할 수 있도록 간단히 다른 이름과 정의를 갖는 새 필드를 추가할 수 있습니다. 물론 애플리케이션이 필드 이름 또한 변경해야 합니다.

다중 필드(multi-field)로 업그레이드

다중 필드(multi-field) 를 사용하면 하나의 필드를 다양한 목적으로 사용할 수 있습니다. 일반적인 사용 방법은 하나의 필드를 질의를 위한 analyzed, 그리고 정렬을 위한 not_analyzed 와 같은 두 가지 방식으로 인덱싱 하는 것입니다.

스칼라 필드(객체-object 형 또는 중첩-nested 형 을 제외한 타입의 필드)는 다시 인덱싱하지 않고 _mapping API를 put 하여 다중 필드로 업그레이드할 수 있습니다. 예를 들어 현재 매핑이 문자열로 되어 있는 필드가 호출되어 생성된 경우:

{
    "created": { "type": "string"}
}

이 코드를 다중 필드로 업그레이드하고 날짜 형식의 하위 필드(sub-field)를 추가할 수 있습니다.

curl -XPUT localhost:9200/my_index/my_type/_mapping -d '
{
    "my_type": {
        "properties": {
            "created": {
                "type":   "multi_field",
                "fields": {
                    "created": { "type": "string" },
                    "date":    { "type": "date"   }
                }
            }
        }
    }
}
'

기존의 created 필드는 "main" 하위 필드로 존재하게 되며 created 또는 created.created 로 질의가 가능합니다. 새로운 날짜 형식은 created.date 로 질의가 가능하며 새 도큐먼트를 입력한 경우에만 채워지게 됩니다.

별칭(alias) 사용을 통한 유연성 향상

때로 위와 같은 방식으로는 부족할 때가 있습니다. 간혹 당신의 애플리케이션에는 100,000개 이상의 사용자 도큐먼트와 10,000,000 이상의 블로그 도큐먼트가 있는 경우가 있을 것입니다. 만약 이 경우에 당신은 사용자 도큐먼트에 대한 매핑을 바꾸기를 원하지만 블로그 도큐먼트를 새로 인덱싱 하고 싶지는 않을 것입니다.

좋은 방법 한가지는 이 두가지 타입을 서로 다른 인덱스에 저장을 하는 것 입니다. Elasticsearch는 단일 인덱스에서 검색하는 것만큼이나 쉽게 다중 인덱스를 동시에 검색할 수 있습니다. 이와 같은 방법으로, 변경하고자 하는 타입이 포함된 인덱스만 다시 인덱싱하면 됩니다. 이렇게 별칭을 신중하게 사용하면, 재 인덱싱 프로세스를 완전히 애플리케이션에 투명하게 하는 것이 가능합니다.

이 접근법을 사용하려면 애플리케이션이 각 타입마다 별도의 별칭을 사용해야 합니다. 예를 들어 사용자 도큐먼트는 my_index로 인덱싱하는 대신 my_index_user로, 블로그 도큐먼트는 tomy_index_blog로 인덱싱하는 것이 좋습니다.

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_blog",
            "index": "my_index_v2"
        }}
    ]
}
'

사용자 도큐먼트와 블로그 도큐먼트를 동시에 검색하려면 두 별칭(alias)을 함께 지정하기만 하면 됩니다.

curl localhost:9200/my_index_blog,my_index_user/_search

사용자 매핑을 변경하려면 먼저 사용자만을 위한 새 인덱스를 만들고 사용자 도큐먼트만을 위한 올바른 샤드 수를 지정합니다.

curl -XPUT localhost:9200/my_index_users_v1 -d '
{
    "settings": {
        "index": {
            "number_of_shards": 1
        }
    },
    "mappings": {
        "user": { ... new user mapping ... }
    }
}
'

사용자 도큐먼트들을 기존 인덱스에서 새 인덱스로 다시 인덱싱합니다.

curl 'localhost:9200/my_index_user/user?scroll=1m&search_type=scan' -d '
{
    "size": 1000
}
'

그리고 별칭을 업데이트합니다.

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "remove": {
            "alias": "my_index_user",
            "index": "my_index_v2"
        }},
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }}
    ]
}
'

delete-by-query 요청을 사용하여 기존 인덱스에서 사용자 도큐먼트들을 제거할 수 있습니다.

curl -XDELETE localhost:9200/my_index_v1/user

이제부터 사용자 도큐먼트에 대한 매핑을 변경하려는 경우 언제든지 위에서 설명한 방법대로 일반적인 재인덱싱을 할 수 있습니다.

재인덱싱 없이 별칭 사용

만약에 변경 사항을 새 도큐먼트에만 적용하려는 경우라면, 재인덱싱할 필요 없이 별칭 접근법을 사용할 수 있습니다. 여전히 새 my_index_user_v1 인덱스를 생성하는데, 이번에는 인덱싱을 위한 my_index_user, 검색을 위한 my_index_users(복수형) 2개의 별칭을 생성합니다.

curl -XPOST localhost:9200/_aliases -d '
{
    "actions": [
        { "add": {
            "alias": "my_index_user",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_user_v1"
        }},
        { "add": {
            "alias": "my_index_users",
            "index": "my_index_v1"
        }},
    ]
}
'

my_index_user 별칭은 새 인덱스를 가리키며 모든 새 사용자 도큐먼트는 이 별칭을 사용하여 인덱싱됩니다. my_index_users 별칭은 새 인덱스와 기존 인덱스를 모두 가리킵니다. 따라서 새 인덱스와 기존 인덱스를 동시에 검색할 수 있습니다. 기존 인덱스는 기존 매핑을 사용하며 새 인덱스는 새 매핑을 사용하게 됩니다.

이와 같이, Elasticsearch는 미리 약간만 준비하면 되는 풍부한 인덱스 관리 옵션을 제공하며 중단 시간 없이 변경 사항을 관리할 수 있습니다.