ElasticsearchError - [400] {"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"fail
RCA: ElasticsearchError - mapper_parsing_exception on geo_point location field
Overview#
What Happened#
2026-05-22 04:0004:43 UTC 사이에 cupixworks-api 서비스에서 ElementTrace를 Elasticsearch에 인덱싱할 때 90)를 초과하여 ES가 문서 인덱싱을 거부한 것이 원인이다.mapper_parsing_exception 에러가 6,356건 발생했다. BIM 좌표계의 center point 값(예: latitude 2139310.163528784)이 유효한 geo_point 위도 범위(-90
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Elasticsearch::Transport::Transport::Errors::BadRequest |
| exception.message | [400] mapper_parsing_exception: failed to parse field [location] of type [geo_point] - illegal latitude value |
| top_frame | app/models/concerns/searchable.rb:116 |
| env | production, us-west-2 |
| deploy | production-us-west-2-20260521T1035Z0-3e770a15-cupixworks |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| clark-vdc (team 87) | ~5000+ | ElementTrace ES 검색 인덱스 미동기화 |
| ellisdon (team 268) | ~1000+ | ElementTrace ES 검색 인덱스 미동기화 |
Timeline#
- 2026-05-22T04:00:01Z — 최초 에러 발생 (facility_key: 3fq7tj, team clark-vdc)
- 2026-05-22T04:04:49Z — 다수 사용자에서 동시 다발적 에러 (facility_key: 3fq7tj, rkuup2)
- 2026-05-22T04:43:07Z — 마지막 에러 기록
- 2026-05-22T05:00:00Z — Error Sweeper 감지
Error Log#
ElasticsearchError - [400] {"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse field [location] of type [geo_point]"}],"type":"mapper_parsing_exception","reason":"failed to parse field [location] of type [geo_point]","caused_by":{"type":"illegal_argument_exception","reason":"illegal latitude value [2139310.163528784] for location"}},"status":400}
Impact#
- Service:
cupixworks-api - 발생 횟수: 6,356
- 최초 발생: 2026-05-22T04:00:01.281Z
- 최근 발생: 2026-05-22T04:43:07.973Z
- 사용자 영향: ElementTrace의 ES 인덱스가 동기화되지 않아 검색/필터링 결과에서 해당 element trace가 누락될 수 있다. 단, API 응답은 200을 반환하므로 사용자에게는 에러가 노출되지 않는다.
Root Cause Summary#
ElementTrace를 ES에 인덱싱할 때 location 필드를 element.bim_center_location에서 계산한다. 이 메서드는 Element의 bim_bounds 배열에서 center point를 산출하고 BIM의 offset과 origin을 더한다. 문제는 이 값들이 BIM 로컬 좌표계(미터 단위)이며, WGS84 위경도가 아니라는 점이다. BIM 좌표값이 그대로 { lat: center_y, lon: center_x } 형태로 ES geo_point에 전달되어, latitude가 -90~90 범위를 벗어나면 ES가 mapper_parsing_exception을 발생시킨다.
Technical Analysis#
Code Path#
- Entry point:
Api::V1::ElementTracesController#bulk→BulkableController#bulk→BulkableRepository#bulk - Bulk update가 DB에 성공적으로 저장된 후,
after_commit콜백에서 ES 인덱싱이 트리거된다. _update_document메서드에서@__changed_model_attributes가 없으면_index_document로 fallback한다._index_document가as_indexed_json을 호출하여ElementTraceSerializer를 통해 문서를 직렬화한다.- Serializer가
element.bim_center_location을 호출하여 location을 계산한다.
1. ES 인덱스 매핑 (location을 geo_point로 정의):
indexes 'location', type: 'geo_point'
2. Serializer에서 location 계산 위임:
attribute :location do |element_trace|
element_trace.element&.bim_center_location
end
3. BIM center location 계산 (실제 문제 지점):
# Return center point of BIM bounds as geo_point for ES geo aggregation
def bim_center_location
return nil if bim_bounds.blank?
return nil unless bim_bounds.is_a?(Array) && bim_bounds.size == 6
x_min, y_min, _z_min, x_max, y_max, _z_max = bim_bounds
center_x = (x_min + x_max) / 2.0
center_y = (y_min + y_max) / 2.0
bim = bim_revision&.bim
if bim.present?
offset = bim.offset || [0, 0, 0]
origin = bim.origin || [0, 0, 0]
center_x += (offset[0] || 0) + (origin[0] || 0)
center_y += (offset[1] || 0) + (origin[1] || 0)
end
{ lat: center_y, lon: center_x }
end
이 메서드는 BIM 좌표를 그대로 lat/lon으로 반환한다. BIM 모델의 좌표가 프로젝트 로컬 좌표계(미터 단위)일 때, center_y 값이 2,139,310과 같은 큰 수가 되어 ES geo_point의 유효 범위를 초과한다.
4. _update_document에서 fallback으로 _index_document 호출:
Cupix::Logger.warn('NotFound - attributes_in_database', class: self.class.name, function: __method__)
_index_document
attributes_in_database가 nil이면 전체 문서를 재인덱싱하며, 이때 invalid location이 포함된다.
5. _index_document에서 에러 catch 및 로깅:
rescue StandardError => e
Cupix::Logger.error("Index error - #{e.message}", class: self.class.name, function: __method__)
BulkIndexWorker.perform_async(self.class.name, [id], 'index')
rescue Elasticsearch::Transport::Transport::Error => e
Cupix::Logger.error("ElasticsearchError - #{e.message}", class: self.class.name, function: __method__)
BulkIndexWorker.perform_async(self.class.name, [id], 'index')
에러가 rescue되어 BulkIndexWorker에 재시도를 위임하지만, 같은 잘못된 데이터로 재시도하므로 무한 실패 루프가 된다.
Log Evidence#
Datadog 쿼리:
service:cupixworks-api status:error @environment:production "mapper_parsing_exception" "location"
로그 패턴 (모든 에러가 동일한 구조):
{
"message": "ElasticsearchError - [400] {\"error\":{\"root_cause\":[{\"type\":\"mapper_parsing_exception\",\"reason\":\"failed to parse field [location] of type [geo_point]\"}],\"type\":\"mapper_parsing_exception\",\"reason\":\"failed to parse field [location] of type [geo_point]\",\"caused_by\":{\"type\":\"illegal_argument_exception\",\"reason\":\"illegal latitude value [2139310.163528784] for location\"}},\"status\":400}",
"class": "ElementTrace",
"function": "_update_document",
"http.method": "PUT",
"http.url": "/api/v1/element_traces",
"controller": "Api::V1::ElementTracesController#bulk",
"bulk.items_count": 2,
"bulk.action": "update",
"bulk.status": "success",
"params.facility_key": "3fq7tj",
"usr.id": 32795,
"team.domain": "clark-vdc",
"http.status_code": 200
}
관측된 잘못된 latitude 값:
2139310.163528784— BIM 로컬 좌표 (미터 단위)2139321.038608871— 같은 프로젝트의 다른 요소2139321.663608871— 같은 프로젝트의 다른 요소2479.503375969592— 다른 프로젝트 (ellisdon team)189.6852867958929— 범위 초과이나 비교적 작은 값
모든 에러에서 HTTP 응답은 200이며 bulk.status: "success" — DB 저장은 성공하지만 ES 인덱싱만 실패한다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | BIM 로컬 좌표가 검증 없이 ES geo_point에 전달됨 | bim_center_location 코드가 center_y를 직접 lat으로 반환 (element.rb:120). 로그에서 latitude 값 2139310은 BIM 미터 좌표와 일치 |
— | Confirmed |
| H2 | API 파라미터에서 잘못된 location이 직접 전달됨 | ElementTrace가 GeoQueriable 미포함, Parameter concern의 location 파싱 코드 존재 | ElementTrace는 respond_to?(:location=) false이므로 Parameter 로직 미적용. Serializer가 element에서 계산 |
Rejected |
| H3 | BIM offset/origin 계산 오류로 좌표 폭증 | offset/origin이 큰 값일 가능성 | 2139310 값은 bim_bounds 자체가 이미 큰 로컬 좌표. offset 없이도 재현 가능 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
app/models/concerns/properties/element.rb:120—bim_center_location메서드에서 반환 직전에 lat/lon 값이 유효한 geo_point 범위인지 검증하고, 범위를 벗어나면nil을 반환하도록 수정- 유효 범위: latitude -90
90, longitude -180180
단기 개선 (1주 이내)#
- BIM 좌표를 WGS84 위경도로 변환하는 로직 도입 검토. BIM 모델의
georef또는 CRS(Coordinate Reference System) 정보가 있다면 이를 활용하여 실제 위경도로 변환 - 변환이 불가능한 경우(CRS 정보 없음), location 필드를 nil로 설정하여 ES 에러를 방지
BulkIndexWorker의 재시도가 같은 invalid 데이터로 무한 반복되는 문제를 방지하기 위해, 재시도 횟수 제한 또는 validation 에러 시 재시도 skip 로직 추가
장기 개선 (재발 방지)#
- ES 인덱싱 전 문서 validation 레이어 추가 — geo_point, date 등 타입별 값 검증
- BIM 좌표계 메타데이터(CRS, georef)를 활용한 정확한 좌표 변환 파이프라인 구축
- ES 인덱싱 실패 시 silent failure가 아닌 모니터링/알림 체계 구축
Monitoring#
- ES 인덱싱 실패율 모니터링:
service:cupixworks-api "ElasticsearchError" "mapper_parsing_exception" @environment:production
- BulkIndexWorker 재시도 횟수 메트릭 추가
- geo_point validation 실패 시 warn 로그 추가 후 모니터링:
service:cupixworks-api "invalid geo_point" @environment:production
Risk Assessment#
- Risk level: medium
- 예상 복잡도: standard —
bim_center_location에 범위 검증 추가는 단순하나, 근본적 좌표 변환은 별도 작업 필요