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#
- 2026-05-03T18:30:28Z — cron job
Cupix::Cron::Searchable.partial_reindex실행.update_mapping!호출이 전 리전에서 동시 시작, Record 모델에서 즉시 실패 - 2026-05-03T18:30:28Z~18:34:46Z — Record, Editing, Capture, Bim, Integration 5개 모델에서 총 21건의
update_mapping!에러 발생 (이 클러스터는 Recordpriority관련 13건) - 2026-05-04 — Error sweeper 감지 및 RCA 수행
Error Log#
[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_mappingwrapper가 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_reindex가 partial_reindex! → update_mapping → update_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 실행
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! 호출
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-293 — update_mapping 호출 후 stale documents 재인덱싱
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 반환
def update_mapping
update_mapping!
rescue StandardError => e
false
else
true
end
5. Failure point: app/models/concerns/searchable.rb:303-316 — update_mapping!가 put_mapping API를 호출하고 BadRequest 발생 시 에러 로깅 후 re-raise
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 모델의 경우:
indexes 'priority', type: 'integer'
Editing 모델도 동일:
indexes 'priority', type: 'integer'
DB 마이그레이션에서 priority 컬럼은 integer로 추가되었다:
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를 전송하면 기존 long → integer 타입 변경이 거부된다.
Log Evidence#
Datadog에서 사용한 쿼리:
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!:
{
"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건:
service:cupixworks-worker @class:Cupix::Cron::Searchable @function:partial_reindex "Partial reindex begins"
{
"class": "Cupix::Cron::Searchable",
"function": "partial_reindex",
"message": "Partial reindex begins",
"module": "Cupix::Cron"
}
더 넓은 검색으로 총 21건의 매핑 충돌 에러가 확인되었다 — 이 클러스터 외에 다른 모델/필드에서도 동일 패턴:
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)과 정확히 일치한다.
배포 전 동일 에러 이력 검색:
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:148에 every '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:102—priority필드 타입을integer에서long으로 변경하여 기존 인덱스 매핑과 일치시킨다.app/models/concerns/searchable/editing.rb:103의priority와pointclouds_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로 통일하여 코드 정의와 인덱스 매핑이 일치하도록 한다.
장기 개선 (재발 방지)#
Searchableconcern의 매핑 정의에서 Elasticsearch의 dynamic mapping 기본값과 일치하는 타입 컨벤션을 수립한다: 정수형은long(ES 기본값), 문자열 enum은keyword.es:check_integrity_of_mappingsrake task(elasticsearch.rake:84)를 CI/CD 파이프라인에 추가하여 배포 전 매핑 불일치를 감지한다.update_mapping!메서드에 dry-run 모드를 추가하여, 실제put_mapping전에 타입 충돌을 사전 검증하고 충돌 필드만 skip하는 로직을 도입한다.
Monitoring#
- 다음 Datadog 쿼리로 매핑 충돌 에러를 모니터링:
service:cupixworks-worker status:error "illegal_argument_exception" "mapper"
update_mapping!호출 성공/실패 비율에 대한 custom metric 추가 권장partial_reindexcron job 실행 결과를 Datadog에 리포트하는 scheduled monitor 추가 권장 — 매주 일요일 18:30 이후 30분간 에러 유무 확인
Risk Assessment#
- Risk level: low
- 예상 복잡도: standard — 숫자형 필드 타입 수정(
integer→long)은 단순하나, 문자열 필드(text→keyword)는 reindex가 필요하며 데이터 규모에 따라 실행 시간이 달라진다.update_mapping이 실패해도partial_reindex!는 계속 진행되므로 데이터 유실 위험은 없다. 에러는 매주 반복되지만 기능적 영향은 해당 필드의 검색/정렬 기능에 한정된다.