벌크 API

벌크 API (Bulk API)

여러 개의 인덱싱, 업데이트, 삭제 작업을 한 번에 처리하고 싶으시죠? 벌크 API가 단일 요청으로 여러 인덱싱, 업데이트, 삭제 작업을 수행해요. 네트워크 왕복과 처리 사이클 수를 최소화해 오버헤드를 크게 줄이고 인덱싱 속도를 대폭 높여요.

출처: 문서

본문

1.0에서 도입

벌크 API는 단일 요청으로 여러 인덱싱, 업데이트, 삭제 작업을 수행해요. 이는 네트워크 왕복과 처리 사이클 수를 최소화해 오버헤드를 크게 줄이고 인덱싱 속도를 크게 높여요.

벌크 API는 요청 본문에 개행으로 구분된 JSON(NDJSON) 구조를 사용해요. 각 작업은 한 줄에 지정되고, 작업에 소스 데이터가 필요하면(index, create, update 연산처럼) 소스 데이터는 다음 줄에 제공돼요. 이 형식 덕분에 OpenSearch는 요청 본문 전체를 메모리에 읽지 않고도 작업을 빠르게 파싱하고 처리할 수 있어요.

OpenSearch 2.9부터 벌크 연산으로 문서를 인덱싱할 때 문서 _id는 512바이트 이하여야 해요.

벌크 API를 다음 용도로 사용해요.

  • 여러 문서 연산을 단일 요청으로 묶어 대규모 데이터셋을 효율적으로 인덱싱해요.
  • 여러 문서에 대해 혼합 작업(index, create, update, delete)을 동시에 수행해요.
  • 많은 문서 연산을 수행할 때 네트워크 오버헤드를 최소화해요.
  • 클라이언트 벌크 헬퍼를 사용해 한 인덱스에서 다른 인덱스로 데이터를 재인덱싱해요.

벌크 API는 작업을 독립적으로 처리해요. 작업 하나가 실패해도 OpenSearch는 후속 작업을 계속 처리해요. 응답은 각 개별 작업이 성공했는지 실패했는지 나타내요.

엔드포인트

POST /_bulk
PUT  /_bulk
POST /{index}/_bulk
PUT  /{index}/_bulk

대상 인덱스를 경로에 지정하거나 요청 본문에 포함할 수 있어요.

OpenSearch는 _bulk 엔드포인트에 PUT 요청도 받지만 POST 사용을 강력히 권장해요. PUT의 전형적인 의미—특정 경로에서 단일 리소스 생성 또는 대체—는 벌크 연산의 동작과 맞지 않아요.

경로 파라미터

다음 표는 사용 가능한 경로 파라미터예요. 모든 경로 파라미터는 선택적이에요.

Parameter Data type Description
index String 벌크 작업을 수행할 데이터 스트림, 인덱스 또는 인덱스 alias의 이름이에요.

쿼리 파라미터

다음 표는 사용 가능한 쿼리 파라미터예요. 모든 쿼리 파라미터는 선택적이에요.

Parameter Data type Description
_source Boolean or List or String _source 필드를 반환할지(true/false) 또는 반환할 필드 목록을 지정해요.
_source_excludes List or String 응답에서 제외할 소스 필드의 쉼표로 구분된 목록이에요.
_source_includes List or String 응답에 포함할 소스 필드의 쉼표로 구분된 목록이에요.
index String 벌크 작업을 수행할 데이터 스트림, 인덱스 또는 인덱스 alias의 이름이에요.
pipeline String 들어오는 문서를 전처리하는 데 사용할 파이프라인의 ID예요. 인덱스에 기본 ingest 파이프라인이 지정된 경우, 이 값을 _none으로 설정하면 이 요청에 대해 기본 ingest 파이프라인이 비활성화돼요. final 파이프라인이 구성된 경우 이 파라미터 값과 무관하게 항상 실행돼요.
refresh Boolean or String true면 OpenSearch가 영향을 받은 샤드를 refresh해 이 연산을 검색에 표시하고, wait_for면 refresh를 기다려 이 연산을 검색에 표시하며, false면 refresh 관련 작업을 하지 않아요. 유효한 값: true, false, wait_for.
require_alias Boolean true면 요청의 작업이 인덱스 alias를 대상으로 해야 해요. (기본값: false)
routing String 작업을 특정 샤드로 라우팅하는 데 사용되는 사용자 정의 값이에요.
timeout String 각 작업이 다음 연산을 기다리는 기간이에요: 자동 인덱스 생성, 동적 매핑 업데이트, 활성 샤드 대기.
type String 유형을 제공하지 않은 항목의 기본 문서 유형이에요.
wait_for_active_shards Integer or String or NULL or String 연산을 진행하기 전에 활성 상태여야 하는 샤드 복사본 수예요. all 또는 인덱스의 총 샤드 수까지의 양의 정수(number_of_replicas+1)로 설정해요. 유효한 값은 다음과 같아요. - all: 모든 샤드가 활성화될 때까지 기다려요.

cURL로 벌크 요청 제출하기

cURL 명령으로 파일에서 벌크 요청을 제출할 때 별표 문자를 보존하려면 -d 대신 --data-binary 플래그를 사용해요. -d 플래그는 개행을 제거해 벌크 API에 필요한 NDJSON 형식을 깨뜨려요.

다음 예시는 올바른 방법을 보여줘요. 먼저 벌크 작업이 포함된 파일을 만들어요.

cat > bulk-operations.ndjson << 'EOF'
{ "index": { "_index": "movies", "_id": "curl-test1" } }
{ "title": "Curl Test Movie 1", "year": 2025 }
{ "index": { "_index": "movies", "_id": "curl-test2" } }
{ "title": "Curl Test Movie 2", "year": 2025 }
EOF

그런 다음 --data-binary로 파일을 제출해요.

curl -H "Content-Type: application/x-ndjson" -X POST "localhost:9200/_bulk?pretty" --data-binary "@bulk-operations.ndjson"

cURL로 인라인 벌크 요청을 만들 때는 셸에서 개행을 올바르게 이스케이프해야 해요. 작은따옴표와 실제 개행을 사용하는 것이 \n 이스케이프 시퀀스보다 명확한 경우가 많아요.

낙관적 동시성 제어 (Optimistic concurrency control)

OpenSearch는 여러 프로세스가 동시에 같은 문서를 수정하려고 할 때 충돌을 방지하기 위해 낙관적 동시성 제어를 사용해요. 벌크 API는 시퀀스 번호 기반과 버전 기반 두 가지 동시성 제어 방식을 지원해요.

시퀀스 번호 기반 동시성 제어 (Sequence number-based concurrency control)

시퀀스 번호는 가장 안정적인 동시성 제어 방법을 제공해요. 각 문서 연산은 _seq_no 필드를 증가시키고, _primary_term은 primary 샤드 선거를 추적해요. 벌크 연산에서 두 값을 모두 지정하면, 문서를 마지막으로 읽은 이후 변경되지 않은 경우에만 연산이 성공하도록 보장해요.

다음 예시는 시퀀스 번호 기반 동시성 제어를 보여줘요.

{ "index": { "_index": "movies", "_id": "version-test", "if_seq_no": 13, "if_primary_term": 1 } }
{ "title": "Updated with OCC", "year": 2025 }

다른 프로세스가 읽기와 업데이트 사이에 문서를 수정했다면 _seq_no나 _primary_term이 변경됐을 것이고, OpenSearch는 version_conflict_engine_exception 오류를 반환해요. 애플리케이션은 문서의 최신 버전을 가져와 연산을 재시도할 수 있어요.

이 접근 방식은 명시적 잠금 없이 손실된 업데이트를 방지해, 여러 프로세스가 데이터 일관성을 유지하면서 동시에 작업할 수 있게 해줘요.

버전 기반 동시성 제어 (Version-based concurrency control)

OpenSearch는 동시성 제어를 위한 명시적 버전 번호도 지원해요. 모든 문서에는 수정될 때마다 증가하는 _version 필드가 있어요. 벌크 연산에서 필수 버전을 지정할 수 있어요.

{ "index": { "_index": "movies", "_id": "doc1", "version": 5, "version_type": "internal" } }
{ "title": "Version-controlled update", "year": 2025 }

이 연산은 문서의 현재 버전이 5인 경우에만 성공해요. version_type 파라미터는 다음 값을 지원해요.

  • internal(기본값): OpenSearch의 내부 버전 번호를 사용해요.
  • external: 외부 시스템의 버전 번호를 유지할 수 있게 해줘요. 버전이 현재 버전보다 커야 해요.
  • external_gte: external과 유사하지만 버전이 현재 버전보다 크거나 같을 수 있어요.

버전 관리 (Versioning)

OpenSearch의 문서 버전 관리는 시간에 따른 문서 변경을 추적해요. _version 필드는 문서가 index, update, delete 연산으로 수정될 때마다 자동으로 증가해요.

내부 버전 관리 (Internal versioning)

기본적으로 OpenSearch는 내부 버전 관리를 사용해 새 문서에서 1부터 시작해 수정될 때마다 증가해요. 내부 버전은 자동으로 관리되며, 문서가 삭제되고 같은 ID로 다시 생성돼도 유지돼요.

다음 예시는 외부 버전 번호로 문서를 만들어요.

{ "index": { "_index": "movies", "_id": "external-version-test", "version": 100, "version_type": "external" } }
{ "title": "External Version Movie", "year": 2025 }

외부 버전 관리 (External versioning)

외부 버전 관리는 OpenSearch를 자체 버전 번호를 유지하는 외부 데이터 소스와 동기화할 때 유용해요. version_type: external을 사용하면 OpenSearch가 사용자의 버전 번호를 받아들이고, 제공된 버전이 저장된 버전보다 클 때만 문서를 인덱싱해요. 이렇게 하면 순서가 어긋난 업데이트가 더 오래된 버전으로 더 새로운 데이터를 덮어쓰지 않도록 보장해요.

버전 충돌 (Version conflicts)

버전 충돌은 연산이 현재 문서 버전과 일치하지 않는 버전을 지정할 때 발생해요. 이 경우 OpenSearch는 해당 특정 연산에 대해 응답에서 version_conflict_engine_exception 오류를 반환해요. 벌크 요청은 다른 연산 처리를 계속해서, 일부 연산이 버전 충돌로 실패해도 부분적인 성공이 가능해요.

라우팅 (Routing)

라우팅은 특정 문서를 저장할 샤드를 결정해요. 기본적으로 OpenSearch는 문서 ID의 해시를 사용해 문서를 라우팅하며, 샤드에 걸쳐 문서를 고르게 분산해요. 사용자 정의 라우팅으로 이 동작을 재정의하고 문서 위치를 제어할 수 있어요.

라우팅은 쿼리 파라미터 수준이나 개별 작업 메타데이터에서 두 가지 방식으로 지정할 수 있어요.

쿼리 파라미터 라우팅 (Query parameter routing)

쿼리 파라미터 수준에서 라우팅을 적용하면 벌크 요청의 모든 연산에 영향을 줘요.

POST /_bulk?routing=user123
{ "index": { "_index": "movies", "_id": "routed-doc" } }
{ "title": "Routed Movie", "user_id": "user123" }

작업 수준 라우팅 (Action-level routing)

작업 메타데이터에서 라우팅을 지정하면 더 세밀한 제어가 가능해요. 같은 벌크 요청의 서로 다른 연산에 서로 다른 라우팅 값을 사용할 수 있어요.

POST /_bulk
{ "index": { "_index": "movies", "_id": "routed-action", "routing": "user456" } }
{ "title": "Action Routed Movie", "user_id": "user456" }

사용자 정의 라우팅은 특정 테넌트의 모든 문서를 같은 샤드에 저장하려는 다중 테넌트 애플리케이션에서 특히 유용해요. OpenSearch가 인덱스의 모든 샤드 대신 하나의 샤드만 쿼리하면 되기 때문에 단일 테넌트 내 검색 시 쿼리 성능이 향상돼요.

사용자 정의 라우팅을 사용할 때는 문서에 대한 모든 연산(index, get, update, delete)에 같은 라우팅 값을 제공해야 해요. 그렇지 않으면 OpenSearch가 잘못된 샤드를 검색해 문서를 찾지 못할 수 있어요.

Refresh

refresh 파라미터는 벌크 연산으로 만든 변경 사항이 검색 쿼리에 언제 표시되는지 제어해요. OpenSearch는 문서가 인덱싱 직후 검색할 수 없는 근실시간(near-real-time) 검색 모델을 사용해요.

refresh 파라미터는 세 가지 값을 받아요.

  • false(기본값): 문서가 즉시 refresh되지 않아요. 인덱스의 refresh 간격(보통 1초)에 따라 검색 가능해져요.
  • true: 영향받은 모든 샤드를 즉시 refresh해 문서를 즉시 검색 가능하게 하지만 성능 비용이 있어요.
  • wait_for: 반환하기 전에 다음 예약된 refresh를 기다려 가시성과 성능의 균형을 맞춰요.

다음 예시는 refresh=wait_for를 사용해요.

POST /_bulk?refresh=wait_for
{ "index": { "_index": "movies", "_id": "refresh-test" } }
{ "title": "Refresh Test Movie", "year": 2025 }

벌크 요청에서 문서를 받은 샤드만 refresh돼요. 벌크 요청에 5개 샤드 인덱스의 3개 샤드로 라우팅된 문서가 있다면 그 3개 샤드만 refresh되고 나머지 2개는 영향받지 않아요.

refresh=true를 사용하면 특히 잦은 벌크 요청에서 클러스터 성능에 큰 영향을 줄 수 있어요. 프로덕션 작업에서는 기본 동작이나 refresh=wait_for를 사용하는 것이 좋은데, 더 나은 성능 특성을 제공하면서 문서가 제한된 시간 내에 검색에 사용 가능하게 보장해요.

활성 샤드 대기 (Wait for active shards)

wait_for_active_shards 파라미터는 OpenSearch가 벌크 요청을 처리하기 전에 활성 상태여야 하는 샤드 복사본 수를 제어해요. 너무 많은 샤드 복사본을 사용할 수 없을 때 연산이 진행되는 것을 방지하는 데이터 내구성에 도움이 돼요.

기본적으로 wait_for_active_shards는 1로 설정돼서 primary 샤드만 활성이면 돼요. 다음과 같이 설정할 수 있어요.

  • 양의 정수: 해당 수의 샤드 복사본(primary 포함)이 활성화될 때까지 연산이 기다려요.
  • all: 모든 샤드 복사본(primary와 모든 replica)이 활성화될 때까지 연산이 기다려요.

다음 예시는 primary와 replica 샤드 하나가 활성화되기를 기다려요.

POST /_bulk?wait_for_active_shards=2
{ "index": { "_index": "movies", "_id": "active-shards-test" } }
{ "title": "Active Shards Test", "year": 2025 }

하나의 primary와 두 개의 replica(number_of_replicas=2)로 구성된 인덱스에서 wait_for_active_shards=2로 설정하면 primary와 최소한 하나의 replica가 활성이어야 해요. 이는 가용성과 내구성 사이의 균형을 제공해요.

필요한 활성 샤드 수를 타임아웃 기간(timeout 파라미터로 제어) 내에 사용할 수 없으면 벌크 연산은 타임아웃 오류로 실패해요. 활성이 된 샤드에는 성공적으로 인덱싱된 문서가 여전히 포함될 수 있어요.

성능 고려 사항 (Performance considerations)

벌크 API를 사용할 때 여러 요소가 성능과 처리량에 영향을 줘요. 이러한 점을 이해하면 작업 부하에 맞게 벌크 연산을 최적화하는 데 도움이 돼요.

최적 배치 크기 (Optimal batch size)

단일 벌크 요청에 포함할 보편적으로 "올바른" 연산 수는 없어요. 최적의 배치 크기는 여러 요소에 따라 달라져요.

  • 문서 크기: 문서가 클수록 이상적인 요청 크기에 도달하려면 요청당 더 적은 연산이 필요해요.
  • 인덱싱 복잡성: 필드가 많거나 복잡한 매핑을 가진 문서는 처리하는 데 더 오래 걸려요.
  • 하드웨어 리소스: 클러스터 노드의 사용 가능한 메모리와 CPU 용량이에요.
  • 네트워크 대역폭: 클라이언트와 OpenSearch 클러스터 사이의 연결 속도예요.

1,0005,000개 연산의 배치로 시작해 다양한 크기로 실험해 보세요. 작업 부하에 맞는 최적의 배치 크기를 찾으려면 클러스터의 성능 지표(CPU 사용량, 메모리 소비, 인덱싱 지연)를 모니터링해요. 좋은 벌크 요청 크기는 보통 5MB15MB 사이예요.

HTTP 청킹 (HTTP chunking)

HTTP API를 사용할 때 클라이언트가 HTTP 청크(Transfer-Encoding: chunked)를 보내지 않도록 확인해야 해요. HTTP 청킹은 OpenSearch가 청크가 도착할 때 데이터를 증분 처리해야 하므로 요청 본문을 효율적으로 파싱하지 못하게 해요.

대부분의 HTTP 클라이언트는 기본적으로 청킹을 비활성화하지만, 벌크 연산이 느리다면 클라이언트 구성이 chunked transfer encoding을 활성화하지 않는지 확인해 보세요.

클라이언트 측 버퍼링 (Client-side buffering)

벌크 API가 사용하는 NDJSON 형식은 버퍼링을 최소화하도록 설계됐어요. 각 작업과 그 선택적 소스 데이터는 별도의 줄에 나타나므로 OpenSearch는 요청 전체를 메모리에 로드하지 않고 파싱되는 즉시 연산을 처리할 수 있어요.

애플리케이션에서 벌크 연산을 구현할 때:

  • 모든 연산을 먼저 메모리에 누적하지 마세요. 대신 연산을 생성할 때 벌크 API로 스트리밍하세요.
  • 모든 연산이 완료되기를 기다리는 대신 응답을 증분 처리하세요.
  • 자동으로 일괄 처리와 오류 재시도를 처리하는 효율적인 벌크 헬퍼를 지원하는 클라이언트 라이브러리를 사용하세요.

요청 파싱 (Request parsing)

OpenSearch는 수신 노드에서 작업 메타데이터만 파싱해 벌크 요청 처리를 최적화해요. 작업 메타데이터에는 어떤 샤드가 연산을 처리해야 하는지 결정하는 라우팅 정보가 포함돼요. 라우팅이 결정되면 OpenSearch는 완전한 연산(메타데이터와 소스 데이터)을 적절한 샤드로 전달해요.

이 설계는 조정 노드의 처리를 최소화해, OpenSearch가 진입점에서 병목을 만들지 않고 벌크 연산을 클러스터 전체에 효율적으로 분산할 수 있게 해줘요.

요청 본문 (Request body)

벌크 요청 본문은 개행으로 구분된 JSON(NDJSON) 형식을 사용해요. 각 작업은 단일 줄에 지정되고 그 뒤에 개행 문자(\n)가 와야 하며, 소스 데이터(필요할 때)는 다음 줄에 개행 문자와 함께 와야 해요.

Action and metadata\n
Optional document\n
Action and metadata\n
Optional document\n

각 JSON 문서에는 가독성을 위해 공백을 포함할 수 있지만 단일 줄이어야 해요. OpenSearch는 개행 문자를 사용해 벌크 요청을 파싱하며, 요청 본문이 개행 문자로 끝나야 해요. 벌크 API에 요청을 보낼 때 Content-Type 헤더를 application/x-ndjson으로 설정해야 해요.

작업 메타데이터 필드 (Action metadata fields)

모든 작업은 작업 줄에서 다음 메타데이터 필드를 지원해요. 요청 경로에 인덱스를 지정하지 않는 한 _index 필드는 필수예요.

Field Data type Description
_index String 인덱스 이름이에요. 요청 경로에 지정되지 않으면 필수예요.
_id String 문서 ID예요. 선택적. 제공하지 않으면 OpenSearch가 ID를 자동 생성해요.
_require_alias Boolean true면 대상이 인덱스 alias여야 해요. 기본값은 false예요.
routing String 문서 연산의 사용자 정의 라우팅 값이에요.
version Integer 문서의 명시적 버전 번호예요. 낙관적 동시성 제어에 사용돼요.
version_type String 버전 유형: internal, external, external_gte. 기본값은 internal이에요.
if_seq_no Integer 문서가 이 시퀀스 번호를 가질 때만 연산을 수행해요. 낙관적 동시성 제어에 사용돼요.
if_primary_term Integer 문서가 이 primary term을 가질 때만 연산을 수행해요. 낙관적 동시성 제어에 사용돼요.

작업 (Actions)

벌크 API는 다음 작업을 지원해요.

Create

문서가 아직 존재하지 않으면 생성하고 존재하면 오류를 반환해요. 다음 줄에 JSON 문서가 포함되어야 해요.

{ "create": { "_index": "movies", "_id": "tt1392214" } }
{ "title": "Prisoners", "year": 2013 }
Delete

이 작업은 문서가 존재하면 삭제해요. 문서가 존재하지 않으면 OpenSearch는 오류를 반환하지 않고 result 아래 not_found를 반환해요. delete 작업은 다음 줄에 문서가 필요하지 않아요.

{ "delete": { "_index": "movies", "_id": "tt2229499" } }
Index

Index 작업은 문서가 아직 없으면 생성하고 이미 있으면 대체해요. 다음 줄에 JSON 문서가 포함되어야 해요.

{ "index": { "_index": "movies", "_id": "tt1979320" } }
{ "title": "Rush", "year": 2013}
Update

기본적으로 이 작업은 기존 문서를 업데이트하고 문서가 없으면 오류를 반환해요. 문서를 얼마나 업데이트할지에 따라 다음 줄에 전체 또는 부분 JSON 문서가 포함되어야 해요.

{ "update": { "_index": "movies", "_id": "tt0816711" } }
{ "doc" : { "title": "World War Z" } }

update 작업은 작업 메타데이터에서 retry_on_conflict 필드를 지원해요. 이 필드는 버전 충돌이 발생하면 업데이트를 몇 번 재시도할지 지정해요.

{ "update": { "_index": "movies", "_id": "tt0816711", "retry_on_conflict": 3 } }
{ "doc" : { "title": "World War Z" } }

사용자 정의 ingest 파이프라인은 update 연산에 실행되지 않아요. 문서를 ingest 파이프라인으로 처리해야 한다면 upsert 연산을 대신 사용해요.

Upsert

문서를 upsert하려면 다음 옵션 중 하나를 사용해요.

  • doc 필드에 문서를 지정하고 doc_as_upsert=true로 설정해요. 문서가 존재하면 doc 필드 내용으로 업데이트돼요. 문서가 존재하지 않으면 doc 필드에 지정된 파라미터로 새 문서가 인덱싱돼요:
    { "update": { "_index": "movies", "_id": "tt0816711" } }
    { "doc" : { "title": "World War Z" }, "doc_as_upsert": true }
    
  • 업데이트할 문서(존재할 때)를 doc 필드에, 삽입할 문서(존재하지 않을 때)를 upsert 필드에 지정하고 doc_as_upsert는 false로 둡니다:
    { "update": { "_index": "products", "_id": "widget-123" } }
    { "doc": { "stock": 75, "updated_at": "2025-01-15T10:30:00Z" }, "upsert": { "name": "Widget", "price": 39.99, "stock": 100, "created_at": "2025-01-15T10:30:00Z" }}
    

문서가 존재할 때 특정 필드만 업데이트하고, 존재하지 않을 때는 완전한 문서를 삽입하려는 경우 이 옵션을 사용해요.

Upsert 연산은 ingest 파이프라인을 트리거해서, 문서가 인덱싱되거나 업데이트되기 전에 전처리할 수 있게 해줘요.

Script

보다 복잡한 문서 업데이트를 위해 문서의 source 또는 id로 스크립트를 정의할 수 있어요.

{ "update": { "_index": "movies", "_id": "tt0816711" } }
{ "script" : { "source": "ctx._source.title = \"World War Z\"" } }
스크립트 upsert (Scripted upsert)

스크립트로 문서를 업데이트하거나 upsert하는 방법은 다음과 같아요.

  • Script + upsert(scripted_upsert=false, 기본값): 문서가 있으면 script로 업데이트되고, 없으면 스크립트를 실행하지 않고 upsert 필드의 문서가 삽입돼요:
    POST _bulk
    { "update": { "_index": "movies", "_id": "tt0816711" } }
    { "script": { "source": "ctx._source.title = params.title; ctx._source.genre = params.genre;", "params": { "title": "World War Z", "genre": "Action" } }, "upsert": { "title": "World War Z", "genre": "Action", "author": "Tom Smith" } }
    
  • Script + upsert + scripted_upsert=true: 문서가 있으면 script로 업데이트되고, 없으면 스크립트가 upsert 필드에 대해 실행되어 결과 문서가 삽입돼요:
    POST _bulk
    { "update": { "_index": "movies", "_id": "tt0816711" } }
    { "script": { "source": "ctx._source.title = params.title; ctx._source.genre = params.genre;", "params": { "title": "World War Z", "genre": "Action" } }, "scripted_upsert": true }
    

예시: 여러 작업 수행하기

다음 예시 요청은 delete, index, create, update 연산을 포함해 단일 요청으로 여러 문서 연산을 수행해요.

POST /_bulk
{ "delete": { "_index": "movies", "_id": "tt2229499" } }
{ "index": { "_index": "movies", "_id": "tt1979320" } }
{ "title": "Rush", "year": 2013 }
{ "create": { "_index": "movies", "_id": "tt1392214" } }
{ "title": "Prisoners", "year": 2013 }
{ "update": { "_index": "movies", "_id": "tt0816711" } }
{ "doc" : { "title": "World War Z" } }

예시: 경로에 인덱스 지정하기

다음 예시 요청은 요청 경로에 인덱스를 지정해 각 작업 줄에 _index를 포함할 필요를 없앤다.

POST /movies/_bulk
{ "index": { "_id": "tt0468569" } }
{ "title": "The Dark Knight", "year": 2008, "director": "Christopher Nolan" }
{ "index": { "_id": "tt0137523" } }
{ "title": "Fight Club", "year": 1999, "director": "David Fincher" }

예시: upsert 연산 사용하기

다음 예시 요청은 doc_as_upsert를 사용해 문서가 있으면 업데이트하고 없으면 생성해요.

POST /_bulk
{ "update": { "_index": "movies", "_id": "tt0468569" } }
{ "doc": { "rating": 9.0 }, "doc_as_upsert": true }
{ "update": { "_index": "movies", "_id": "tt9999999" } }
{ "doc": { "title": "New Movie", "year": 2024 }, "doc_as_upsert": true }

예시: retry_on_conflict로 버전 충돌 처리하기

다음 예시 요청은 버전 충돌이 발생하면 retry_on_conflict로 업데이트를 자동으로 재시도해요.

POST /_bulk
{ "update": { "_index": "movies", "_id": "tt0468569", "retry_on_conflict": 3 } }
{ "doc": { "rating": 9.5 } }

예시: 오류만 표시하도록 응답 필터링하기

다음 예시 요청은 filter_path 쿼리 파라미터를 사용해 실패한 연산만 반환해요.

POST /_bulk?filter_path=items.*.error
{ "update": { "_index": "movies", "_id": "missing1" } }
{ "doc": { "title": "Error" } }
{ "update": { "_index": "movies", "_id": "missing2" } }
{ "doc": { "title": "Error" } }
{ "update": { "_index": "movies", "_id": "tt0468569" } }
{ "doc": { "title": "Success" } }

예시 응답

벌크 API는 제출된 순서와 동일한 순서로 요청의 각 연산에 대한 정보를 반환해요. 최상위 errors 불리언에 특히 주의하세요. true면 하나 이상의 연산이 실패했고, 개별 항목에서 세부 정보를 확인할 수 있어요.

벌크 API는 일부 연산이 샤드 실패나 다른 오류로 실패해도 항상 완전한 응답을 반환해요. 이 부분 응답 동작은 실패한 연산이 완료되기를 무한정 기다리지 않고 성공적으로 처리된 모든 연산의 결과를 받을 수 있게 보장해요.

다음 예시 응답은 여러 작업을 가진 첫 번째 예시 요청에 해당해요.

{
  "took": 35,
  "errors": false,
  "items": [
    {
      "delete": {
        "_index": "movies",
        "_id": "tt2229499",
        "_version": 1,
        "result": "not_found",
        "_shards": {
          "total": 1,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 1,
        "_primary_term": 1,
        "status": 404
      }
    },
    {
      "index": {
        "_index": "movies",
        "_id": "tt1979320",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 1,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 2,
        "_primary_term": 1,
        "status": 201
      }
    },
    {
      "create": {
        "_index": "movies",
        "_id": "tt1392214",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 1,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 3,
        "_primary_term": 1,
        "status": 201
      }
    },
    {
      "update": {
        "_index": "movies",
        "_id": "tt0816711",
        "_version": 2,
        "result": "updated",
        "_shards": {
          "total": 1,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 4,
        "_primary_term": 1,
        "status": 200
      }
    }
  ]
}

연산이 실패하면 응답에 실패 세부 정보가 있는 error 객체가 포함돼요.

{
  "took": 9,
  "errors": true,
  "items": [
    {
      "update": {
        "_index": "movies",
        "_id": "nonexistent1",
        "status": 404,
        "error": {
          "type": "document_missing_exception",
          "reason": "[nonexistent1]: document missing",
          "index": "movies",
          "shard": "0",
          "index_uuid": "UZdzhOjDQvGihxfS-m_UFA"
        }
      }
    }
  ]
}

응답 본문 필드

다음 표는 모든 응답 본문 필드를 나열해요.

Field Data type Description
took Integer 벌크 요청을 처리하는 데 걸린 시간(밀리초)이에요.
errors Boolean 벌크 요청의 어떤 연산이 실패했는지 여부를 나타내요. true면 오류 세부 정보를 위해 개별 항목을 확인해요.
items Array of objects 제출된 순서대로 벌크 요청의 각 연산 결과를 포함해요.

items 배열 (The items array)

items 배열의 각 객체는 하나의 연산에 해당하며 작업 유형(index, create, update, delete)과 일치하는 키를 포함해요. 값은 다음 필드를 포함하는 객체예요.

Field Data type Description
_index String 연산과 관련된 인덱스의 이름이에요.
_id String 연산과 관련된 문서 ID예요.
_version Integer 연산 후 문서 버전이에요. 문서가 업데이트될 때마다 증가해요. 성공한 연산에만 반환돼요.
result String 연산의 결과예요: created, updated, deleted, not_found. 성공한 연산에만 반환돼요.
_shards Object 연산에 대한 샤드 정보를 포함해요. 성공한 연산에만 반환돼요.
_shards.total Integer 연산이 시도된 샤드 수예요.
_shards.successful Integer 연산이 성공한 샤드 수예요.
_shards.failed Integer 연산이 실패한 샤드 수예요.
_seq_no Integer 연산에 대해 문서에 할당된 시퀀스 번호예요. 낙관적 동시성 제어에 사용돼요. 성공한 연산에만 반환돼요.
_primary_term Integer 연산에 대해 문서에 할당된 primary term이에요. 낙관적 동시성 제어에 사용돼요. 성공한 연산에만 반환돼요.
status Integer 연산의 HTTP 상태 코드예요: 200(업데이트), 201(생성), 404(없음), 409(버전 충돌).
error Object 실패한 연산에 대한 정보를 포함해요. 실패한 연산에만 반환돼요.
error.type String document_missing_exception, version_conflict_engine_exception 같은 실패한 연산의 오류 유형이에요.
error.reason String 연산이 실패한 이유에 대한 사람이 읽을 수 있는 설명이에요.
error.index String 실패한 연산과 관련된 인덱스의 이름이에요.
error.shard String 실패한 연산과 관련된 샤드의 ID예요.
error.index_uuid String 실패한 연산과 관련된 인덱스의 UUID(범용 고유 식별자)예요.

부분 응답 및 샤드 실패 (Partial responses and shard failures)

빠른 응답을 보장하기 위해 벌크 API는 일부 샤드 연산이 실패해도 결과를 반환해요. 벌크 요청의 각 연산은 독립적으로 처리되며, OpenSearch는 다른 연산이 성공했는지 실패했는지와 관계없이 응답에 모든 연산의 결과를 포함해요.

하나 이상의 샤드가 연산 처리에 실패하면 OpenSearch는 나머지 연산 처리를 계속하고 응답에 실패한 연산에 대한 오류 정보를 포함해요. 최상위 errors 필드는 어떤 연산에 오류가 발생했는지 나타내 이 부분을 확인할 필요가 있는지 빠르게 판단하게 해줘요.

샤드 실패는 여러 이유로 발생할 수 있어요.

  • 리소스 부족: 샤드를 호스팅하는 노드의 메모리나 디스크 공간이 부족해요.
  • 네트워크 파티션: 네트워크 문제로 샤드가 일시적으로 도달할 수 없어요.
  • 버전 충돌: 낙관적 동시성 제어가 연산 완료를 방지해요.
  • 매핑 오류: 문서가 인덱스의 매핑 요구 사항을 준수하지 않아요.

애플리케이션은 항상 응답의 errors 필드를 확인하고 실패한 연산을 적절히 처리해야 해요. 사용 사례에 따라 실패한 연산을 재시도하거나, 나중에 검토하기 위해 로그로 남기거나, 운영자에게 근본 원인을 조사하도록 경고할 수 있어요.

보안

Security 플러그인을 사용한다면 벌크 연산을 수행할 적절한 권한이 있는지 확인해야 해요. 필요한 권한은 벌크 요청의 연산에 따라 달라져요.

  • indices:data/write/bulk: 모든 벌크 연산에 필요해요.
  • indices:data/write/index: index와 create 연산에 필요해요.
  • indices:data/write/update: update 연산에 필요해요.
  • indices:data/write/delete: delete 연산에 필요해요.

또한 벌크 요청에 지정된 대상 인덱스에 대한 권한도 필요해요. 인덱스 패턴이나 alias를 사용할 때 보안 역할이 패턴이나 alias가 매칭할 수 있는 모든 인덱스에 접근 권한을 부여하는지 확인해야 해요.

더 알아보기 (Learn more)

  • 대용량 일괄 인덱싱 시 --data-binary와 NDJSON 형식을 지키면 오류를 피할 수 있어요.
  • 병렬 프로세스가 있는 환경에서는 시퀀스 번호 기반 OCC로 데이터 무결성을 지켜요.