ES /docs

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에 인덱싱할 때 mapper_parsing_exception 에러가 6,356건 발생했다. BIM 좌표계의 center point 값(예: latitude 2139310.163528784)이 유효한 geo_point 위도 범위(-9090)를 초과하여 ES가 문서 인덱싱을 거부한 것이 원인이다.

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#

  1. 2026-05-22T04:00:01Z — 최초 에러 발생 (facility_key: 3fq7tj, team clark-vdc)
  2. 2026-05-22T04:04:49Z — 다수 사용자에서 동시 다발적 에러 (facility_key: 3fq7tj, rkuup2)
  3. 2026-05-22T04:43:07Z — 마지막 에러 기록
  4. 2026-05-22T05:00:00Z — Error Sweeper 감지

Error Log#

Datadog Logs

text
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의 offsetorigin을 더한다. 문제는 이 값들이 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#bulkBulkableController#bulkBulkableRepository#bulk
  • Bulk update가 DB에 성공적으로 저장된 후, after_commit 콜백에서 ES 인덱싱이 트리거된다.
  • _update_document 메서드에서 @__changed_model_attributes가 없으면 _index_document로 fallback한다.
  • _index_documentas_indexed_json을 호출하여 ElementTraceSerializer를 통해 문서를 직렬화한다.
  • Serializer가 element.bim_center_location을 호출하여 location을 계산한다.

1. ES 인덱스 매핑 (location을 geo_point로 정의):

app/models/concerns/searchable/element_trace.rb:114ruby
indexes 'location', type: 'geo_point'

2. Serializer에서 location 계산 위임:

app/serializers/element_trace_serializer.rb:68-70ruby
attribute :location do |element_trace|
  element_trace.element&.bim_center_location
end

3. BIM center location 계산 (실제 문제 지점):

app/models/concerns/properties/element.rb:103-121ruby
# 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 호출:

app/models/concerns/searchable.rb:107-110ruby
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 및 로깅:

app/models/concerns/searchable.rb:50-52ruby
rescue StandardError => e
  Cupix::Logger.error("Index error - #{e.message}", class: self.class.name, function: __method__)
  BulkIndexWorker.perform_async(self.class.name, [id], 'index')
app/models/concerns/searchable.rb:115-117ruby
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 쿼리:

text
service:cupixworks-api status:error @environment:production "mapper_parsing_exception" "location"

로그 패턴 (모든 에러가 동일한 구조):

json
{
  "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:120bim_center_location 메서드에서 반환 직전에 lat/lon 값이 유효한 geo_point 범위인지 검증하고, 범위를 벗어나면 nil을 반환하도록 수정
  • 유효 범위: latitude -9090, 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 인덱싱 실패율 모니터링:
text
service:cupixworks-api "ElasticsearchError" "mapper_parsing_exception" @environment:production
  • BulkIndexWorker 재시도 횟수 메트릭 추가
  • geo_point validation 실패 시 warn 로그 추가 후 모니터링:
text
service:cupixworks-api "invalid geo_point" @environment:production

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — bim_center_location에 범위 검증 추가는 단순하나, 근본적 좌표 변환은 별도 작업 필요