ES /docs

Elasticsearch mapping schema drift — immutable field type mismatch

RCA: Elasticsearch mapping type conflict — mapper [priority] cannot be changed from type [long] to [integer]

Overview#

What Happened#

2026-05-03 18:30 UTC에 매주 일요일 실행되는 cron job Cupix::Cron::Searchable.partial_reindex가 트리거되면서, cupixworks-worker 서비스에서 Elasticsearch put_mapping API 호출이 5개 모델(Record, Editing, Capture, Bim, Integration)에 걸쳐 일괄 실패했다. 총 9개 필드에서 기존 인덱스의 필드 타입과 코드에 정의된 매핑 타입이 불일치하여 Elasticsearch가 HTTP 400 illegal_argument_exception으로 거부했다. 13건의 에러가 전 리전(eu-central-1, ap-southeast-2, us-west-2, ap-northeast-1, ap-southeast-1)에서 동시 발생했다.

Quick Facts#

Field Value
exception.class Elasticsearch::Transport::Transport::Errors::BadRequest
exception.message [400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [priority] cannot be changed from type [long] to [integer]"}]}}
top_frame app/models/concerns/searchable.rb:311
trigger Cupix::Cron::Searchable.partial_reindex (cron: 30 18 * * 0 — 매주 일요일 18:30 UTC)
deploy 20260503T1830Z0 (dev: b9f4f414, stage: 6418d0ea, production: 57c6026d)
env production (eu-central-1, ap-southeast-2, us-west-2, ap-northeast-1, ap-southeast-1), stage, dev

Timeline#

  1. 2026-05-03T18:30:28Z — cron job Cupix::Cron::Searchable.partial_reindex 실행. update_mapping! 호출이 전 리전에서 동시 시작, Record 모델에서 즉시 실패
  2. 2026-05-03T18:30:28Z~18:34:46Z — Record, Editing, Capture, Bim, Integration 5개 모델에서 총 21건의 update_mapping! 에러 발생 (이 클러스터는 Record priority 관련 13건)
  3. 2026-05-04 — Error sweeper 감지 및 RCA 수행

Error Log#

Datadog Logs

text
[400] {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"mapper [priority] cannot be changed from type [long] to [integer]"}],"type":"illegal_argument_exception","reason":"mapper [priority] cannot be changed from type [long] to [integer]"},"status":400}

Impact#

  • Service: cupixworks-worker
  • 발생 횟수: 13 (이 클러스터는 priority 필드만 해당. 같은 cron 실행에서 총 21건 이상의 매핑 충돌 에러 발생)
  • 최초 발생: 2026-05-03T18:30:28.572Z
  • 최근 발생: 2026-05-03T18:34:46.681Z
  • 기능적 영향: update_mapping! 실패 후 update_mapping wrapper가 exception을 삼키고 false를 반환하므로, partial_reindex!는 계속 진행된다. 즉, 데이터 인덱싱 자체는 수행되지만 새로 추가된 필드의 매핑이 기존 인덱스에 반영되지 않는다. priority, pointclouds_count 등의 필드로 정렬/필터링하는 검색 기능이 비정상 동작할 수 있다. 기존 인덱싱된 데이터에는 직접적 영향 없음.
  • 반복 패턴: 이 에러는 매주 일요일 18:30 UTC에 반복 발생한다 — cron schedule이 변경되거나 인덱스 매핑이 수정되지 않는 한 계속 재발한다.

Root Cause Summary#

Elasticsearch 인덱스에 이미 dynamic mapping으로 생성된 priority 필드의 타입(long)과, 코드의 Searchable::Record concern에서 명시적으로 정의한 타입(integer)이 불일치한다. 매주 일요일 18:30 UTC에 실행되는 cron job Cupix::Cron::Searchable.partial_reindexpartial_reindex!update_mappingupdate_mapping!을 호출하여 전체 매핑을 put_mapping API로 전송하면, Elasticsearch가 기존 필드의 타입 변경을 거부하여 HTTP 400 illegal_argument_exception을 반환한다. Elasticsearch는 정수값을 dynamic mapping할 때 기본적으로 long 타입을 사용하지만, Rails 코드에서는 DB 컬럼 타입(integer)에 맞춰 integer로 정의했다. 이 불일치는 인덱스가 최초 생성된 이래 잠재적으로 존재했으며, partial_reindex cron job이 매주 실행될 때마다 반복 발생한다.

Technical Analysis#

Code Path#

1. Cron trigger: config/schedule.rb:148 — 매주 일요일 18:30 UTC에 Cupix::Cron::Searchable.partial_reindex 실행

config/schedule.rb:147-149ruby
every '30 18 * * 0' do # 18:30 every Sunday
  runner 'Cupix::Cron::Searchable.partial_reindex'
end

2. Cron handler: lib/cupix/cron/searchable.rb:3-35 — 5개 모델에 대해 순차적으로 partial_reindex! 호출

lib/cupix/cron/searchable.rb:3-10ruby
def self.partial_reindex
  Cupix::Logger.info('Partial reindex begins', class: self.name, function: __method__, module: 'Cupix::Cron')

  begin
    ::Record.partial_reindex!
  rescue StandardError => e
    Cupix::Logger.error("Partial reindex failed on Record with error: #{e.message}", class: self.name, function: __method__, module: 'Cupix::Cron')
  end

이어서 Level, Capture, Bim, Integration 모델에 대해서도 동일한 패턴으로 호출한다.

3. Partial reindex: app/models/concerns/searchable.rb:271-293update_mapping 호출 후 stale documents 재인덱싱

app/models/concerns/searchable.rb:271-293ruby
def partial_reindex!
  update_mapping

  _indexed_document_count = 0

  stale_documents.find_in_batches.with_index do |group, batch|
    group.each do |model|
      model._index_document
    rescue StandardError
      Cupix::Logger.error("error on indexing stale documents with batch #{batch}", class: self.name, function: __method__)
      next
    else
      _indexed_document_count += 1
    end

    Cupix::Logger.info("document refreshing on #{self.name} with batch #{batch}", class: self.name, function: __method__)
    sleep(1)
  end

  _indexed_document_count
end

여기서 update_mapping (bang 없음)을 호출한다. 이 메서드는 exception을 삼키고 false를 반환하므로 reindex는 계속 진행된다.

4. update_mapping wrapper: app/models/concerns/searchable.rb:295-301 — exception을 삼기고 boolean 반환

app/models/concerns/searchable.rb:295-301ruby
def update_mapping
  update_mapping!
rescue StandardError => e
  false
else
  true
end

5. Failure point: app/models/concerns/searchable.rb:303-316update_mapping!put_mapping API를 호출하고 BadRequest 발생 시 에러 로깅 후 re-raise

app/models/concerns/searchable.rb:303-316ruby
def update_mapping!
  request = {
    index: __elasticsearch__.index_name,
    body: __elasticsearch__.mappings.to_hash
  }

  request.merge!(type: __elasticsearch__.document_type) if __elasticsearch__.document_type

  self.__elasticsearch__.client.indices.put_mapping(request)
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
  Cupix::Logger.error(e.message.to_s, class: self.name, function: __method__)

  raise e
end

__elasticsearch__.mappings.to_hash는 모델의 Searchable concern에 정의된 매핑을 반환한다. Record 모델의 경우:

app/models/concerns/searchable/record.rb:102ruby
indexes 'priority', type: 'integer'

Editing 모델도 동일:

app/models/concerns/searchable/editing.rb:103ruby
indexes 'priority', type: 'integer'

DB 마이그레이션에서 priority 컬럼은 integer로 추가되었다:

db/migrate/20240530044030_add_priority_to_editing.rb:1-6ruby
class AddPriorityToEditing < ActiveRecord::Migration[6.0]
  def change
    add_column :records, :priority, :integer, default: 0
    add_column :editings, :priority, :integer, default: 0
  end
end

그러나 Elasticsearch는 integer 값이 처음 인덱싱될 때 dynamic mapping으로 long 타입을 생성한다. 코드에서 put_mapping으로 integer를 전송하면 기존 longinteger 타입 변경이 거부된다.

Log Evidence#

Datadog에서 사용한 쿼리:

text
service:cupixworks-worker status:error "mapper [priority] cannot be changed"

시간 범위: 2026-05-03T17:30:00Z ~ 2026-05-03T19:00:00Z

6건의 로그가 확인되었으며, 모두 @class:Record, @function:update_mapping!:

json
{
  "class": "Record",
  "function": "update_mapping!",
  "message": "[400] {\"error\":{\"root_cause\":[{\"type\":\"illegal_argument_exception\",\"reason\":\"mapper [priority] cannot be changed from type [long] to [integer]\"}]}}",
  "tenant": "cupix",
  "environment": "production"
}
시각 (UTC) Class Environment Region/Host Tenant
18:30:28.572Z Record production eu-central-1 cupix
18:30:28.656Z Record production ap-southeast-2 cupix
18:30:29.216Z Record production us-west-2 cupix
18:30:32.549Z Record production ap-southeast-2 nswgov
18:30:32.750Z Record stage us-west-2 cupix
18:30:37.750Z Record dev us-west-2 cupix

cron trigger 확인 — 동일 시간대에 partial_reindex 시작 로그 9건:

text
service:cupixworks-worker @class:Cupix::Cron::Searchable @function:partial_reindex "Partial reindex begins"
json
{
  "class": "Cupix::Cron::Searchable",
  "function": "partial_reindex",
  "message": "Partial reindex begins",
  "module": "Cupix::Cron"
}

더 넓은 검색으로 총 21건의 매핑 충돌 에러가 확인되었다 — 이 클러스터 외에 다른 모델/필드에서도 동일 패턴:

text
service:cupixworks-worker status:error "illegal_argument_exception" @function:update_mapping!
Class Field 충돌 건수
Record priority long → integer 6
Editing pointclouds_count long → integer 2
Editing editing_type text → keyword 1
Capture reconstruction_state text → keyword 3
Capture upload_platform text → keyword 2
Capture analysis_state text → keyword 2
Integration provider text → keyword 2
Integration region text → keyword 1
Bim grid_system_state text → keyword 2

모든 에러의 타임스탬프가 cron schedule 30 18 * * 0 (매주 일요일 18:30 UTC)과 정확히 일치한다.

배포 전 동일 에러 이력 검색:

text
service:cupixworks-worker status:error @function:update_mapping! "illegal_argument_exception"

시간 범위: 2026-05-02T00:00:00Z ~ 2026-05-03T18:00:00Z — 0건. 이는 에러가 특정 배포가 아닌 매주 일요일 cron 실행에 의해 트리거됨을 시사한다 (이전 주 일요일 데이터는 14일 retention 내에 있으나, 동일 에러가 반복되고 있었을 가능성이 높다).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Elasticsearch dynamic mapping 타입(long)과 코드 정의 타입(integer) 불일치로 put_mapping 실패 ES는 정수값 dynamic mapping 시 long 사용. searchable/record.rb:102에서 integer로 정의. 에러 메시지 mapper [priority] cannot be changed from type [long] to [integer]가 정확히 이 시나리오를 설명. 9개 필드에서 동일 패턴 (숫자형: long→integer, 문자열형: text→keyword) Confirmed
H2 배포 과정에서 update_mapping!이 트리거됨 배포 버전 20260503T1830Z0의 타임스탬프와 에러 시점이 일치. 배포 시 es:migrate 실행됨 config/schedule.rb:148every '30 18 * * 0' (매주 일요일 18:30 UTC) cron이 정의되어 있고, 2026-05-03은 일요일. Datadog에 Cupix::Cron::Searchable.partial_reindex "Partial reindex begins" 로그 9건이 동일 시각에 확인됨. 배포 시 실행되는 es:migrate에는 최근 마이그레이션 파일이 없음 Rejected
H3 최근 코드 변경으로 매핑 정의가 변경됨 git log --since=2026-04-28 결과 searchable mapping 파일(record.rb, editing.rb, capture.rb 등)에 최근 변경 없음. priority 필드는 commit 3a658144d (TSLA-7254, 2024년)에서 추가된 이후 변경 없음. 이 에러는 인덱스 최초 생성 시점부터 잠재적으로 존재했으며 매주 cron 실행 시 반복 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/models/concerns/searchable/record.rb:102priority 필드 타입을 integer에서 long으로 변경하여 기존 인덱스 매핑과 일치시킨다. app/models/concerns/searchable/editing.rb:103prioritypointclouds_count도 동일하게 long으로 변경한다.
  • 문자열 필드(reconstruction_state, upload_platform, analysis_state, provider, region, grid_system_state, editing_type)는 keyword가 의도된 타입이므로, 코드 변경이 아닌 인덱스 reindex가 필요하다.
  • update_mapping!(searchable.rb:303)에서 BadRequest 발생 시 로그 레벨을 error에서 warn으로 낮추는 것을 고려한다 — 매핑 충돌은 기능 장애가 아니며 매주 반복되는 노이즈를 줄일 수 있다.

단기 개선 (1주 이내)#

  • 영향받는 5개 모델(Record, Editing, Capture, Bim, Integration)에 대해 reindex_on_migration!을 실행하여 올바른 매핑으로 인덱스를 재생성한다. 이 작업은 새 인덱스 생성 → 데이터 복사 → alias 전환 방식으로 수행되어야 하며, 서비스 다운타임 없이 진행할 수 있다.
  • reindex 후에는 숫자형 필드를 long으로, 문자열 enum 필드를 keyword로 통일하여 코드 정의와 인덱스 매핑이 일치하도록 한다.

장기 개선 (재발 방지)#

  • Searchable concern의 매핑 정의에서 Elasticsearch의 dynamic mapping 기본값과 일치하는 타입 컨벤션을 수립한다: 정수형은 long(ES 기본값), 문자열 enum은 keyword.
  • es:check_integrity_of_mappings rake task(elasticsearch.rake:84)를 CI/CD 파이프라인에 추가하여 배포 전 매핑 불일치를 감지한다.
  • update_mapping! 메서드에 dry-run 모드를 추가하여, 실제 put_mapping 전에 타입 충돌을 사전 검증하고 충돌 필드만 skip하는 로직을 도입한다.

Monitoring#

  • 다음 Datadog 쿼리로 매핑 충돌 에러를 모니터링:
text
service:cupixworks-worker status:error "illegal_argument_exception" "mapper"
  • update_mapping! 호출 성공/실패 비율에 대한 custom metric 추가 권장
  • partial_reindex cron job 실행 결과를 Datadog에 리포트하는 scheduled monitor 추가 권장 — 매주 일요일 18:30 이후 30분간 에러 유무 확인

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard — 숫자형 필드 타입 수정(integerlong)은 단순하나, 문자열 필드(textkeyword)는 reindex가 필요하며 데이터 규모에 따라 실행 시간이 달라진다. update_mapping이 실패해도 partial_reindex!는 계속 진행되므로 데이터 유실 위험은 없다. 에러는 매주 반복되지만 기능적 영향은 해당 필드의 검색/정렬 기능에 한정된다.