요약
pgvector 0.8 시리즈(2024-10-30 첫 릴리스 → 2026-07-29 0.8.6 최신)는 PostgreSQL 안에서 프로덕션급 벡터 검색을 가능하게 한 두 가지 핵심 기능을 가져왔다. 첫째는 반복 인덱스 스캔(iterative index scan) — WHERE 절 필터가 많은 후보를 걸러내더라도 HNSW/IVFFlat 인덱스가 스스로 더 깊이 탐색해 요청 개수를 채운다. 둘째는 sparsevec 타입 — BM25·SPLADE 같은 희소(sparse) 임베딩을 0이 아닌 값만 저장해 메모리와 디스크를 절약한다. 두 기능을 조합하면 밀집·희소 벡터를 하나의 테이블에 두고, 필터 조건이 복잡한 상황에서도 정확한 k-NN 결과를 돌려줄 수 있다.
배경: 왜 0.7 → 0.8이 중요한가
0.6 이전 pgvector는 단순했다. vector 타입, L2/코사인/내적 세 가지 연산자, HNSW·IVFFlat 두 인덱스. 그것만으로도 소규모 사용에는 충분했지만, 실무에서는 두 가지 문제가 자주 터졌다.
문제 1 — 필터 후 후보 고갈. WHERE user_id = 42 ORDER BY embedding <=> query LIMIT 10처럼 필터를 붙이면, HNSW가 처음 꺼내는 ef_search개 후보 중 필터를 통과하는 게 10개 미만일 수 있다. 결과가 적게 나오거나 아예 0개가 된다.
문제 2 — 희소 임베딩 저장 낭비. BM25나 SPLADE의 출력은 수만 차원에 걸쳐 99% 이상이 0.0이다. 기존 vector 타입은 모든 0까지 float4로 저장하므로 100,000차원 벡터는 ~400KB를 차지한다.
0.7.0은 sparsevec·halfvec·bit 타입으로 두 번째 문제를 해결했다. 0.8.0은 반복 스캔으로 첫 번째 문제를 해결했다. 0.8.x 패치 시리즈는 플래너 비용 추정·성능·안정성을 꾸준히 개선했고, 2026-07-29 출시된 0.8.6이 현재 최신 버전이다.
네 가지 벡터 타입
pgvector 0.8.x가 제공하는 타입은 총 네 가지다.
| 타입 | 저장 단위 | 최대 차원 | 대표 용도 |
|---|---|---|---|
vector | float4 (4B) | 16,000 | OpenAI, Cohere 밀집 임베딩 |
halfvec | float2 (2B) | 16,000 | 메모리 절약, 정밀도 소폭 감소 |
bit | 1bit | 64,000 | 이진 해싱, Hamming 거리 |
sparsevec | (index, float4) pairs | 구성 가능 | BM25, SPLADE, 범주형 원-핫 |
sparsevec 실전
-- 100,000차원 sparsevec 컬럼
ALTER TABLE documents ADD COLUMN sparse_emb sparsevec(100000);
-- 파이썬에서 삽입 (scipy sparse → pgvector 표현)
from pgvector.psycopg import SparseVector
row.sparse_emb = SparseVector(100000, {42: 0.9, 1337: 0.3, 99999: 0.1})sparsevec는 {index:value, ...}/dimensions 리터럴로 직렬화된다. 내부 표현은 비-0 인덱스 배열과 값 배열의 쌍이므로, 100,000차원 중 300개만 비-0이라면 저장 크기는 ~2.4KB로 줄어든다.
sparsevec가 지원하는 연산자는 현재 내적(<#>)과 코사인(<=>)이다. L2는 미지원이므로 BM25/SPLADE용으로는 코사인이나 내적을 쓴다.
여섯 가지 거리 연산자
반복 인덱스 스캔: 핵심 원리
문제: 필터 후 후보 고갈
HNSW는 그래프를 탐색하며 가장 가까운 ef_search개 후보를 꺼낸다. 그런데 WHERE tenant_id = 'acme' 같은 필터를 PostgreSQL이 적용하면, ef_search개 중 상당수가 걸러진다. 결과가 10개 미만이 되면 PostgreSQL은 조용히 부족한 개수를 반환한다.
해결: iterative scan
0.8.0부터 hnsw.iterative_scan을 활성화하면 HNSW가 배치 단위로 추가 탐색을 반복한다. 필터를 통과한 후보가 LIMIT를 채울 때까지 계속 그래프를 더 깊이 뒤진다.
-- 세션 레벨
SET hnsw.iterative_scan = strict_order;
-- 또는 IVFFlat
SET ivfflat.iterative_scan = on;strict_order는 거리 정렬 순서를 보장하고, relaxed_order는 약간의 순서 완화를 허용하는 대신 더 빠르다. 필터 선택도(selectivity)가 매우 높을 때는 relaxed_order가 실용적이다.
최대 탐색 제한
무한 탐색을 막기 위해 두 파라미터가 있다.
-- HNSW: 최대 스캔 후보 수 (기본값 20000)
SET hnsw.max_scan_tuples = 50000;
-- IVFFlat: 최대 프로브 수 (기본값 lists 수)
SET ivfflat.max_probes = 100;max_scan_tuples에 도달하면 탐색을 멈추고 그때까지 모인 결과를 반환한다. 쿼리가 LIMIT를 채우지 못할 수 있다는 뜻이므로, 애플리케이션에서 반환 개수를 검증해야 한다.
동작 흐름
비용 추정 개선 (0.8.x)
0.8.0 이전에는 플래너가 HNSW 인덱스 비용을 과소 추정해 순차 스캔(seqscan)을 선택하거나, 반대로 B-tree 복합 인덱스가 더 효율적인 상황에서 HNSW를 선택하는 일이 있었다.
0.8.x는 필터 선택도에 따른 비용 추정 로직을 개선했다. 실제 운영에서는 EXPLAIN (ANALYZE, BUFFERS) 로 어느 인덱스가 선택되는지 확인하고, 필요하면 SET enable_seqscan = off 또는 인덱스 힌트로 강제한다.
-- 플래너 결정 확인
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, content
FROM documents
WHERE tenant_id = 'acme'
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;출력에서 Index Scan using documents_embedding_idx 가 보여야 한다. Seq Scan이 나오면 테이블 통계(ANALYZE)가 오래됐거나 인덱스 파라미터 조정이 필요하다.
DBA 프로덕션 체크리스트
인덱스 생성
-- HNSW: m(그래프 연결도)과 ef_construction(빌드 품질)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- halfvec 사용 시 메모리 절반으로 절약
CREATE INDEX ON documents USING hnsw (embedding halfvec_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- sparsevec (내적)
CREATE INDEX ON documents USING hnsw (sparse_emb sparsevec_ip_ops)
WITH (m = 16, ef_construction = 64);m을 높이면 정확도↑ 빌드 시간↑ 메모리↑, ef_construction을 높이면 인덱스 품질↑ 빌드 시간↑. 기본값 m=16, ef_construction=64는 좋은 시작점이다.
쿼리 파라미터 튜닝
-- ef_search: 검색 시 후보 수 (기본값 40)
SET hnsw.ef_search = 100; -- 정확도↑ 속도↓
-- iterative scan 활성화
SET hnsw.iterative_scan = strict_order;
-- 최대 스캔 한계 (필터 선택도에 맞게 조정)
SET hnsw.max_scan_tuples = 40000;필터 선택도가 1%라면 LIMIT 10을 채우려면 평균 1,000개 후보가 필요하다. max_scan_tuples는 그보다 여유 있게 설정한다.
메모리 설정
-- HNSW 인덱스 빌드 시 shared_buffers를 활용
-- postgresql.conf
maintenance_work_mem = '2GB' -- 인덱스 빌드 시 사용
work_mem = '256MB' -- 쿼리 정렬 버퍼정기 유지관리
-- 벡터 통계 갱신 (기본 ANALYZE는 벡터 컬럼 샘플링 부족)
ALTER TABLE documents ALTER COLUMN embedding SET STATISTICS 1000;
ANALYZE documents;
-- 인덱스 재구축 (INSERT/DELETE 많을 경우)
REINDEX INDEX CONCURRENTLY documents_embedding_idx;
-- bloat 확인
SELECT pg_size_pretty(pg_relation_size('documents_embedding_idx')) AS idx_size;멀티테넌트 필터 패턴
테넌트마다 벡터 품질이 다른 경우 파티셔닝이 효과적이다.
-- tenant_id 기반 파티션 테이블
CREATE TABLE documents (
id bigserial,
tenant_id text NOT NULL,
embedding vector(1536),
content text
) PARTITION BY LIST (tenant_id);
-- 테넌트별 파티션 + 개별 HNSW 인덱스
CREATE TABLE documents_acme PARTITION OF documents FOR VALUES IN ('acme');
CREATE INDEX ON documents_acme USING hnsw (embedding vector_cosine_ops);파티션 프루닝(pruning)이 작동하면 플래너는 acme 테넌트 쿼리에서 해당 파티션 인덱스만 스캔한다. iterative scan과 조합 시 필터 비용이 크게 줄어든다.
하이브리드 검색: 밀집 + 희소 결합
BM25(희소)와 벡터 임베딩(밀집)을 결합하는 패턴은 검색 품질을 높인다. pgvector 0.8.x에서는 단일 테이블로 구현한다.
CREATE TABLE documents (
id bigserial PRIMARY KEY,
content text,
dense_emb vector(1536), -- OpenAI text-embedding-3-small
sparse_emb sparsevec(100000) -- SPLADE 희소 벡터
);
CREATE INDEX ON documents USING hnsw (dense_emb vector_cosine_ops);
CREATE INDEX ON documents USING hnsw (sparse_emb sparsevec_ip_ops);-- 하이브리드 RRF(Reciprocal Rank Fusion) 쿼리
WITH dense AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY dense_emb <=> $1) AS rank
FROM documents LIMIT 50
),
sparse AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY sparse_emb <#> $2) AS rank
FROM documents LIMIT 50
)
SELECT d.id, 1.0/(60+d.rank) + 1.0/(60+s.rank) AS rrf_score
FROM dense d JOIN sparse s USING (id)
ORDER BY rrf_score DESC
LIMIT 10;RRF 가중치 60은 관행적 기본값이다. 도메인에 따라 조정한다.
버전별 주요 변경 요약
| 버전 | 날짜 | 주요 변경 |
|---|---|---|
| 0.7.0 | 2024-01 | halfvec, bit, sparsevec 타입 추가 |
| 0.8.0 | 2024-10-30 | iterative scan, 비용 추정 개선, HNSW 빌드 성능↑ |
| 0.8.1 | 2024-11 | 필터 비용 추정 버그 수정 |
| 0.8.2 | 2025-01 | 플래너 안정성 개선 |
| 0.8.3–0.8.5 | 2025 | 보안 패치, PG 17 호환성 |
| 0.8.6 | 2026-07-29 | 현재 최신 — 안정성·성능 패치 |
제한사항과 주의사항
인덱스 지원 거리 연산자: 모든 타입이 모든 연산자를 지원하지 않는다. sparsevec는 코사인(<=>)과 내적(<#>)만 HNSW 인덱싱이 가능하다. L2 거리는 순차 스캔이다.
정확도 vs 속도 트레이드오프: ef_search가 낮으면 빠르지만 근사 오류가 커진다. 프로덕션 배포 전 recall@10 측정을 권장한다.
iterative scan 보장: max_scan_tuples에 도달하면 LIMIT보다 적은 결과가 올 수 있다. 애플리케이션이 이 경우를 처리해야 한다.
VACUUM 필요: DELETE가 많으면 HNSW 그래프에 dead tuple이 쌓인다. autovacuum 설정을 벡터 테이블에 맞게 조정한다(autovacuum_vacuum_scale_factor = 0.01).
Large vector dimensions: 3072차원(text-embedding-3-large) HNSW 인덱스는 메모리를 많이 쓴다. halfvec로 전환하면 메모리를 절반으로 줄이되 정밀도 손실이 미미하다.