ES /docs

Elasticsearch::Transport::Transport::Errors::NotFound: [404] {"error":{"root_cause":[{"type":"index_not_found_exception"

RCA: capture text rank search failed — Elasticsearch Terms Query exceeds max_terms_count

Overview#

What Happened#

cupixworks-api 의 capture 검색(Api::V1::CapturesController#indexCaptureRepository)이 Elasticsearch 에 보내는 terms 쿼리에 사용자가 접근 가능한 capture ID 전부를 인라인으로 넣는다. 접근 권한이 매우 넓은 사용자(281,707개 capture 접근)의 경우 이 ID 목록이 Elasticsearch 의 index.max_terms_count(65,536 → 100,000) 한도를 초과하여 query_shard_exception ([400]) 으로 검색이 실패한다.

중요 — Representative Error 는 STALE 하다. 클러스터에 고정된 Representative Error(index_not_found_exception, "no such index [captures]", first_seen 2025-05-14) 는 이 Error Tracking 이슈의 최초 샘플이다. last_seen(2026-08-01) 근처의 실제 최근 로그는 전혀 다른 메시지 — query_shard_exception / "The number of terms [281707] ... has exceeded the allowed maximum of [100000]" — 를 보여준다. 아래 분석과 판정은 최근 발생 메시지 기준이다.

Quick Facts#

Field Value
exception.class Elasticsearch::Transport::Transport::Errors::BadRequest (recent) / ...Errors::NotFound (stale representative)
exception.message [400] ... query_shard_exception ... The number of terms [281707] used in the Terms Query request has exceeded the allowed maximum of [100000]
top_frame app/repositories/capture_repository.rb:662-681
es_index captures (index_uuid g-WDOysARW6pDrn--kkcwA)
env production (cupixworks-api)

Affected Teams#

Team / Domain Error Count Impact
cupixworks-api (capture search) 36 (issue total) 광범위한 접근 권한을 가진 사용자의 capture 목록/검색 API 가 [400] 로 실패 — 해당 사용자는 capture 리스트를 조회할 수 없음

Timeline#

  1. 2025-05-14 19:00 KST — Error Tracking 이슈 first_seen. 당시 대표 샘플은 index_not_found_exception (captures 인덱스 부재 시점).
  2. 2026-07-27 19:53 KST — 최근 발생: terms 수 89,058 이 한도 65,536 초과 (query_shard_exception).
  3. 2026-07-29 23:19 KST — terms 수 281,693 이 한도 100,000 초과.
  4. 2026-07-30 23:16–23:17 KST — terms 수 281,707 이 한도 100,000 초과 (다발성).
  5. 2026-08-01 09:01 KST — 클러스터 last_seen.

Error Log#

Datadog Logs

Representative Error (STALE — first_seen 샘플):

json
[404] {"error":{"root_cause":[{"type":"index_not_found_exception","reason":"no such index [captures]","resource.type":"index_or_alias","resource.id":"captures","index_uuid":"_na_","index":"captures"}],"type":"index_not_found_exception","reason":"no such index [captures]","resource.type":"index_or_alias","resource.id":"captures","index_uuid":"_na_","index":"captures"},"status":404}

Actual recent occurrence (2026-07-30, from Datadog logs):

json
{"error":{"root_cause":[{"type":"query_shard_exception","reason":"failed to create query: The number of terms [281707] used in the Terms Query request has exceeded the allowed maximum of [100000]. This maximum can be set by changing the [index.max_terms_count] index level setting.","index_uuid":"g-WDOysARW6pDrn--kkcwA","index":"captures"}],"type":"search_phase_execution_exception","reason":"all shards failed","phase":"query","grouped":true,"failed_shards":[{"shard":0,"index":"captures","reason":{"type":"query_shard_exception","reason":"failed to create query: The number of terms [281707] used in the Terms Query request has exceeded the allowed maximum of [100000]."}}]},"status":400}

Impact#

  • Service: cupixvista-elasticsearch (실제 앱 서비스: cupixworks-api, tesla repo)
  • 발생 횟수: 36
  • 최초 발생: 2025-05-14 19:00 KST
  • 최근 발생: 2026-08-01 09:01 KST

Root Cause Summary#

CaptureRepository#_search 는 로그인 사용자의 접근 범위를 강제하기 위해 terms: { id: current_user.directly_accessible_capture_ids }terms: { "record.id": current_user.readable_record_ids } 절을 Elasticsearch 쿼리에 추가한다. directly_accessible_capture_ids 는 사용자가 접근 가능한 모든 capture ID 를 배열로 반환하며 상한이 없다. 접근 권한이 매우 넓은 사용자(내부 admin/대형 테넌트, 281,707개)의 경우 이 배열이 그대로 Elasticsearch terms 쿼리에 인라인되어 인덱스의 index.max_terms_count(65,536 → 100,000) 를 초과, query_shard_exception 으로 400 에러가 발생한다. terms 수가 시간이 지나며 89,058 → 281,707 로 증가하는 것은 접근 가능 capture 가 계속 늘어난다는 방증이다. 이는 권한 필터를 ID 목록 인라인으로 구현한 데서 오는 확장성 결함(genuine bug)이며, Representative Error 의 index_not_found_exception 과는 무관하다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/captures_controller.rb:22#indexrepository_instance.search 호출
app/controllers/api/v1/captures_controller.rb:22-24ruby
def index
  capture_query_option = Cupix::QueryOption::Capture.new(get_query_option, params)
  captures = repository_instance.search(capture_query_option)
  • Failure point: app/repositories/capture_repository.rb:662-681 — 접근 권한 필터를 ID 목록으로 인라인
app/repositories/capture_repository.rb:662-681ruby
if self.current_user.present?
  self.query_option.query[:bool][:must] += [
    {
      bool: {
        should: [
          {
            terms: {
              "record.id": self.current_user.readable_record_ids
            }
          },
          {
            terms: {
              id: self.current_user.directly_accessible_capture_ids
            }
          }
        ]
      }
    }
  ]
end
  • directly_accessible_capture_ids 는 상한 없이 전체 접근 가능 ID 를 반환한다:
app/models/concerns/accessible_entities/directly_accessible.rb:61-67ruby
def directly_accessible_capture_ids(visibility = Cyclable.visibility[:UNTRASHED], fresh: false)
  if PERMISSION_CACHE_ENABLED && !fresh
    _directly_permitted_item_ids(::Capture, visibility, min_permission: 1)
  else
    directly_permitted_item_ids(::Capture, visibility, min_permission: 1)
  end
end
  • 이후 app/repositories/capture_repository.rb:690-695 에서 이 쿼리로 ::Capture.search 실행 → Elasticsearch 가 terms 수를 검증하며 한도 초과 시 [400] query_shard_exception 반환
app/repositories/capture_repository.rb:690-695ruby
response = ::Capture.search(
  self.query_option.serializable_hash
).paginate(
  per_page: self.query_option.per_page,
  page: self.query_option.page
)

기대 동작 vs 실제 동작: 권한 필터는 접근 가능 엔티티 수와 무관하게 동작해야 한다(기대). 실제로는 접근 가능 capture 가 100,000개를 넘는 사용자에게 검색 API 가 전면 실패한다.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api status:error "index"
text
service:cupixworks-api "text rank search failed"

(time range: 2026-07-19 → 2026-08-02, retention 내)

핵심 로그 (class CaptureRepository, 2026-07-30 14:16–14:17 UTC = 07-30 23:16–23:17 KST):

text
capture text rank search failed: [400] {"error":{"root_cause":[{"type":"query_shard_exception","reason":"failed to create query: The number of terms [281707] used in the Terms Query request has exceeded the allowed maximum of [100000]. This maximum can be set by changing the [index.max_terms_count] index level setting.","index":"captures"}],"type":"search_phase_execution_exception","reason":"all shards failed"},"status":400}

terms 수 증가 추이 (같은 index_uuid g-WDOysARW6pDrn--kkcwA):

text
2026-07-27 10:53 UTC  terms [89058]  > max [65536]
2026-07-29 14:19 UTC  terms [281693] > max [100000]
2026-07-30 14:16 UTC  terms [281707] > max [100000]

index_not_found_exception / no such index 키워드로는 retention 창(최근 14일) 내 로그가 0건 — Representative Error 는 재현되지 않음(stale 확인).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 권한 필터의 terms: { id: directly_accessible_capture_ids } 가 상한 없는 ID 배열을 인라인하여 index.max_terms_count 초과 최근 로그 전부 query_shard_exception + "terms [281707] > [100000]"; capture_repository.rb:673-674 가 unbounded directly_accessible_capture_ids 를 terms 에 삽입; terms 수 89,058→281,707 증가 추이 Confirmed
H2 captures 인덱스 부재 (index_not_found_exception) — Representative Error 대로 first_seen(2025-05-14) 대표 샘플 메시지 최근 14일 로그에 index_not_found/no such index 0건; 최근 에러의 index_uuid 가 _na_ 아닌 g-WDOysARW6pDrn--kkcwA (인덱스 존재) Rejected
H3 Elasticsearch 클러스터 outage (dep:elasticsearch) 2026-07-29 dep-elasticsearch 인시던트가 최근 resolved 로 존재 status-board active: null; 최근 에러는 outage 가 아닌 결정적 [400] 쿼리 검증 실패 (재시도해도 동일) Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • app/repositories/capture_repository.rb:662-681 의 권한 필터를 ID 목록 인라인 방식에서 벗어나도록 변경. 접근 가능 capture 수가 많은 사용자에서 terms 절이 한도를 넘지 않게 해야 한다. 방향(택1 또는 조합):
    • terms lookup 사용: directly_accessible_capture_ids 를 별도 인덱스/문서에 저장하고 Elasticsearch terms lookup 으로 참조 (인라인 배열 제거).
    • 권한을 문서에 비정규화: capture 문서에 접근 주체(team/user/permission) 필드를 색인하고, 쿼리에서 current_user 의 소수 권한 식별자로 필터 (ID 목록 대신 소속/권한 term 매칭).
    • 근거: 현재 방식은 접근 가능 엔티티 수에 선형 비례하는 쿼리를 생성해 대형 테넌트/admin 에서 필연적으로 한도를 초과한다.

단기 개선 (1주 이내)#

  • directly_accessible_capture_ids 결과 크기가 임계치(예: 50,000)를 넘으면 별도 처리 경로(권한을 term 필터로 치환하거나, 접근 범위가 사실상 전체인 admin 은 필터 생략)를 타도록 분기.
  • capture text rank search failed 를 던지는 rescue 지점에서, query_shard_exception (한도 초과)와 실제 인프라 오류(index_not_found, 503)를 구분해 로깅 레벨/알림을 분리. 한도 초과는 코드 결함 신호이므로 별도 태깅.

장기 개선 (재발 방지)#

  • 권한 기반 검색 필터를 ID 목록 인라인이 아닌 문서-레벨 권한 색인 + term 매칭 아키텍처로 표준화 (record/level/capture 공통).
  • Error Tracking 이슈가 서로 다른 근본 원인(404 index 부재 vs 400 terms 초과)을 하나로 묶는 문제 — fingerprint 를 exception class + reason type 까지 세분화해 stale 대표 샘플로 인한 오분석 방지.

Monitoring#

  • capture 검색 400 에러율 추이:
text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api_v1_captures_index}.as_count()
  • ES 요청 지연으로 대형 terms 쿼리 영향 관찰:
text
avg:trace.rack.request.duration{service:cupixworks-api,resource_name:api_v1_captures_index}

Risk Assessment#

  • Risk level: medium — 광범위 접근 사용자에 한정되나 해당 사용자는 capture 조회 자체가 불가능(기능 전면 차단).
  • 예상 복잡도: standard — 권한 필터 재설계가 필요하며 검색 정확도 회귀 테스트 요구.

Noise Verdict#

bug — 최근 발생 로그가 unbounded terms: { id: directly_accessible_capture_ids } 로 인해 index.max_terms_count 를 초과하는 query_shard_exception 이므로, 권한 필터 구현을 고쳐야 하는 명백한 코드 결함이다.