기술 리드가 벡터 검색을 두고 가장 흔히 묻는 질문은 “벡터 데이터베이스가 정말 필요한가, 아니면 Postgres로도 되는가?“다. pgvector가 그 답이며, 나는 “Postgres로도 된다”는 말에 실제로 어떤 비용이 따르는지 확인하려고 **v0.8.4, 커밋 1d458ad**에서 코드를 읽었다. 놀라운 건 알고리즘이 아니다. pgvector는 내가 Qdrant에서 뜯어본 것과 같은 그래프인 HNSW를 쓴다. 놀라운 건 자리다. pgvector는 HNSW를 Postgres 인덱스 접근 방식으로 구현하며, 흥미로운 모든 것이 이 하나의 결정에서 따라 나온다. 모든 주장은 코드의 한 줄을 가리킨다.
Postgres 인덱스로서의 HNSW
pgvector는 내장 B-tree와 같은 방식으로 인덱스 유형을 등록한다. 핸들러 함수가 훅으로 가득 찬 IndexAmRoutine을 반환한다. hnswhandler는 (hnsw.c:266) .ambuild = hnswbuild, .aminsert = hnswinsert, .amrescan = hnswrescan, .amgettuple = hnswgettuple를 연결한다 (hnsw.c:298). 표준 인덱스 수명주기다. 특이한 플래그는 .amcanorderbyop = true 하나다 (hnsw.c:277). 이 인덱스는 동등성 조회가 아니라 연산자 기준 정렬 쿼리에 답한다. 거리 연산자 <->, <#>, <=>는 opclass에 FOR ORDER BY 전략-1 연산자로 등록되어 있다 (vector.sql:313).
그러니 벡터 검색은 그저 SQL이다. SELECT ... ORDER BY embedding <-> '[...]' LIMIT 10처럼 쓰면 플래너가 ORDER BY <->를 정렬된 인덱스 스캔으로 바꾼다. 결정적으로 이 인덱스가 하는 일은 이것뿐이다. order-by 절이 없으면 hnswcostestimate가 무한대 비용을 반환한다 (hnsw.c:147, “정렬 없이 인덱스를 절대 사용하지 않음”). HNSW 인덱스는 WHERE 조건자를 처리할 수 없다. 이 점을 기억해두자. 뒤에서 다룰 필터링 이야기의 전부다.
공짜로 얻는 것: Postgres의 내구성
그래프가 Postgres 인덱스이므로, 모든 Postgres 인덱스와 같은 방식으로 저장된다. 공유 버퍼 관리자를 거쳐 8 KB 페이지의 튜플로 저장된다. 그래프 노드는 자신의 레벨과 힙 TID, 별도 이웃 튜플을 가리키는 포인터를 담은 HnswElementTupleData다 (hnsw.h:357). 페이지는 전용 mmap이 아니라 ReadBuffer/MarkBufferDirty로 읽고 더럽힌다. 그리고 WAL에 기록된다. 빌드가 끝나면 전체 인덱스가 WAL에 기록되고 (hnswbuild.c:1138), 삽입이 건드리는 모든 페이지는 범용 WAL을 거친다 (hnswinsert.c:194).
“그냥 Postgres를 쓴다”는 선택이 실제로 주는 것은 바로 이 하나의 사실, 즉 인덱스가 WAL에 기록되고 버퍼로 관리되는 릴레이션이라는 점이다. 크래시 복구, 스트리밍 복제, 특정 시점 복구, 베이스 백업까지 벡터 인덱스가 모두 함께 올라탄다. Postgres가 이미 모든 릴레이션에 이 기능을 제공하기 때문이다. 또한 스캔은 힙 TID를 반환하고 실행기가 가시성을 검사하게 하므로 (xs_heaptid와 xs_recheck = false, hnswscan.c:323), 벡터 검색은 MVCC에 맞고 트랜잭션을 지킨다. 커밋되지 않은 행은 다른 세션의 검색에 보이지 않는다. 독립형 엔진은 이 모든 속성을 직접 구축해야 하지만, pgvector는 Postgres 인덱스이기 때문에 이를 물려받는다.
검색, 그리고 필터링 문제
스캔 자체는 교과서적인 HNSW다. 진입점에서 시작해 각 레이어를 탐욕적으로 내려간 뒤, 베이스 레이어에서 ef 너비의 빔 검색을 HnswSearchLayer와 hnsw.ef_search로 실행한다 (기본값 40, hnswscan.c:55). 각 hnswgettuple 호출마다 순전히 거리로 순위가 매겨진 힙 TID 하나를 스트리밍한다.
여기서 Postgres 인덱스라는 점이 발목을 잡는다. 이 인덱스는 필터를 모른다. order-by 키가 없으면 오류를 내고 쿼리 벡터만 읽으며, WHERE 절은 전혀 보지 않는다 (hnswscan.c:213). 따라서 WHERE tenant = 'acme' ORDER BY embedding <-> q LIMIT 10은 이렇게 동작한다. 인덱스가 필터를 무시하고 가장 가까운 행 40개를 반환하면, Postgres가 acme가 아닌 행을 버린다. acme가 데이터의 1%라면 40개 중 거의 전부가 버려져 결과를 10개보다 훨씬 적게 받을 수 있다. 전형적인 사후 필터 리콜 붕괴다.

pgvector의 HNSW 인덱스는 벡터 후보의 순위를 먼저 매긴다. 관계형 WHERE 필터는 나중에 Postgres 실행기가 적용한다.
pgvector 0.8의 답은 반복 스캔이다 (hnsw.c:97, 기본값 꺼짐). 작업 집합이 비면 멈추는 대신 (hnswscan.c:249) ResumeScanItems를 호출해 (hnswscan.c:280) 전에 버린 후보에서 그래프 탐색을 계속한다. 필터를 통과하는 결과가 충분해질 때까지 더 가져오되, hnsw.max_scan_tuples(기본값 20,000)와 작업 메모리 예산으로 한도를 둔다. relaxed_order(찾은 순서대로 반환, 더 빠름)나 strict_order(감소하지 않는 거리 순서로만 반환, hnswscan.c:313)를 고를 수 있다.
전용 엔진과의 차이가 선명하게 드러나는 지점이다. Qdrant는 필터 검색 문제를 빌드 시점에 공략한다. 필터링된 서브그래프가 연결된 상태를 유지하도록 페이로드를 인식하는 그래프 링크를 추가한다. pgvector의 그래프는 고정되어 있고 조건자를 모른다. 같은 문제를 쿼리 시점에 빔 검색을 재개하는 방식으로 공략한다. 선택은 다르지만 목표는 같다. 선택도 높은 필터가 리콜을 망가뜨리지 못하게 하는 것이다.
빌드와 조정값
빌드는 그래프를 maintenance_work_mem 한도까지 메모리에 유지하다가 더는 들어맞지 않으면 디스크 빌드로 전환한다 (hnswbuild.c:530). 따라서 maintenance_work_mem이 너무 작아도 실패하지는 않고, 그저 느려진다. 병렬 워커도 지원한다. 조정값은 인덱스 reloption과 GUC로 제공되는 익숙한 HNSW 항목들이다. m(기본값 16), ef_construction(기본값 64이며 2*m 이상이어야 함), 쿼리 시점의 ef_search(기본값 40)가 있다. pgvector는 더불어 HNSW보다 더 빠르게 빌드되고 메모리를 덜 쓰지만 리콜을 맞바꾸는 k-means/역 리스트 인덱스 (lists, probes)인 IVFFlat도 제공한다. 같은 확장에 들어 있는 또 하나의 고전적인 벡터 인덱스다.
이 선택은 지도 어디에 놓이는가?
벡터 데이터베이스의 축을 그린다면 나는 전용 엔진 대 데이터베이스 내장형으로 나누겠다. Qdrant는 전용 엔진이다. 자체 스토리지를 소유하고, 필터를 인덱스 구조와 쿼리 계획 모두의 일급 입력으로 취급한다. pgvector는 반대쪽에 건 선택이다. 벡터를 Postgres 데이터 유형으로, HNSW를 Postgres 인덱스로 만들어 임베딩이 나머지 데이터와 같은 트랜잭션 저장소에 살게 하고, 이미 운영 중인 장치가 이를 백업하고 복제하게 한다. 그 비용은 필터링이 그래프 안에 설계되는 대신 Postgres가 모든 것을 덧붙이는 방식(사후 필터, 이제 반복 스캔으로 보완됨)으로 덧붙는다는 것이다.
언제 이걸 선택해야 할까?
데이터가 이미 Postgres에 있고 두 번째 시스템을 운영하지 않으면서 벡터 검색을 원한다면 pgvector를 선택하자. 트랜잭션, 실제 테이블과의 조인, 하나로 통일된 백업 방식, 운영할 시스템 하나를 얻는다. 아주 많은 애플리케이션에는 이것이 필터 검색 성능의 마지막 몇 점보다 더 가치 있다. 경계는 알고 들어가야 한다. HNSW 빌드에는 maintenance_work_mem이 필요하고, 선택도 높은 WHERE 필터에서 리콜을 유지하려면 hnsw.iterative_scan을 켜야 하며, 규모가 아주 크거나 필터링이 무거우면 전용 엔진의 빌드 시점 필터 장치가 앞서 나간다. 이미 Postgres를 쓰고 있지 않다면 이런 상속의 논거 대부분이 사라지고, 전용 엔진이 다시 선택지에 들어온다.
방법론과 범위
나는 **pgvector v0.8.4, 커밋 1d458ad**를 읽었고, src/hnsw.c, hnswbuild.c, hnswinsert.c, hnswscan.c, hnswutils.c, SQL 설치 파일에 집중했다. 핵심 주장 세 가지, 즉 인덱스 접근 방식으로서의 HNSW, WAL 기록/페이지/MVCC 스토리지, 필터를 모르는 스캔과 반복 스캔을 통한 복구는 각각 이 커밋에서 적대적 주장 검증기를 거쳐 확인됐다.
범위의 단서를 명확히 적는다.
- “크래시 복구 / 복제 / 백업을 물려받는다”는 주장은 이 인덱스가 WAL에 기록되고 버퍼로 관리되는 릴레이션이라는 사실에서 따라온다. 이는 표준 Postgres 속성이며, 여기서 내가 벤치마크한 내용은 아니다.
- IVFFlat이 “빌드는 더 빠르고 리콜은 더 낮다”는 설명은 아키텍처적 특성(k-means + 프로브 수가 제한된 스캔)에 따른 것이며, 이 저장소에서 측정한 수치가 아니다.
- 나는 성능 비교가 아니라 인덱스 내부를 읽었다. 리콜과 지연 시간은 데이터,
ef_search, 반복 스캔 활성화 여부에 따라 달라진다.



