대량 제로 리콜은 어떻게 트러블슈팅하는가 — 4층 원인 규명법

← 전체 목차 · 이전: 신구 제도가 동시에 존재하면 어떻게 처리하는가 · 다음: RAG 시스템의 프롬프트 인젝션은 어떻게 막나

대다수는 "벡터 데이터베이스 연결을 확인하고 Embedding 서비스가 죽지 않았는지 본다"부터 시작하는데, 이것이 바로 치명적인 방향 착오다.

제로 리콜 + 어떤 연결 에러도 없음
  → 인덱스는 살아 있고 서비스는 통해 있다
  → 고장 나지 않은 것을 조사하고 있으니 아무 결과도 나오지 않는다

원칙: 제로 리콜은 검색 파라미터와 인프라를 먼저 확인할 것이 아니라, 인덱스 안의 문서 본문에 유효한 내용이 들어 있는지를 먼저 확인해야 한다. 문제는 파이프라인의 중간 고리에서 발생했는데 당신은 파이프라인의 양 끝단 연통성을 조사하고 있으니 방향이 틀린 것이다.

차원내용
표면 쟁점원인 규명 순서
실제 쟁점인덱스 구축 윈도우 기간 리스크에 대한 이해
무효한 사고"서비스 상태 확인, 커넥션 풀 확인" — 이 유형의 장애는 영원히 특정되지 않는다

1층: 검색이 빈 Query에 의해 트리거되지 않았는지 확인

두 가지 트리거 방식:

  1. 사용자 프런트엔드가 빈 문자열을 그대로 전달했다.
  2. Query 재작성 모듈이 "좀 찾아 줘"를 무의미한 벡터로 번역했다.

결과는 같다: 검색 모듈이 빈 입력을 받아 곧바로 단락(short-circuit)해 빈 목록을 반환한다.

검증 방법: 요청 로그의 Query 필드를 10초만 보면 가려낼 수 있다. 그런데 많은 사람이 이 단계조차 하지 않는다.

server_request.log
  WARN  query: ""
  INFO  status: short_circuit_empty_return
  response_time: 2ms

2층: 인덱스 문서 본문이 비어 있는지 확인(핵심 시나리오)

장애 시나리오:

  1. 새벽에 정기 동기화 Job이 업무 데이터베이스에서 문서 내용을 추출한다.
  2. Embedding 서비스를 호출해 벡터를 생성한다.
  3. 벡터 데이터베이스에 기록한다.
  4. 전부 정상으로 보인다.

모니터링 패널의 세 가지 "정상" 신호:

지표표시값
Job 상태SUCCESS
로그 처리 레코드3000건
인덱스 콘솔 문서 수3000편

그런데 검색이 전부 0이다. 인덱스 문서 하나를 무작위로 열어 보니 — 본문 내용 필드가 전부 빈 문자열이다.

{"_id": "doc_8921", "title": "테스트 문서", "content": ""}

2층의 근인: DBA가 필드명을 바꿨는데 ETL의 SQL이 따라가지 않았다

전날:   DBA가 두 업무 테이블의 필드명을 변경
        content_body  →  article_text

당일:   ETL 추출 SQL에서 select하는 것은 여전히 옛 필드명 content_body

        SQL이 에러를 내지 않음(필드명 철자에는 문제가 없고 현재 테이블 구조에 없을 뿐)

        데이터베이스 엔진이 NULL 반환

        한 건씩 그대로 받아들여 3000건의 NULL 본문이 적재되고 인덱스가 만들어짐

        300차원 벡터가 전부 빈 문자열로부터 인코딩됨

이것이 벡터 오염이다 — 라이브러리 전체의 벡터가 [0.000, 0.000, 0.000, ...] 같은 EMPTY STRING VECTOR가 된다.

3층: 메타데이터 필터 조건을 전부 떼고 재현

조작: 모든 메타데이터 필터 조건을 임시로 떼고, 아무 필터도 하지 않은 벌거벗은 Query로 직접 검색한다.

벌거벗은 Query로 직접 검색
  ├─ 리콜 있음  → 필터층이 후보 집합을 몰살했다 → 원인 규명 방향을 필터 조건의 필드 매핑으로
  └─ 여전히 0   → 문제가 필터층에 없다, 계속 상류로

필터 조건에서 흔한 세 종류의 필드 매핑 오류:

유형증상예시
타임존 필드8시간 차이"timezone": "+00:00" // 기대값 +08:00
열거값대소문자 불일치"status": "Active" // 기대값 "active"
폐기 필드(Schema vs ETL)필터에 쓰는 상태 코드 필드가 인덱스 Schema에서 이미 폐기 표시되었는데 ETL은 여전히 옛 필드명으로 기록"status_code": 200 // Deprecated in Schema

4층: Embedding 모델 버전 일관성

사고 형태:

기록 시:  V1 모델로 벡터 생성
조회 시:  게이트웨이 카나리 라우팅에 의해 V2로 전환됨

두 모델의 벡터 공간이 완전히 비호환

내적 계산 결과가 전부 무작위 값

리콜이 0으로 귀결

발생 창: 모델 버전 업그레이드의 카나리 윈도우 기간에 가장 흔하다.

검증 방법: 인덱스 메타데이터에 기록된 Embedding 모델 버전 번호를 조회하면 확인된다.

답변 템플릿

0. 먼저 원인 규명 원칙을 선언
   제로 리콜의 원인 규명은 데이터 흐름 방향을 따라 하류에서 상류로 구간별 검증해야 한다

1. 조회 입력        —— 요청 로그의 Query 필드가 비어 있는가
2. 인덱스 내용      —— 인덱스 문서 하나를 무작위로 뽑아 본문 필드를 본다
3. 필터 조건        —— 벌거벗은 Query로 모든 필터를 떼고 재현
4. Embedding 버전   —— 인덱스 메타데이터의 모델 버전 번호

* 각 단계마다 구체적 검증 방법을 하나씩 붙일 것
* 마지막에 나쁜 경로 두 개를 보탤 것

나쁜 경로 두 가지

나쁜 경로 1: 다짜고짜 Embedding 서비스를 재시작하거나 인덱스를 재구축하지 말 것

  • 제로 리콜이 데이터 원천이 끊긴 것이라면 재구축해도 여전히 빈 라이브러리이므로 문제가 조금도 해결되지 않는다.
  • 게다가 재구축 과정에서 옛 인덱스가 이미 삭제되어 장애 현장조차 사라진다. → 현장 파괴.

나쁜 경로 2: 원인 규명 범위를 검색 모듈 자체로 한정하지 말 것

  • 많은 제로 리콜의 근인은 ETL 파이프라인에 있다.
  • 심지어 상류 DBA의 한 번의 테이블 구조 변경에 있다.
  • 당신의 검색 코드는 한 줄도 틀리지 않았는데 상류가 끊겼으니 결과는 0이다. → 시야 한정.

핵심 통찰

  1. "에러가 없다"는 것 자체가 가장 강력한 진단 정보다 — 제로 리콜인데 연결층이 한마디도 하지 않는다는 것은, 시스템이 링크가 통해 있고 프로세스가 살아 있다고 말해 주는 것이다. 이때 계속 연결과 프로세스를 조사하는 것은 이미 반증된 가설을 검증하는 것이다. "에러 없음"을 읽을 줄 아는 사람은 원인 규명 반경이 남들보다 한 자릿수 작아진다.
  2. 데이터 파이프라인의 장애는 중간 고리에 숨기를 좋아하는데 사람은 본능적으로 양 끝단만 조사한다 — 입력단(Query)과 출력단(벡터 DB 연결)이 관측 가능성이 가장 좋은 곳이라 먼저 조사하게 되지만, 실제로 끊어진 것은 중간의 추출—인코딩—기록 고리다. 방향을 잘못 고르면 시간을 아무리 투입해도 수익이 0이다.
  3. "Job SUCCESS + 레코드 수 일치"는 데이터가 정확하다는 뜻이 아니라 절차가 다 돌았다는 뜻일 뿐이다 — 3000건, 3000편 두 숫자가 전부 맞는데 본문은 전부 빈 문자열이다. 계수형 지표는 "무언가 흘러갔음"만 증명할 뿐 "흘러간 것이 유효한 내용임"은 증명하지 못한다.
  4. 빈 문자열이 가장 위험한 종류의 더러운 데이터다. 파이프라인 전체를 순조롭게 통과할 수 있기 때문이다 — NULL 예외도 포맷 오류도 아니어서 Embedding 서비스가 성실히 300차원 벡터로 인코딩하고 벡터 DB가 성실히 받아들인다. 모든 고리가 "성공"이고 최종 검색 결과만 0이다.
  5. 데이터베이스가 "존재하지 않는 필드명"에 관대한 것이 이런 사고의 증폭기다 — 필드명 철자에 문제가 없으면 SQL은 에러를 내지 않고 현재 테이블 구조에서 찾지 못할 뿐이며, 엔진이 NULL을 반환하면 그대로 받아들여진다. 상류의 소리 없는 schema 변경 한 번이 하류 인덱스 전체를 오염시킬 수 있다.
  6. 벡터 검색의 정확성은 "기록과 조회가 같은 벡터 공간에 있다"는 암묵적 계약에 의존한다 — 게이트웨이 카나리가 조회 측을 V2로 바꿨는데 기록 측이 V1이면 내적은 무작위 값으로 퇴화한다. 이 계약을 지켜 주는 명시적 에러가 전혀 없으므로 인덱스 메타데이터의 모델 버전 번호로만 대조할 수 있다.
  7. "인덱스 재구축"은 원인 규명 단계의 안티패턴이다. 증상과 증거를 동시에 파괴하기 때문이다 — 원천이 끊겼다면 재구축은 새 빈 라이브러리만 얻을 뿐이고, 옛 인덱스를 지우고 나면 장애 현장이 완전히 사라져 "당시 본문이 정말 비어 있었는지"에 영영 답할 수 없다.
  8. 좋은 장애 답변은 구조의 가치가 지식점의 밀도보다 높다 — 먼저 원인 규명 원칙을 선언(데이터 흐름을 따라 하류에서 상류로)하고, 층별로 전개하며 각 층에 실행 가능한 검증 동작을 하나씩 붙이고, 마지막에 나쁜 경로를 보탠다. 이 구조 자체가 실제 사고를 처리해 봤음을 증명한다.

엔지니어링 실전 Tips

  • "본문 비어 있지 않음"을 적재 전 하드 검증으로 만들 것: 3000건의 NULL 본문이 벡터 DB까지 그대로 간 것은 기록 링크에 내용 유효성 관문이 하나도 없었다는 뜻이다. 추출 후 Embedding 전에 널값 차단을 한 겹 두면 이런 사고는 발생하지 않는다.
  • 계수형 모니터링에는 내용형 모니터링을 짝지을 것: Job 상태와 처리 건수만 보면 영원히 SUCCESS / 3000이 보인다. 최소한 **"본문 평균 길이"**나 **"빈 본문 비율"**을 하나 더 수집해야 이런 침묵 장애가 떠오른다.
  • 인덱스 메타데이터에 Embedding 모델 버전 번호를 반드시 기록할 것: 4층 원인 규명의 유일한 근거다. 이 필드가 없으면 기록 V1 / 조회 V2의 어긋남은 추측에만 의존해야 한다.
  • 모델 카나리 업그레이드 기간에는 "검색 측"과 "기록 측"의 버전 결합을 심사할 것: 카나리 라우팅이 이런 사고의 고정 유인이므로 업그레이드 창 안에서는 기본적으로 버전 일관성 대조를 한 차례 더 할 것.
  • 상류 schema 변경에는 하류 구독자가 있어야 한다: 근인은 DBA가 필드명을 바꿨는데 ETL이 전혀 몰랐던 것이다. 테이블 구조 변경이 소비자에게 통지되지 않으면 하류는 "에러 없음"의 방식으로 조용히 파손된다.
  • 장애 현장 보존이 서비스 복구보다 우선한다: "인덱스 재구축"이 나쁜 경로인 이유는 쓸모없어서가 아니라 옛 인덱스를 함께 지워 근인을 영영 확인할 수 없게 만들기 때문이다.
  • 원인 규명 순서를 "데이터 흐름 방향을 따라 하류에서 상류로 구간별 검증"으로 고정할 것: 고정 순서가 있으면 원인 규명이 복기 가능하고 인수인계 가능한 절차가 되지, 직감에 기댄 시행착오가 아니게 된다.
  • 더 파고들 방향: 「제로 리콜」에서 「리콜은 되는데 무관함」으로의 확장(그때는 원인 규명 대상이 청킹 전략, Query 재작성 품질, 유사도 임계값으로 옮겨간다), 4층 원인 규명 절차를 자동화 헬스체크로 침전(빈 본문 비율, 인덱스와 조회 측 Embedding 버전 일관성, 벌거벗은 Query 기준 리콜 수), 상류 schema 변경의 계약화 거버넌스, 카나리 기간의 이중 기록·이중 인덱스 전략.