Vector DB

Python 벡터 DB 하이브리드 검색

Supabase, Chroma, Milvus, Qdrant, Weaviate, FAISS의 하이브리드 검색 구현 방식을 코드로 비교한다. 인덱스 알고리즘과 1M 벡터 기준 메모리 사용량, 보안 모델까지 다룬다.

2026.06.07 · 19분 읽기 · 4,754 단어
📊 슬라이드

1. 왜 하이브리드 검색인가 — 키워드·시맨틱의 결함을 RRF로 메운다

검색 품질이 LLM 응답 품질의 상한선을 결정한다는 사실은 이제 RAG 실무자에게 상식이다. 그런데 단일 검색 방식만 쓰면 어떤 모델을 갈아 끼워도 답이 어긋난다. 키워드와 시맨틱은 각자 다른 형태의 실패를 가지고 있고, 그 실패가 정확히 상호 보완 관계라는 점이 하이브리드 검색이 표준이 된 이유다.

한 줄 요약: 키워드는 정확한 토큰을 잡고, 시맨틱은 의미를 잡는다. RRF는 두 결과의 랭크 역수만 더해 점수 스케일 정규화 없이 둘을 합친다.

1.1 두 검색의 실패 패턴

검색 방식 잘하는 것 못하는 것 대표 실패 사례
키워드 (BM25 / tsvector) 토큰 정확 일치, 식별자·코드·SKU 동의어, 어순 변형, 다국어, 의역 "결제 실패" 검색에 "payment error"가 안 잡힘
시맨틱 (dense vector) 의미·맥락·다국어 정확한 식별자, 숫자, 희귀 명사 ERR_2048 같은 에러 코드가 의미적으로 비슷한 다른 코드와 섞임

코드 검색에서 dense 임베딩이 자주 망가지는 이유는 토크나이저가 getUserById, get_user_by_id, findUserById를 거의 같은 벡터로 매핑하기 때문이다. 반대로 상담 FAQ에서 BM25가 망가지는 이유는 "환불 받고 싶어요"와 "결제 취소 가능한가요?"의 토큰 교집합이 사실상 0이기 때문이다. 두 결함은 분포 자체가 다르고, 그래서 단순히 두 결과를 합치면 평균적으로 좋아진다.

1.2 RRF — 점수 스케일 문제를 우회하는 단순한 트릭

BM25 점수는 무제한 양수, 코사인 유사도는 [-1, 1], 내적은 모델마다 다른 분포를 가진다. 둘을 직접 가중합하려면 z-score나 min-max 정규화가 필요한데, 쿼리마다 분포가 달라져 일관성이 깨진다. RRF(Reciprocal Rank Fusion)는 이 문제를 우회한다.

$$ \text{score}(d) = \sum_{r \in R(d)} \frac{1}{k + \text{rank}_r(d)} $$

핵심은 점수가 아니라 순위만 본다는 점이다. 키워드 결과에서 3등, 시맨틱 결과에서 9등이면 1/(60+3) + 1/(60+9) ≈ 0.0303. 한쪽 리스트에만 등장하면 그쪽 점수만 더해진다. k는 smoothing 상수로 보통 50~60을 쓴다 — k가 크면 상위 항목 간 점수 차이가 작아져 두 리스트의 합의가 더 중요해지고, 작으면 1등의 영향력이 커진다.

k 효과 적합한 상황
1~10 상위 항목 가중치 매우 큼 한쪽 검색이 압도적으로 신뢰될 때
50~60 표준값, TREC 논문 기본값 일반적인 RAG·검색
100+ 두 리스트의 합의에 가까운 결과 노이즈 많은 BM25 + 노이즈 많은 임베딩

1.3 워크로드별 적합도

경험칙: 한 시스템에 정확 매칭과 의미 매칭이 섞인 쿼리가 들어오면 거의 항상 하이브리드가 우세하다.

  • 코드/로그 검색 — 키워드 우세 (alpha 0.2~0.3 또는 BM25 단독)
  • 상담/FAQ/뉴스 추천 — 시맨틱 우세 (alpha 0.7~0.8)
  • 이커머스/문서 RAG/지원 KB — 하이브리드가 거의 항상 우세 (alpha 0.4~0.6)
  • 다국어 검색 — 시맨틱 우세, 단 고유명사는 BM25 필수

2. Supabase의 hybrid_search 동작 원리 — GIN + HNSW + RRF SQL

Supabase 공식 docs의 hybrid_search 함수는 Postgres의 두 인덱스(GIN for tsvector, HNSW for pgvector)와 RRF를 SQL 한 함수에 담는 깔끔한 레퍼런스 구현이다. 다른 벡터 DB의 하이브리드를 이해하려면 이 함수부터 해부하는 것이 가장 빠르다.

2.1 스키마: 생성 컬럼 + 두 종류 인덱스

create table documents (
  id bigint primary key generated always as identity,
  content text,
  fts tsvector generated always as (to_tsvector('english', content)) stored,
  embedding extensions.vector(512)
);

-- 전문 검색용 GIN — tsvector 같은 다중값에 최적화된 역색인
create index on documents using gin(fts);

-- 시맨틱용 HNSW — 내적 연산자 <#>를 쓸 계획이므로 vector_ip_ops
create index on documents using hnsw (embedding vector_ip_ops);

fts 컬럼이 stored generated 컬럼이라는 점이 중요하다. INSERT/UPDATE 시 자동 갱신되므로 애플리케이션 코드가 tsvector를 수동 관리할 필요가 없다. 연산자(<#>, <=>, <->)와 인덱스 ops 클래스(vector_ip_ops, vector_cosine_ops, vector_l2_ops)는 반드시 짝이 맞아야 한다 — 짝이 안 맞으면 인덱스가 묵묵히 무시되고 풀스캔이 돈다.

2.2 GIN 인덱스가 GiST가 아닌 이유

인덱스 특성 tsvector 적합도
GIN 역색인, 다중값 항목 빠르게 검색, 빌드 느림 ✅ 표준 선택
GiST 일반 트리, 업데이트 빠름, 검색 약간 느림 자주 업데이트되는 작은 테이블에서만

문서 검색은 거의 항상 read-heavy이므로 GIN이 기본이다. tsvector는 lexeme(어간 추출된 토큰)와 위치 정보를 담은 다중값 타입이고, GIN은 이런 컬럼을 키별로 posting list로 뒤집어 저장한다.

2.3 hybrid_search 함수 전체

Supabase docs에 기재된 함수를 정리하면 다음과 같다.

create or replace function hybrid_search(
  query_text text,
  query_embedding extensions.vector(512),
  match_count int,
  full_text_weight float = 1,
  semantic_weight float = 1,
  rrf_k int = 50
)
returns setof documents
language sql
as $$
with full_text as (
  select
    id,
    row_number() over(order by ts_rank_cd(fts, websearch_to_tsquery(query_text)) desc) as rank_ix
  from documents
  where fts @@ websearch_to_tsquery(query_text)
  order by rank_ix
  limit least(match_count, 30) * 2
),
semantic as (
  select
    id,
    row_number() over(order by embedding <#> query_embedding) as rank_ix
  from documents
  order by rank_ix
  limit least(match_count, 30) * 2
)
select documents.*
from full_text
full outer join semantic on full_text.id = semantic.id
join documents on coalesce(full_text.id, semantic.id) = documents.id
order by
  coalesce(1.0 / (rrf_k + full_text.rank_ix), 0.0) * full_text_weight +
  coalesce(1.0 / (rrf_k + semantic.rank_ix), 0.0) * semantic_weight
  desc
limit least(match_count, 30);
$$;

2.4 함수가 영리하게 잘하는 것 3가지

  1. ts_rank_cd는 인덱스 사용 불가. 그래서 WHERE fts @@ websearch_to_tsquery(...)로 GIN 인덱스를 태워 후보를 좁힌 뒤에만 ranking 점수를 계산한다. 만약 WHERE 없이 전체 테이블에 ts_rank_cd를 걸면 풀스캔이 돈다.
  2. FULL OUTER JOIN — 한쪽 리스트에만 있는 결과도 보존. 둘 다 hit해야만 점수를 받는 INNER JOIN 버그를 피한다.
  3. coalesce(..., 0.0) — 한쪽 검색에 없으면 그쪽 점수를 0으로 처리. RRF의 핵심 시맨틱.

2.5 튜닝 파라미터 표

파라미터 기본값 영향 권장 조정 방향
full_text_weight 1.0 키워드 결과 가중치 식별자 검색이면 1.5~2.0
semantic_weight 1.0 시맨틱 결과 가중치 의역 많으면 1.5~2.0
rrf_k 50 상위 항목 영향력 노이즈 많으면 ↑
match_count (인자) 최종 반환 개수 보통 10~20
후보 limit match_count * 2 각 검색의 prefetch 크기 정확도 부족하면 ×3~×5
hnsw.ef_search 40 HNSW 후보 큐 크기 recall 부족하면 100~200

2.6 hybrid_search 호출 예시 (Python)

from supabase import create_client
from openai import OpenAI

sb = create_client(SUPABASE_URL, SUPABASE_KEY)
oai = OpenAI()

query = "환불 절차 알려줘"
emb = oai.embeddings.create(
    model="text-embedding-3-small",
    input=query,
    dimensions=512,
).data[0].embedding

res = sb.rpc("hybrid_search", {
    "query_text": query,
    "query_embedding": emb,
    "match_count": 10,
    "full_text_weight": 1.0,
    "semantic_weight": 1.0,
    "rrf_k": 50,
}).execute()

3. 벡터 DB별 하이브리드 구현 — 네이티브 vs 워크어라운드

각 벡터 DB가 하이브리드를 다루는 방식은 크게 세 갈래다: (a) 단일 쿼리에서 fusion까지 처리하는 네이티브, (b) 두 검색을 따로 실행하고 서버 측에서 합치는 다단계, (c) 라이브러리만 제공하고 BM25는 외부에 위임. 어느 갈래인지가 운영 복잡도를 좌우한다.

3.1 한눈에 보는 지원 매트릭스

DB 하이브리드 지원 fusion 방식 sparse vector BM25 내장
Supabase (pgvector + tsvector) SQL 함수로 구현 RRF (수동) sparsevec 타입 tsvector (full-text)
Qdrant ✅ 네이티브 (v1.10+) RRF, DBSF ✅ named vector ❌ sparse vector로 대체
Weaviate ✅ 네이티브 alpha 가중합, RRF, Relative Score ❌ (BM25 직접) ✅ BM25F
Milvus ✅ 네이티브 (v2.4+) WeightedRanker, RRFRanker ✅ BM25 함수 (v2.5+)
Chroma ❌ 클라이언트 결합 직접 구현 일부
FAISS ❌ 라이브러리 전용 직접 구현

3.2 Qdrant — Query API의 prefetch 패턴

Qdrant는 v1.10부터 Query APIprefetch를 도입해, 한 요청 안에 여러 서브쿼리를 실행하고 최종 단계에서 RRF/DBSF로 합칠 수 있다. sparse와 dense를 named vector로 한 컬렉션에 같이 보관한다는 점이 핵심이다.

from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.query_points(
    collection_name="docs",
    prefetch=[
        models.Prefetch(
            query=models.SparseVector(indices=[1, 42], values=[0.22, 0.8]),
            using="sparse",
            limit=20,
        ),
        models.Prefetch(
            query=[0.01, 0.45, 0.67, ...],  # dense
            using="dense",
            limit=20,
        ),
    ],
    query=models.RrfQuery(rrf=models.Rrf(k=60)),
    limit=10,
)

Qdrant는 zero-based rank를 쓴다 — 최상위가 r_d = 0이라서 RRF 분모가 k만 남는다. Supabase의 1-based와 다르므로 두 시스템 점수를 직접 비교하지 말 것.

3.3 Weaviate — alpha 한 줄로 끝나는 API

Weaviate는 가장 사용자 친화적인 API를 제공한다. alpha로 키워드/벡터 비중을 한 숫자로 표현하고, fusion 방식은 v1.24부터 Relative Score Fusion이 기본.

from weaviate.classes.query import HybridFusion, MetadataQuery

docs = client.collections.use("Docs")
response = docs.query.hybrid(
    query="food",
    alpha=0.5,                               # 0=BM25, 1=vector
    fusion_type=HybridFusion.RELATIVE_SCORE, # 또는 RANKED (RRF)
    return_metadata=MetadataQuery(score=True, explain_score=True),
    limit=10,
)

alpha 모델은 직관적이지만 두 점수의 분포가 다를 때 비선형적으로 반응하는 단점이 있다. 그래서 Weaviate도 RRF 옵션(HybridFusion.RANKED)을 함께 제공한다.

3.4 Milvus — 다중 AnnSearchRequest + Ranker

Milvus 2.4+는 여러 AnnSearchRequest를 동시에 보내고 WeightedRanker 또는 RRFRanker로 합친다. 2.5부터는 BM25 함수를 컬렉션 스키마에 직접 정의할 수 있다.

from pymilvus import AnnSearchRequest, RRFRanker, MilvusClient

client = MilvusClient(uri="http://localhost:19530")

dense_req = AnnSearchRequest(
    data=[query_dense_vec],
    anns_field="dense",
    param={"metric_type": "IP", "params": {"ef": 100}},
    limit=20,
)
sparse_req = AnnSearchRequest(
    data=[query_sparse_vec],
    anns_field="sparse",
    param={"metric_type": "IP"},
    limit=20,
)

results = client.hybrid_search(
    collection_name="docs",
    reqs=[dense_req, sparse_req],
    ranker=RRFRanker(k=60),
    limit=10,
)

3.5 Chroma — 클라이언트 측 결합

Chroma는 네이티브 하이브리드가 없다. BM25는 외부(rank_bm25, Whoosh, Tantivy)로 돌리고 결과를 클라이언트에서 RRF로 합친다.

import chromadb
from rank_bm25 import BM25Okapi

collection = chromadb.PersistentClient("./data").get_collection("docs")

# 1) 시맨틱
sem = collection.query(query_embeddings=[q_emb], n_results=40)
sem_ids = sem["ids"][0]

# 2) BM25
tokenized = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized)
bm_scores = bm25.get_scores(query.split())
bm_ids = [doc_ids[i] for i in sorted(range(len(bm_scores)),
                                       key=lambda i: -bm_scores[i])[:40]]

# 3) RRF
def rrf(rankings, k=60):
    scores = {}
    for ranked in rankings:
        for rank, doc_id in enumerate(ranked, start=1):
            scores[doc_id] = scores.get(doc_id, 0) + 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[1])

merged = rrf([sem_ids, bm_ids])[:10]

3.6 FAISS — IDMap2로 외부 ID 묶기

FAISS는 라이브러리이고, 텍스트 인덱스는 Tantivy/Whoosh를 직접 결합한다. 외부 ID를 살리려면 IndexIDMap2로 감싸야 한다.

import faiss, numpy as np

base = faiss.IndexHNSWFlat(768, 32)
index = faiss.IndexIDMap2(base)
index.add_with_ids(vectors, doc_ids.astype(np.int64))

D, I = index.search(query_vec.reshape(1, -1), 40)
# I[0]을 BM25 결과와 RRF로 결합

3.7 공통 함정 — prefetch 크기

각 검색의 top-K가 최종 K와 같으면 fusion이 거의 동작하지 않는다. 두 결과 집합의 교집합이 너무 작아지기 때문.

최종 결과 K 권장 prefetch (각 검색) 비고
10 30~50 안전한 기본
50 200~300 RAG 컨텍스트용
100+ 500+ reranker 앞단에 흔히 사용

4. 인덱싱 알고리즘 비교 — HNSW·IVFFlat·IVF-PQ·ScaNN·DiskANN·RaBitQ

ANN(Approximate Nearest Neighbor) 알고리즘 선택은 (recall, latency, RAM, build time, update cost)의 다차원 트레이드오프다. 데이터셋 크기와 워크로드 특성에 맞춰 골라야 한다.

4.1 알고리즘 핵심 비교

알고리즘 자료구조 주요 파라미터 메모리 빌드 속도 정확도 적합 규모
HNSW 계층 그래프 m=16, ef_construction=64, ef_search=40 높음 느림 매우 높음 ~100M
IVFFlat K-means + flat lists, nprobe 낮음 빠름 중간 ~10M
IVF-PQ K-means + PQ lists, M, nbits 매우 낮음 중간 중하 100M~1B
ScaNN anisotropic VQ num_leaves, reorder 중간 빠름 높음 ~1B
DiskANN (Vamana) SSD 그래프 R, L, α 매우 낮음 (RAM) 느림 높음 1B+
Annoy RP forest n_trees, search_k 중간 (mmap) 중간 중간 ~10M, 읽기 전용
RaBitQ 1bit/dim 압축 rotation seed 극저 빠름 중상 (rerank 필요) ~1B

4.2 HNSW — 그래프 기반의 표준

HNSW(Hierarchical Navigable Small World)는 다층 그래프를 만들어 상층에서 거칠게, 하층에서 정밀하게 탐색한다. m은 노드당 최대 연결 수, ef_construction은 빌드 시 후보 큐, ef_search는 검색 시 후보 큐 크기다.

파라미터 값 ↑ 효과 값 ↓ 효과
m 정확도 ↑, 메모리 ↑, 빌드 ↓ 정확도 ↓, 메모리 ↓
ef_construction 정확도 ↑, 빌드 매우 ↓ 정확도 ↓
ef_search recall ↑, latency ↑ latency ↓, recall ↓

FAISS docs 기준 HNSW 메모리 사용량은 (d * 4 + M * 2 * 4) bytes/vector다. 768d, m=16이면 벡터당 768*4 + 128 = 3,200 bytes.

4.3 IVFFlat — 클러스터 기반의 가성비

IVFFlat은 K-means로 데이터를 lists 개의 클러스터로 나누고, 검색 시 nprobe 개의 클러스터만 본다. pgvector docs의 권장 값:

  • lists ≈ rows / 1000 (1M 이하)
  • lists ≈ sqrt(rows) (1M 초과)
  • nprobe ≈ sqrt(lists) (시작값)

핵심 주의사항: IVFFlat은 데이터가 적재된 후에 빌드해야 K-means 클러스터링이 의미가 있다. HNSW는 빈 테이블에도 만들 수 있지만 IVFFlat은 그렇지 않다.

4.4 IVF-PQ — 메모리 30× 절감의 비결

Product Quantization은 d차원 벡터를 M개 서브벡터로 쪼개고, 각 서브공간에서 2^nbits 개 코드북을 학습해 각 서브벡터를 코드북 인덱스로 대체한다. 768d, M=96, nbits=8이면 벡터당 96 bytes만 저장.

import faiss

# d=768, lists=4096, M=96, nbits=8
quantizer = faiss.IndexFlatIP(768)
index = faiss.IndexIVFPQ(quantizer, 768, 4096, 96, 8)
index.train(training_vectors)  # 학습 필수
index.add(vectors)
index.nprobe = 32

4.5 RaBitQ — 1비트 양자화의 최신 기법

FAISS 최신 권장 옵션. 각 차원을 1비트로 압축 후 약간의 오버헤드(d/8 + 8 bytes/vec)만 추가. 768d 벡터가 104 bytes로 줄어들고, multi-bit 변형(RaBitQN, N=2~9)으로 정확도-크기 트레이드오프 조절 가능. IVFK,RaBitQfsN 형태로 IVF와 결합해 FastScan 변형 사용이 표준.

4.6 DiskANN — 십억 단위에서의 유일한 선택지

Microsoft Research가 발표한 Vamana 그래프 기반. 그래프 일부만 RAM, 나머지는 SSD에 두고 검색. Milvus·Vespa·일부 클라우드 검색이 채택. RAM 비용이 십억 단위 데이터셋에서 비현실적일 때 유일한 현실적 선택지.

4.7 FAISS 공식 결정 트리

FAISS docs의 권장 흐름을 요약하면:

조건 권장 index_factory
검색 횟수 ≤ 10k Flat (직접 계산)
정확 결과 필수 Flat
RAM 충분, 데이터 ≤ 1M HNSWM (M=16~64)
RAM 약간 절약, ≤ 1M IVFK,Flat (K=4√N~16√N)
RAM 크게 절약 OPQM_D,IVFK,PQMx4fsr
최대 압축 IVFK,RaBitQfsN
1M ~ 10M ...,IVF65536_HNSW32,...
10M ~ 100M ...,IVF262144_HNSW32,...
100M ~ 1B ...,IVF1048576_HNSW32,...

5. 영속성 / 파일 / 인메모리 모드

벡터 DB를 운영에 올릴 때 가장 자주 빠뜨리는 질문은 "이 DB가 죽으면 데이터가 살아남는가, ACID인가, PITR 되는가"다. 라이브러리(FAISS)에서 트랜잭션 DB(pgvector)까지 스펙트럼이 너무 넓다.

5.1 영속성 비교 표

DB 영속성 모델 인메모리 전용 파일 형식 트랜잭션 PITR/백업
pgvector Postgres heap + WAL ❌ (Postgres가 그 자체) Postgres data dir ✅ ACID ✅ WAL 기반 PITR
Qdrant mmap 디스크 (기본) 컬렉션 단위 on_disk: false RocksDB + segment 최종 일관성 snapshot
Weaviate LSM-tree 디스크 ❌ (embedded도 디스크) LSM segments 컬렉션 단위 backup module
Milvus Standalone/Distributed Lite는 단일 파일 Parquet + etcd + MinIO 컬렉션 단위 binlog 기반
Chroma SQLite + parquet EphemeralClient chroma.sqlite3 + bin 제한적 파일 복사
FAISS 인메모리 본질 ✅ 기본 write_index 직렬화 파일 복사

5.2 클라이언트 모드 매트릭스

DB 클라이언트 패턴 용도
Chroma EphemeralClient() / PersistentClient(path) / HttpClient(host) 테스트 / 로컬 / 프로덕션
Qdrant :memory: / 로컬 디스크 / 클라우드 단위 테스트 / 로컬 / 프로덕션
Milvus Lite (단일 파일) / Standalone / Distributed 노트북 / 단일 노드 / 클러스터
FAISS 메모리 only (write_index/read_index로 직렬화) 라이브러리 임베드
Weaviate embedded (별도 프로세스) / Docker / Cloud 데모 / 자체 호스팅 / SaaS
pgvector Postgres 인스턴스만 있으면 됨 모든 곳

5.3 pgvector의 결정적 이점

Postgres 위에 올라간다는 사실 자체가 다음을 공짜로 준다:

  • ACID 트랜잭션 — 벡터와 메타데이터를 같은 트랜잭션에 묶을 수 있다
  • JOINdocuments JOIN users JOIN permissions 같은 쿼리가 그냥 된다
  • PITR (Point-in-Time Recovery) — WAL 기반 시점 복구
  • Row-Level Security — 멀티테넌트 SaaS의 표준 격리

다른 벡터 DB는 이걸 흉내 내려면 별도 OLTP DB와 dual-write를 해야 하고, 그 순간 dual-write 일관성 문제가 들어온다.

5.4 인메모리 전용 사용처

시나리오 적합
단위 테스트 Chroma EphemeralClient, Qdrant :memory:, FAISS
람다/엣지 함수 FAISS (read_index로 mmap), Annoy
Notebook 데모 Chroma, Milvus Lite
프로덕션 ❌ — 영속 모드로 전환 필수

6. 인메모리 메모리 사용량 추정 — 1M × 768/1536d 기준

"이 데이터셋이 RAM에 들어갈까?"는 가장 자주 묻고 가장 자주 틀리는 추정 문제다. 정확한 계산은 (벡터 raw 크기) + (인덱스 오버헤드) + (페이로드/메타데이터) + (Python 객체 오버헤드)로 나눠야 한다.

6.1 Raw 벡터 크기 — 베이스라인

dtype 바이트/차원 1M × 768d 1M × 1536d
float32 4 2.93 GB 5.86 GB
float16 (halfvec) 2 1.46 GB 2.93 GB
int8 (SQ8) 1 0.73 GB 1.46 GB
binary (1-bit) 0.125 92 MB 184 MB

계산: N * d * sizeof(dtype). 1M = 1,000,000. 1 MiB = 1,048,576 bytes 기준. 마케팅 수치는 보통 1MB=10^6 사용해 약간 작게 나옴.

6.2 인덱스 오버헤드 추가

인덱스 벡터당 오버헤드 1M × 768d 추가 RAM (float32 기준)
Flat 0 0 (벡터 자체만)
HNSW (m=16) m × 2 × 4 = 128 bytes ~122 MB
HNSW (m=32) m × 2 × 4 = 256 bytes ~244 MB
IVFFlat list id 4 bytes + centroid (작음) ~4 MB + α
IVF-PQ (M=96, 8bit) PQ code 96 bytes (raw 대체) ~92 MB (raw 대체)
RaBitQ (1bit) d/8 + 8 = 104 bytes ~99 MB (raw 대체)

HNSW 식은 FAISS docs의 (d * 4 + M * 2 * 4) bytes/vector 공식에서 추출했다. 768d, m=16이면 768*4 + 128 = 3,200 bytes/vec, 1M ≈ 3.05 GB. 즉 벡터 raw(2.93 GB) + 그래프 오버헤드(122 MB).

6.3 시나리오별 종합 추정 (1M 벡터)

구성 768d 1536d
float32 + HNSW(m=16) ~3.05 GB ~5.98 GB
float16 + HNSW(m=16) ~1.58 GB ~3.05 GB
int8 + HNSW(m=16) ~0.85 GB ~1.58 GB
float32 + IVFFlat ~2.94 GB ~5.87 GB
IVF-PQ (M=96) ~96 MB ~192 MB
RaBitQ ~99 MB ~196 MB

6.4 정확도 손실 가이드 (대략적 경향, 데이터 분포 의존)

양자화 메모리 절감 recall 손실 (일반적 경향)
fp16 50% < 1%p
SQ8 (int8) 75% 1~3%p
PQ8 (M=d/8, 8bit) ~94% 5~10%p (reranking으로 회복)
RaBitQ 1bit ~97% 10~20%p (reranking으로 회복)
binary ~97% 데이터 의존, 흔히 큰 손실

운영 RAM 안전 마진: 위 인덱스 수치 × 1.52.0. Python 객체, 페이로드, 메타데이터, OS 페이지 캐시, 동시 쿼리 버퍼가 모두 추가된다. 1M × 768d float32 HNSW를 운영에 올린다면 RAM은 최소 68 GB 잡는 게 안전.

6.5 PQ/RaBitQ는 "공짜 점심"이 아니다

압축으로 RAM은 30× 줄지만:

  • 재정렬(rerank)이 필요 — 압축 결과 상위 K×r개를 원본 또는 fp16으로 다시 정렬해야 recall이 회복됨. 이때 원본도 어딘가에 보관해야 함 (SSD든 fp16 사본이든).
  • CPU 비용 증가 — 코드북 lookup이 캐시 미스를 자주 일으킴.
  • 빌드 시 학습 데이터 필요 — 데이터 분포가 바뀌면 재학습.

7. 2025-2026 시장 점유율과 채택

경고: 벡터 DB 시장은 변동이 크고 정확한 점유율 지표가 부족하다. 아래 수치는 일반적 추세이며 "확인 필요" 항목은 의사결정 근거로 단독 사용하지 말 것.

7.1 GitHub stars 추세 (대략적, 정확한 최신 수치는 확인 필요)

프로젝트 대략적 star 규모 추세
FAISS 30k+ 안정적
Milvus 30k+ 꾸준한 증가
Chroma 15k+ 빠른 증가 (2023~2024)
Qdrant 20k+ 빠른 증가
Weaviate 10k+ 꾸준한 증가
pgvector 15k+ 매우 빠른 증가

정확한 최신 star 수는 확인 필요. star 자체가 프로덕션 채택의 좋은 지표는 아니다.

7.2 DB-Engines vector DBMS 카테고리

DB-Engines는 2023년부터 vector DBMS를 별도 카테고리로 추적한다. 상위에 Pinecone, Milvus, Qdrant, Weaviate, Chroma가 일관되게 등장하지만, 정확한 순위와 점수는 매월 변동되므로 최신값은 DB-Engines 사이트에서 직접 확인 필요.

7.3 LangChain/LlamaIndex 통합 통계

자료에 따라 편차가 있으나 통합 빈도 기준 상위는 통상 pgvector, Chroma, Pinecone, Qdrant, Weaviate — 정확한 순위는 확인 필요.

7.4 공개된 프로덕션 사례

회사/제품 사용 스택 출처 신뢰도
Supabase Vector pgvector 공식 (직접 제공)
Cloudflare Vectorize 자체 인프라 공식
Notion AI 자체 인프라 공식 블로그 (세부는 비공개)
Perplexity / Cohere 자체 인프라 비공개 영역 다수
한국 시장 (Naver/Kakao) Milvus·pgvector 사례 공개됨 일부 공개, 정확한 점유율은 확인 필요

7.5 점유율로 결정하지 말 것

권장: 점유율 순위로 벡터 DB를 고르는 것은 위험하다. 자체 워크로드(쿼리 패턴, 데이터 크기, 운영 인프라)로 PoC 벤치마크를 돌리는 것이 훨씬 신뢰 가능하다. ann-benchmarks.com 같은 표준 벤치마크는 출발점일 뿐.


8. 보안 비교 — 인증·암호화·RBAC·격리

벡터 DB에 임베딩이 들어간다는 건 원본 텍스트(임베딩 inversion으로 복원 가능)와 메타데이터가 한 곳에 모인다는 뜻이다. PII, 의료 정보, 내부 문서를 다룬다면 보안 모델을 비교해야 한다.

8.1 보안 기능 비교 표

DB 인증 암호화 (전송/저장) RBAC 멀티테넌시 네트워크 격리
pgvector (Postgres) role + password, IAM, mTLS TLS / TDE (옵션) ✅ role + RLS RLS 표준 VPC, pg_hba.conf
Qdrant API key, JWT (v1.9+), mTLS TLS / 디스크 암호화는 OS 책임 RBAC (Cloud) payload 기반 Cloud VPC peering
Weaviate API key, OIDC, RBAC TLS / 디스크 OS tenant per collection Cloud VPC
Milvus user/role (v2.3+) TLS / TLS ✅ RBAC DB/collection 단위 Zilliz VPC peering
Chroma 토큰 (서버 모드) TLS / 디스크 OS 제한적 (Cloud 워크스페이스) Cloud 워크스페이스 Cloud
FAISS ❌ 없음 (라이브러리) ❌ 없음 호스팅 레이어 책임

8.2 pgvector + RLS — 멀티테넌트의 표준

alter table documents enable row level security;

create policy tenant_isolation on documents
  using (tenant_id = current_setting('app.current_tenant')::uuid);

-- 애플리케이션은 매 세션마다:
set app.current_tenant = '...uuid...';

이 패턴 하나로 SQL injection·테넌트 누수 방지가 동시에 된다. prepared statement까지 쓰면 OWASP A03(Injection) 대부분이 해결된다.

8.3 임베딩 특유의 위협

위협 설명 완화책
Embedding inversion 임베딩에서 원본 텍스트 일부 복원 가능 페이로드/원본 텍스트는 별도 암호화 저장, 필요한 만큼만 노출
Prompt injection via metadata 검색된 메타데이터에 악성 지시어 삽입 시스템 프롬프트에 포함될 필드를 화이트리스트로 제한
인덱스 파일 유출 FAISS/Chroma 파일 단독으로 외부 노출 디스크 암호화, 파일 권한 600, S3 SSE
Cross-tenant data leak 멀티테넌트에서 필터 누락 RLS / payload filter 강제, 클라이언트 신뢰 금지
Membership inference 특정 문서 색인 여부 추론 응답에 score 노출 최소화, rate limit

8.4 OWASP LLM Top 10 매핑

OWASP LLM 벡터 DB 관련성 대응
LLM01 Prompt Injection 검색 결과 메타데이터 경유 출력 필드 화이트리스트
LLM06 Sensitive Info Disclosure 임베딩/페이로드 누출 인증, 암호화, RLS
LLM08 Excessive Agency 에이전트가 DB에 직접 쓰기 읽기 전용 키, 별도 쓰기 서비스
LLM10 Model Theft 임베딩 대량 추출 rate limit, 쿼리 로깅, 이상 탐지

8.5 FAISS는 보안 책임이 다르다

FAISS는 라이브러리이므로 인증·암호화·RBAC가 없다. 보안은 100% 호스팅 레이어 책임:

  • 애플리케이션 레벨 인증
  • 디스크 암호화 (LUKS/FileVault/EBS encryption)
  • 인덱스 파일 권한 (600, 단일 프로세스 사용자)
  • 네트워크 격리 (FAISS 노출 금지 — 항상 애플리케이션 뒤에)

9. 워크로드별 의사결정 체크리스트 + 안티패턴

추상적인 비교는 끝났다. 실무에서 마주치는 시나리오별로 결정 경로를 정리한다.

9.1 워크로드 → DB 매핑

시나리오 추천 스택 이유
Postgres 이미 운영 중 + 데이터 < 10M pgvector + tsvector (Supabase 패턴) 인프라 추가 0, ACID, JOIN, RLS 공짜
10M~100M + 다중 임베딩 모델 Qdrant (named vectors) 또는 Milvus 모델별 named vector를 한 컬렉션에
100M+ self-host Milvus distributed 또는 Qdrant cluster + IVF-PQ/RaBitQ 수평 확장, 압축
빠른 PoC + Python only Chroma PersistentClient 30초 셋업
메모리 극도 제약 (엣지/람다) FAISS + IVF-PQ 또는 RaBitQ + mmap 라이브러리, 외부 의존 없음
멀티테넌트 SaaS pgvector RLS 또는 Qdrant payload 격리 검증된 격리 모델
십억 단위 + SSD DiskANN 기반 (Milvus / Vespa) RAM 비용 현실화
코드 검색 (정확 토큰 우세) Postgres tsvector 또는 Tantivy + 작은 dense BM25 알파 ↑
다국어 RAG pgvector / Qdrant + 다국어 임베딩 + BM25 alpha 낮춤 시맨틱 우세

9.2 구현 전 체크리스트

데이터 규모

  • 1년 후 예상 벡터 수, 차원, 평균 페이로드 크기 추정?
  • 인덱스 RAM 추정 완료? (벡터 raw + 인덱스 오버헤드 × 1.5)
  • 메타데이터/JOIN 필요? (필요 → pgvector 우선 고려)

검색 품질

  • 정확 매칭(코드/식별자) 비중 추정?
  • 하이브리드 필요성 검증 (단일 검색 baseline vs RRF 베이스라인)?
  • 평가 데이터셋 50~200개 준비?

운영

  • 백업/복구 절차?
  • 멀티테넌트 격리 모델? (RLS / payload / collection)
  • 인증 방식? (mTLS / JWT / API key)
  • 모니터링 (latency P99, recall@k, RAM 사용량)?

보안

  • PII 포함 여부, 페이로드 분리 저장?
  • 임베딩 inversion 위험 평가?
  • 검색 결과 메타데이터 → LLM 프롬프트 화이트리스트?

9.3 안티패턴 카탈로그

❌ 안티패턴 ✅ 권장
1M 이하인데 distributed cluster 셋업 Standalone / pgvector로 충분
BM25와 코사인 점수를 직접 가중합 RRF로 랭크 기반 결합
LIKE '%foo%' 풀스캔으로 키워드 검색 tsvector + GIN
IVFFlat에 nprobe=1 (기본값) 방치 nprobe ≈ sqrt(lists)부터 튜닝
HNSW ef_search 한 번도 안 만짐 recall 부족 시 100~200으로 ↑
빈 테이블에 IVFFlat 인덱스 생성 데이터 로드 후 빌드, 또는 HNSW
RRF prefetch K = 최종 K prefetch는 K × 3~5
페이로드에 raw PII 평문 저장 별도 암호화 저장, ID만 색인
FAISS 인덱스 파일을 그냥 S3 public에 KMS + signed URL, 또는 비공개
매 쿼리마다 nprobe/ef_search 동적 설정 A/B로 고정값 튜닝 후 SET
임베딩 모델 바꾼 뒤 인덱스 재빌드 안 함 차원/분포 다르면 무조건 재빌드
Chroma EphemeralClient를 프로덕션에 PersistentClient 또는 HttpClient
HNSW m을 일단 64로 크게 잡기 16부터 시작, recall 부족 시 ↑
검색 메타데이터를 그대로 LLM에 주입 필드 화이트리스트, prompt injection 방어
두 검색 결과 INNER JOIN으로 RRF FULL OUTER JOIN, 한쪽만 hit해도 보존

9.4 마지막 — 측정 없이 고르지 말 것

벡터 DB 선택의 가장 큰 함정은 "유명하니까", "X사가 쓰니까" 결정하는 것이다. 자체 데이터 1만10만 샘플로 평가셋을 만들고, 후보 23개에서 (recall@10, P99 latency, RAM, 운영 복잡도)를 측정하라. 일주일 PoC가 1년의 마이그레이션 비용을 막는다.

참고 문헌