Skip to content

docs(db): 벡터 인덱스의 실제 동작 기록 — ANN 은 돌지 않는다 - #269

Merged
i3months merged 1 commit into
devfrom
docs/vector-index-reality
Sep 29, 2026
Merged

i3months merged 1 commit into
devfrom
docs/vector-index-reality

Conversation

@i3months

Copy link
Copy Markdown
Member

DB 계층을 처음 점검했다. 성능 문제는 없었다. 대신 문서와 코드가 만드는 잘못된 믿음을
찾았다.

1. 문서가 ivfflat 을 적고 있었다

docs/database.md 의 DDL 은 idx_embeddings_ivfflat 이다. V8 에서 HNSW 로 교체됐는데
문서가 안 따라왔다.

2. HNSW·GIN 인덱스는 한 번도 쓰인 적이 없다

pg_stat_user_indexes:
  idx_document_embeddings_hnsw   idx_scan = 0   (848 kB)
  idx_document_embeddings_tsv    idx_scan = 0   (200 kB)

고장이 아니라 쿼리 모양 때문이다. DocumentEmbeddingService.search 는 항상 요청자
소유의 활성 문서로 범위를 좁혀 넘기고, 비면 조회를 건너뛴다. 그래서 리포지토리에 닿는 쿼리는
예외 없이 WHERE document_id IN (...) 를 달고 있고, 필터가 선택적이라 플래너가
btree + 정렬을 고른다. 그게 더 빠르다 — 옳은 선택이다.

EXPLAIN 으로 확인:

쿼리 모양 계획
JOIN 없음 · 필터 없음 Index Scan using idx_document_embeddings_hnsw
ACTIVE_DOC_JOIN 있음 Sort ← Nested Loop — enable_seqscan=off 로도 미사용
document_id IN (...) 있음 btree + Sort

ANN 인덱스가 붙어 있다고 근사 최근접 탐색이 도는 게 아니다. 실제로는 사용자 범위 안에서
정확(brute-force) 거리 계산 후 정렬한다.

운영 쿼리 실측 실행 0.338ms (계획 1.69ms, 청크 57행).

그래서 지금은 바꾸지 않는다

ORDER BY <=> 가 조인된 관계 위에 있으면 플래너가 ANN 으로 밀어 넣지 못한다.
ACTIVE_DOC_JOIN 은 오늘 기준 아무것도 거르지 않는다 — 문서 삭제 시 임베딩을 함께 지우고
(soft delete 된 문서 19개의 청크가 0개), 서비스가 이미 활성 문서로 좁힌다.

그래도 남겨 둔다. 삭제 경로가 언젠가 새면 이게 마지막 방어선이고 비용은 0.3ms 안쪽이다.
지금 빼면 얻는 것 없이 방어선만 잃는다.

다시 볼 조건과 방법을 적어 뒀다: 한 세션의 청크가 수천 줄이 되어 정렬 비용이 RAG 하드
타임아웃(questions_rag_timeout_sec/followup_rag_timeout_sec 각 1.5초)에 닿기 시작하면,
ANN 을 서브쿼리로 먼저 돌리고 JOIN 을 바깥으로 빼면 인덱스가 살아난다(EXPLAIN 으로 확인함).

함께 확인한 것 (문제 없음)

항목 결과
FK 인덱스 누락 0건
가장 큰 테이블 interview_messages 471행 / 584 kB
실제 RAG 쿼리 0.338ms

코드 변경은 ACTIVE_DOC_JOIN 에 붙인 설명 주석뿐이다 — 다음 사람이 "왜 HNSW 를 안 쓰지" 로
헤매거나, 반대로 "ANN 이 도니까 괜찮다" 고 오해하지 않도록.

DB 계층을 처음 점검했다. 성능 문제는 없었다. 대신 **문서와 코드가 만드는
잘못된 믿음**을 찾았다.

## 1. 문서가 ivfflat 을 적고 있었다

docs/database.md 의 DDL 은 idx_embeddings_ivfflat 이다. V8 에서 HNSW 로
교체됐는데 문서가 안 따라왔다.

## 2. HNSW·GIN 인덱스는 한 번도 쓰인 적이 없다

pg_stat_user_indexes 기준 둘 다 idx_scan = 0.

고장이 아니라 **쿼리 모양** 때문이다. DocumentEmbeddingService.search 는 항상
요청자 소유의 활성 문서로 범위를 좁혀 넘기고, 비면 조회를 건너뛴다. 그래서
리포지토리에 닿는 쿼리는 예외 없이 WHERE document_id IN (...) 를 달고 있고,
필터가 선택적이라 플래너가 btree + 정렬을 고른다. 그게 더 빠르다 — 옳은 선택이다.

EXPLAIN 으로 확인:

  JOIN 없음·필터 없음      Index Scan using idx_document_embeddings_hnsw
  ACTIVE_DOC_JOIN 있음     Sort ← Nested Loop (enable_seqscan=off 로도 미사용)
  document_id IN 있음      btree + Sort

**ANN 인덱스가 붙어 있다고 근사 최근접 탐색이 도는 게 아니다.** 실제로는 사용자
범위 안에서 정확 거리 계산 후 정렬한다. 운영 쿼리 실측 실행 0.338ms(청크 57행).

## 그래서 지금은 바꾸지 않는다

ORDER BY <=> 가 조인된 관계 위에 있으면 ANN 으로 못 민다. ACTIVE_DOC_JOIN 은
오늘 기준 아무것도 거르지 않지만(삭제 시 임베딩을 함께 지우고, 서비스가 이미
활성 문서로 좁힌다) 삭제 경로가 새면 마지막 방어선이고 비용은 0.3ms 안쪽이다.
**지금 빼면 얻는 것 없이 방어선만 잃는다.**

다시 볼 조건과 방법을 적어 뒀다 — 한 세션의 청크가 수천 줄이 되어 정렬 비용이
RAG 하드 타임아웃(1.5초)에 닿기 시작하면, ANN 을 서브쿼리로 먼저 돌리고 JOIN 을
바깥으로 빼면 인덱스가 살아난다(EXPLAIN 으로 확인함).

## 함께 확인한 것 (문제 없음)

  FK 인덱스 누락            0건
  테이블 최대               471행 / 584 kB
  실제 RAG 쿼리             0.338ms
@i3months
i3months merged commit 43af2b6 into dev Sep 29, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant