지난 1월, Pinecone 청구서를 열었다가 잠깐 멈췄다. 월 $340. 우리 팀은 나 포함 3명이고, 사이드 프로젝트 겸 내부 도구 정도로 RAG를 쓰는데 — 프로덕션 트래픽도 많지 않다 — 이게 맞나 싶었다. 그 순간부터 pgvector를 진지하게 고민하기 시작했다.
2주 동안 두 시스템을 병렬로 돌렸다. 결론부터 말하면, 지금은 Pinecone 구독을 끊었다. 단순히 비용 때문만은 아니었다.
Pinecone이 불편해진 진짜 이유
처음엔 Pinecone이 당연한 선택이었다. 설정이 빠르고, 문서가 깔끔하고, 벡터 검색 결과도 나쁘지 않았다. 그런데 쓰다 보니 작은 마찰들이 쌓이기 시작했다.
가장 큰 문제는 데이터 이중화였다. 우리 앱은 PostgreSQL을 메인 DB로 쓰는데, 문서를 저장할 때마다 Postgres에 한 번, Pinecone에 한 번 — 두 군데에 써야 했다. 동기화가 깨지는 경우도 몇 번 있었다. 금요일 오후에 배포했다가 임베딩 파이프라인 일부가 조용히 실패하면서 Pinecone 인덱스가 stale 상태로 남아있던 적이 있는데, 그걸 월요일 아침에 사용자 제보로 알았다. 모니터링을 제대로 안 한 내 잘못이기도 하지만, 시스템이 하나였다면 이런 종류의 버그는 없었을 거다.
솔직히, 메타데이터 필터링도 생각보다 제약이 많았다. Pinecone 필터 문법은 쓸 만은 한데, 복잡한 조건이 들어가면 SQL이 훨씬 자연스럽다는 걸 계속 느꼈다. “이 사용자가 업로드한 문서 중에서, 날짜가 최근 30일 이내이고, 특정 카테고리에 속하는 것”을 벡터 유사도로 검색하는 쿼리 — 이걸 Pinecone 필터로 표현할 때마다 괜히 복잡하다는 생각이 들었다.
pgvector 셋업: 생각보다 덜 고통스러웠다
솔직히 pgvector 셋업에 대해 걱정을 많이 했다. 인덱싱 전략, 차원 수, HNSW vs IVFFlat — 미리 조사하다 보니 선택지가 많아서 오히려 부담스러웠다.
실제로 해보니 기본 셋업은 30분이면 됐다.
-- pgvector 0.7.0 기준 (2024년 말 릴리즈)
CREATE EXTENSION IF NOT EXISTS vector;
-- 기존 documents 테이블에 컬럼 추가
ALTER TABLE documents ADD COLUMN embedding vector(1536);
-- HNSW 인덱스 생성 (IVFFlat보다 쿼리 속도 일관성이 좋음)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 실제 검색 쿼리
SELECT
id,
content,
metadata,
1 - (embedding <=> $1::vector) AS similarity
FROM documents
WHERE user_id = $2
AND created_at > NOW() - INTERVAL '30 days'
ORDER BY embedding <=> $1::vector
LIMIT 10;
이게 전부다. 복잡한 SDK 없이, 별도 서비스 없이, 그냥 SQL이다. user_id와 날짜 필터가 자연스럽게 WHERE 절로 들어가는 게 — 그게 왜 이렇게 반갑던지.
임베딩 생성 쪽은 기존 OpenAI API 코드를 그대로 썼다. 바뀐 건 저장 위치뿐이었다.
한 가지 솔직히 말하면, m과 ef_construction 파라미터를 처음엔 기본값으로 뒀다가 나중에 조정했다. 데이터셋 특성에 따라 다르고, 정직하게 말하면 이 값들을 완벽하게 이해하고 설정한 게 아니라 pgvector GitHub 이슈 몇 개 읽고 우리 규모에 맞게 타협한 거다. 정밀한 튜닝이 필요하다면 직접 벤치마킹을 해봐야 한다.
2주간의 A/B: 실제 숫자
두 시스템을 2주간 병렬로 돌리면서 같은 쿼리를 두 곳에 날렸다. 우리 데이터셋은 약 85만 개 벡터, 1536차원, OpenAI text-embedding-3-small 모델 기준이다.
쿼리 레이턴시 (p50 / p95)
- Pinecone: 45ms / 120ms
- pgvector (HNSW, ef_search=40): 38ms / 95ms
예상 밖이었다. 솔직히 pgvector가 느릴 거라고 생각했다. 완전히 틀렸다. 우리 규모에서는 오히려 pgvector가 더 빨랐는데, Pinecone의 네트워크 왕복 시간이 생각보다 컸던 것 같다. 물론 수억 개 벡터 규모라면 달라질 거다 — 거기까지는 테스트를 못 했다.
검색 품질
이건 수치화가 어려워서 정성적으로만 평가했다. 같은 쿼리에 대해 두 시스템이 반환하는 상위 10개 문서를 비교했는데, 실질적인 차이를 못 느꼈다. 코사인 유사도 기준으로 같은 임베딩 모델을 쓰면 결과가 비슷한 게 당연하긴 하다.
운영 비용
- Pinecone (s1.x1 파드): 월 $340
- pgvector (기존 RDS PostgreSQL에 추가): 추가 비용 거의 없음 (인스턴스 업그레이드 없이 처리)
이 부분은 상황마다 다르다. 이미 PostgreSQL을 쓰고 있다면 pgvector는 사실상 무료에 가깝다. PostgreSQL을 새로 띄워야 한다면 계산이 달라진다.
예상 못 한 함정들
마냥 순탄하지는 않았다.
VACUUM이 문제였다. 임베딩을 대량 업데이트하는 배치 작업을 돌리고 나서 HNSW 인덱스가 bloat되면서 쿼리가 갑자기 느려지는 현상을 겪었다. 처음엔 이유를 몰랐다. REINDEX CONCURRENTLY로 해결했는데, 이게 운영 중에 잠깐 CPU를 꽤 잡아먹었다. Pinecone에서는 이런 걸 신경 쓸 필요가 없었으니까 — 처음에는 이 부분이 좀 아쉬웠다.
그리고 ef_search 파라미터. 인덱스 생성 시의 ef_construction과 별개로, 쿼리 시점에 ef_search를 설정할 수 있는데 이게 정확도와 속도의 트레이드오프를 직접 컨트롤한다. 처음에 이걸 몰라서 기본값으로 뒀다가 recall이 생각보다 낮게 나왔다. SET hnsw.ef_search = 100;으로 올리니 훨씬 나아졌지만, 이 파라미터가 세션 레벨이라는 것도 처음엔 혼란스러웠다.
-- 세션별로 설정하거나, postgresql.conf에 전역으로
SET hnsw.ef_search = 100;
-- 또는 특정 쿼리에만 적용하고 싶다면
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT ... FROM documents ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;
이런 세부 사항들이 Pinecone 쓸 때는 추상화되어 있었는데, pgvector에서는 직접 다뤄야 한다. 컨트롤이 많다는 건 그만큼 알아야 할 것도 많다는 뜻이다.
pgvector가 맞는 팀, Pinecone이 맞는 팀
이건 도구의 우열 문제가 아니라 상황의 문제다.
이미 PostgreSQL을 쓰고 있고 벡터가 수천만 개 미만이라면 — pgvector가 맞다. 복잡한 메타데이터 필터링이 필요한 경우엔 더더욱. 데이터가 한 곳에 있다는 단순함이 생각보다 크다. 동기화 이슈도 없고, 별도 SDK도 없고, 인프라 청구서 항목도 하나 줄어든다. 우리 같은 소규모 팀에게 이게 꽤 의미 있게 다가왔다.
반대로 수억 개 이상의 벡터를 다뤄야 하거나, PostgreSQL을 아예 안 쓰는 스택이거나, 벡터 검색이 서비스 핵심이라서 직접 튜닝에 시간 쓰기 싫다면 — Pinecone 같은 전용 서비스가 낫다. 관리형 서비스의 가치는 결국 운영에서 나오니까. Pinecone의 namespaces나 멀티 테넌시 기능이 훨씬 성숙해 있는 것도 사실이다.
내가 100% 확신하지 못하는 부분은 pgvector의 스케일 한계다. 1억 개 벡터 이상에서 어떻게 동작하는지는 직접 경험이 없다. pgvector 0.7.x에서 스트리밍 HNSW 빌드가 개선됐다고는 하지만, 그 규모를 다뤄야 한다면 직접 벤치마킹하고 결정해야 한다.
그래서 나는 어떻게 하나
pgvector로 옮겼고, 만족하고 있다. 청구서 걱정이 사라진 것도 있지만, 그보다 코드베이스가 단순해진 게 더 좋다. 임베딩 파이프라인, 동기화 로직, 별도 SDK — 이것들이 사라지니까 머릿속이 좀 가벼워졌다.
이전을 고려한다면 한 가지만 챙기길 권한다: 마이그레이션 전에 recall 벤치마크를 직접 돌려보는 것. 데이터셋 특성에 따라 HNSW 파라미터 튜닝이 꽤 달라질 수 있고, 적당한 ef_search 값을 미리 파악해두는 게 나중에 훨씬 편하다.
PostgreSQL을 전혀 안 쓰는 스택이라면 pgvector를 도입하기 위해 새로 PostgreSQL을 띄우는 건 과한 선택이다. 그 경우엔 Pinecone이나 Weaviate가 더 맞다. 하지만 이미 Postgres가 있다면 — 솔직히, 왜 안 쓰나 싶다.