개발/용어 사전

HNSW, FAISS, ANN Index는 왜 대규모 검색에서 필요한가

cedis 2026. 6. 17. 01:02
검색 조합 심화

벡터 검색은 질문 벡터와 문서 벡터 사이의 가까운 대상을 찾는 작업입니다. 문서가 적을 때는 모든 벡터와 비교해도 되지만, 데이터가 커지면 이 방식은 느려집니다.

ANN Index는 정확도를 조금 양보하더라도 가까운 후보를 훨씬 빠르게 찾기 위한 구조입니다. HNSW는 대표적인 인덱스 방식이고, FAISS는 대규모 벡터 검색 라이브러리입니다.

벡터 검색이 느려지는 과정

1단계
쿼리 벡터 생성
질문이나 이미지를 embedding model로 벡터화합니다.
2단계
전체 비교
단순 방식은 저장된 모든 벡터와 거리를 계산합니다.
3단계
데이터 증가
벡터 수가 늘수록 계산량과 응답 시간이 커집니다.
4단계
인덱스 사용
ANN index로 가까운 후보를 빠르게 좁힙니다.

용어 층위 정리

ANN
Approximate Nearest Neighbor입니다. 정확한 전체 탐색 대신 가까운 이웃을 빠르게 찾는 접근입니다.
HNSW
그래프 기반 ANN 인덱스 방식입니다. 가까운 노드들을 따라가며 후보를 찾는 구조로 많이 사용됩니다.
IVF
벡터 공간을 여러 구역으로 나눠 관련 있어 보이는 구역 안에서 먼저 찾는 방식입니다.
PQ
벡터를 압축해 메모리 사용량을 줄이는 방식입니다. 속도와 메모리, 정확도 사이의 절충에 쓰입니다.
FAISS
대규모 dense vector의 similarity search와 clustering을 위한 라이브러리입니다. 여러 인덱스 방식을 제공합니다.
Vector DB
벡터 저장, 인덱싱, 검색, 필터, 운영 기능을 묶어 제공하는 데이터베이스 계층입니다.

무엇을 비교해야 하나

기준 질문 의미
속도 검색 응답이 충분히 빠른가 사용자 경험과 비용에 직접 연결
Recall 가까운 정답 후보를 얼마나 놓치지 않는가 빠르지만 중요한 문서를 놓치면 실패
메모리 인덱스와 벡터를 저장할 공간이 충분한가 HNSW 같은 구조는 추가 메모리를 사용
갱신 새 문서와 수정 문서를 어떻게 반영하는가 색인 재구성 비용이 운영 이슈가 됨

작은 실험: 전체 비교와 후보 축소 비교하기

아래 코드는 실제 HNSW나 FAISS 구현 대신 전체 벡터를 모두 비교하는 방식과 간단한 bucket으로 후보를 줄이는 방식을 비교합니다. 핵심은 대규모 검색에서 모든 벡터를 매번 보지 않는다는 감각입니다.

from math import sqrt

vectors = {
    "doc1": (0.10, 0.10),  # 로그인
    "doc2": (0.12, 0.08),  # 인증
    "doc3": (0.80, 0.80),  # 이미지
    "doc4": (0.82, 0.78),  # 사진
    "doc5": (0.50, 0.10),  # 기타
}

query = (0.11, 0.09)

def dist(a, b):
    return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)

def brute_force():
    checks = 0
    results = []
    for name, vec in vectors.items():
        checks += 1
        results.append((dist(query, vec), name))
    return checks, sorted(results)

def bucket_index():
    # 실제 HNSW/FAISS 구현 대신, "전체를 다 보지 않기"의 감각만 보는 toy index다.
    buckets = {
        "left": ["doc1", "doc2", "doc5"],
        "right": ["doc3", "doc4"],
    }
    selected_bucket = "left" if query[0] < 0.6 else "right"
    checks = 0
    results = []
    for name in buckets[selected_bucket]:
        checks += 1
        results.append((dist(query, vectors[name]), name))
    return checks, sorted(results)

for label, fn in [("brute force", brute_force), ("bucket index", bucket_index)]:
    checks, results = fn()
    print(label, "distance checks:", checks)
    print(results[:2])
관찰할 것
  • brute force는 모든 벡터와 거리를 계산하는지 봅니다.
  • bucket index는 일부 후보만 비교해 distance checks가 줄어드는지 봅니다.
  • 후보를 줄이면 빨라질 수 있지만, 잘못 줄이면 정답 후보를 놓칠 수 있습니다.
예상 출력의 모양
brute force distance checks: 5
[(0.014..., 'doc2'), (0.014..., 'doc1')]
bucket index distance checks: 3
[(0.014..., 'doc2'), (0.014..., 'doc1')]

처음부터 복잡하게 시작하지 않아도 되는 이유

  • 데이터가 작을 때는 brute-force 검색도 충분할 수 있습니다.
  • 대규모 인덱스는 속도를 얻는 대신 튜닝할 값이 늘어납니다.
  • 검색 품질이 낮은 원인이 인덱스인지, 임베딩 모델인지, chunking인지 먼저 분리해야 합니다.
  • 성능 튜닝은 실제 쿼리와 평가셋을 만든 뒤에 해야 의미가 있습니다.

기억할 것

  • ANN Index는 대규모 벡터 검색을 빠르게 만들기 위한 구조입니다.
  • HNSW는 인덱스 방식이고, FAISS는 벡터 검색 라이브러리입니다.
  • 속도, recall, 메모리, 갱신 비용을 함께 봐야 합니다.

참고한 문서