검색 인덱스 92.8%가 안 보였는데 오류가 없었다

8월 26, 2026 · 노이반
현장의 기록: AI Engineer / Forward Deployed Engineer — 12편
한 줄 요약

동기화 작업의 '성공' 로그는 프로세스가 예외 없이 완료됐다는 증거일 뿐이다 — 만들어야 했던 데이터가 실제로 완전한지는 아무것도 말해주지 않으며, 이건 별도로 확인해야 하는 별개의 주장이다.

배경

키워드 검색(BM25)과 벡터 임베딩 기반 시맨틱 검색을 결합한 하이브리드 검색 레이어를 갖춘 지식창고를 운영하고 있다 — 저장된 텍스트와 다르게 표현된 질의도 관련 문서를 찾을 수 있도록 하기 위함이다. 신규/변경 문서를 임베딩하고 인덱스를 최신 상태로 유지하는 동기화 작업이 정기적으로 돌아간다.

발견

정기 동기화 실행을 검증하던 중, 작업의 "성공" 로그 한 줄을 그냥 신뢰하는 대신 임베딩 테이블을 직접 쿼리해봤다 — 이미 몇 번의 이전 사건에서 성과를 냈던 습관이었다. 저장된 약 2,600건 중 2,410건(92.8%)이 빈 벡터였다. 시맨틱 유사도에 의존하는 질의는 지식창고의 90% 이상에 사실상 접근할 수 없었다 — 인덱스 대부분에서 키워드 매칭 레이어만 작동하고 있었다.

근본원인 — 서로 다른 두 환경에서 같은 작업을 하는 두 개의 스케줄러

동기화 스크립트는 두 개의 독립된 곳에서 트리거되고 있었다.

  1. 6시간마다 실행되는 시스템 레벨 예약 작업 — API 인증정보를 환경변수로 사용할 수 없는 환경이었다. 매 실행마다 "API 키 없음 — 임베딩 생략"을 조용히 로그로 남기고도 빈 벡터인 채로 문서를 저장했다.
  2. 메인 애플리케이션 자체의 내부 스케줄러 — 매시간 실행되며, 인증정보를 올바르게 상속받는 프로세스 컨텍스트에서 문서를 제대로 임베딩했다.

이 둘 중 어느 쪽이 특정 문서를 먼저 처리하느냐가 그 문서의 운명을 결정했다. 인증정보 없는 시스템 작업이 먼저 돌면, 문서는 빈 벡터인 채로 저장되고 콘텐츠 해시가 기록됐다. 나중에 올바르게 설정된 시간별 작업이 같은 콘텐츠 해시를 보고, "마지막 동기화 이후 변경 없음"이라고 정확하게 판단해서 재처리를 완전히 건너뛰었다 — 증분 동기화 로직에는 "이미 올바르게 임베딩됨"과 "이미 처리는 됐지만 결과가 비어있음"을 구분할 방법이 없었다.

# 동기화 로직의 사각지대
if stored_hash == current_hash:
    stats["skipped"] += 1
    continue   # 저장된 임베딩이 실제로 비어있지 않은지는 절대 확인 안 함

이건 조용히 위험한 실패 유형이다: 두 예약 작업 중 어느 것도 에러를 낸 적이 없다. 인증정보 없는 실행은 아무도 적극적으로 지켜보지 않는 경고를 로그로 남겼고, 올바르게 설정된 실행은 우연히 이 실패 유형에는 눈이 먼 해시 비교를 근거로 완전히 정확한 "변경 없음, 건너뜀" 판단을 내렸다. 개별적으로는 올바른 두 로직이 결합해서 크고, 조용하고, 꾸준히 커지는 검색 커버리지 공백을 만들어냈다.

조치

  1. 저장된 벡터가 비어있는 문서만 대상으로 한 번의 백필을 실행해서 전부 재임베딩했다.
  2. 중복된, 인증정보 없는 시스템 레벨 작업을 완전히 제거했다 — 올바르게 설정된 시간별 작업이 이미 같은 책임을 올바르게 커버하고 있었으므로, 이 수정은 새 코드를 추가하는 게 아니라 빼는 것이었다.
  3. 코드 변경이 필요 없는 가벼운 검증 습관을 추가했다: 동기화 실행 이후, 작업 자체가 보고하는 요약 줄을 신뢰하는 대신 빈 벡터 행의 개수를 직접 확인한다.

검증, 그리고 솔직한 후속 패턴

백필 직후: SELECT COUNT(*) WHERE length(embedding) <= 2가 0을 반환해서 완전한 회복을 확인했다.

그 이후 며칠간 모니터링하면서 작은 잔존 패턴이 계속 재발했다: 확인할 때마다 1~2건의 문서가, 시간별 동기화 실행 직전에 생성된 것들이, 다음 확인에서 빈 벡터로 나타났다 — 같은 버그의 재발이 아니라 경계 타이밍 엣지케이스였다(스캔 직전의 좁은 시간 창에 생성된 문서가 임베딩 호출이 완료되기 전에 기록될 수 있는 경우). 매번 빠른 수동 백필로 즉시 해소됐다. 이 작고 스스로 해소되는 패턴을 여러 번 연속으로 관찰한 뒤, 나는 이걸 1~2건의 지연을 막기 위한 전용 상시 작업을 새로 만드는 대신 받아들일 만한 낮은 수준의 노이즈 패턴으로 명시적으로 기록하기로 판단을 내렸다 — 새로운 영구 자동화의 비용이, 가끔 몇 건을 알아채고 정리하는 비용보다 컸다.

얻은 교훈

동기화 작업 자체의 "성공" 로그는 그 프로세스가 예외 없이 완료됐다는 증거일 뿐이다 — 그게 만들어냈어야 할 데이터가 실제로 완전한지에 대해서는 아무것도 말해주지 않는다. 여기서의 함정은, 개별적으로는 각각 멀쩡하게 동작하는 두 개의 자동화(자기 한계를 정확히 로그로 남긴 인증정보 부족 작업과, 중복 작업을 정확하게 피한 증분 동기화 작업)가 결합해서, 둘 중 어느 것도 혼자서는 절대 드러내지 못할 큰 공백을 만들어낸 것이다. 두 스케줄러가 같은 데이터를 건드릴 수 있을 때는, 둘 중 어느 스케줄러의 자기 보고를 읽는 게 아니라 그 데이터의 실제 상태를 직접 쿼리해서 검증해야 한다.

Advertisement

첫 댓글을 남겨보세요