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#index → CaptureRepository)이 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#
- 2025-05-14 19:00 KST — Error Tracking 이슈 first_seen. 당시 대표 샘플은
index_not_found_exception(captures 인덱스 부재 시점). - 2026-07-27 19:53 KST — 최근 발생: terms 수 89,058 이 한도 65,536 초과 (
query_shard_exception). - 2026-07-29 23:19 KST — terms 수 281,693 이 한도 100,000 초과.
- 2026-07-30 23:16–23:17 KST — terms 수 281,707 이 한도 100,000 초과 (다발성).
- 2026-08-01 09:01 KST — 클러스터 last_seen.
Error Log#
Representative Error (STALE — first_seen 샘플):
[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):
{"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—#index가repository_instance.search호출
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 목록으로 인라인
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 를 반환한다:
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반환
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 쿼리:
service:cupixworks-api status:error "index"
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):
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):
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를 별도 인덱스/문서에 저장하고 Elasticsearchtermslookup 으로 참조 (인라인 배열 제거). - 권한을 문서에 비정규화: capture 문서에 접근 주체(team/user/permission) 필드를 색인하고, 쿼리에서
current_user의 소수 권한 식별자로 필터 (ID 목록 대신 소속/권한 term 매칭). - 근거: 현재 방식은 접근 가능 엔티티 수에 선형 비례하는 쿼리를 생성해 대형 테넌트/admin 에서 필연적으로 한도를 초과한다.
- terms lookup 사용:
단기 개선 (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 에러율 추이:
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api_v1_captures_index}.as_count()
- ES 요청 지연으로 대형 terms 쿼리 영향 관찰:
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 이므로, 권한 필터 구현을 고쳐야 하는 명백한 코드 결함이다.