2026년 Postgres pgvector를 활용한 RAG: 전체 TypeScript 파이프라인
요약
Postgres의 pgvector 확장을 사용하여 별도의 벡터 데이터베이스 없이 RAG 파이프라인을 구축하는 방법을 설명합니다. 인프라 복잡성을 줄이면서도 HNSW 인덱스를 통해 효율적인 유사도 검색을 구현하는 실무적인 가이드를 제공합니다.
핵심 포인트
- pgvector를 사용하면 추가 인프라 없이 기존 Postgres에서 벡터 검색 가능
- HNSW 인덱스를 활용해 대규모 데이터에서도 빠른 유사도 검색 구현
- 운영 부담과 비용을 줄이기 위해 단일 데이터베이스 통합 권장
- pgvector 0.8.0의 필터링된 유사도 검색 기능 활용 가능
2026년 Postgres pgvector를 활용한 RAG: 전체 TypeScript 파이프라인.
저는 전용 벡터 데이터베이스 (Vector Database)를 평가하는 데 일주일을 보낸 끝에, 이미 보유하고 있는 Postgres 인스턴스를 그대로 사용하기로 결정했습니다. pgvector 확장은 대부분의 프로덕션 워크로드 (Production Workload)에 대해 유사도 검색 (Similarity Search)을 충분히 잘 처리하며, 세 가지 인프라 구성 요소를 하나로 통합해 줍니다. 이 가이드는 스키마 (Schema) 설정부터 답변 생성까지의 모든 과정을 다룹니다: 문서를 청킹 (Chunking)하고, 임베딩 (Embedding)하며, pgvector에 저장하고, 코사인 유사도 (Cosine Similarity)로 검색한 뒤, 그 결과를 LLM 호출에 연결하는 과정입니다.
요약 (TL;DR)
| 단계 | 도구 | 이유 |
|---|---|---|
| 벡터 저장소 활성화 | pgvector 0.8.x, HNSW 인덱스 (Index) | 기존 Postgres에서 실행되므로 추가 인프라가 필요 없음 |
| ... |
1. 전용 벡터 데이터베이스 대신 pgvector를 사용하는 이유
Pinecone과 Weaviate는 좋은 제품입니다. 만약 멀티 테넌트 격리 (Multi-tenant Isolation), 1억 개 이상의 벡터에 대한 밀리초 미만의 p99 성능, 또는 BM25를 이용한 네이티브 하이브리드 검색 (Hybrid Search)이 필요하다면 그럴 만한 가치가 있습니다. 하지만 대부분의 팀에게 이는 미래의 문제입니다.
운영 부담 (Ops Burden)을 고려하면 비용 계산 방식이 달라집니다. 전용 벡터 DB를 사용한다는 것은 새로운 결제 항목, 교체해야 할 새로운 자격 증명 세트, 추적해야 할 새로운 장애 모드, 그리고 애플리케이션에서 최신 상태로 유지해야 할 새로운 SDK를 의미합니다. pgvector는 Postgres 확장 기능 (Extension)으로 실행됩니다: 하나의 연결 문자열 (Connection String), 하나의 백업 전략, 하나의 단일 진실 공급원 (Source of Truth)만 있으면 됩니다. 1,536차원 임베딩을 가진 1,000만 개의 문서의 경우, 적절한 크기의 Postgres 인스턴스에서 HNSW 인덱스를 사용하면 10ms 이내에 상위 10개 결과를 반환합니다. 이는 RAG 사용 사례의 압도적인 부분을 커버합니다.
pgvector 0.8.0 버전에서는 반복적 HNSW 스캔 (Iterative HNSW Scans) 기능이 추가되었습니다. 이 릴리스를 통해 WHERE 절이 구체화될 때마다 매번 순차 스캔 (Sequential Scan)으로 돌아가지 않고도 필터링된 유사도 검색을 실용적으로 수행할 수 있게 되었습니다. 0.8.0 릴리스는 저희 팀이 "나중에 할까"에서 "지금 출시하자"로 마음을 돌리게 만든 결정적인 계기였습니다.
2. 스키마 설정 (Schema setup)
데이터베이스당 한 번 확장을 활성화한 다음, 테이블을 생성합니다.
-- pgvector 활성화 (데이터베이스당 한 번 실행)
CREATE EXTENSION IF NOT EXISTS vector;
...
HNSW와 IVFFlat 중 선택하기
HNSW는 탐색 가능한 스몰 월드 그래프 (navigable small-world graph)를 구축합니다. 쿼리는 모든 행을 비교하는 대신 그래프를 스캔합니다. 한 번 구축하면 즉시 쿼리가 가능합니다. 트레이드오프(tradeoff)는 인덱스가 더 많은 메모리를 차지한다는 점입니다. 기본 설정에서 1,536차원 컬럼의 경우 행당 차원당 약 8바이트가 소요됩니다.
IVFFlat은 임베딩 공간 (embedding space)을 중심점 클러스터 (centroid clusters)로 분할합니다. 구축 속도가 더 빠르고 메모리 점유율이 낮지만, 인덱스를 구축하기 전에 행을 먼저 로드해야 하며 그렇지 않으면 중심점 할당 (centroid assignment)이 무용지물이 됩니다. 데이터가 하나도 없는 상태에서 시작한다면 HNSW를 구축하십시오.
-- HNSW 인덱스 (권장 기본값)
-- m = 레이어당 연결 수 (기본값 16), 높을수록 메모리 비용이 증가하지만 재현율 (recall)이 향상됨
-- ef_construction = 구축 중 후보 리스트 (기본값 64), 높을수록 구축 속도는 느려지지만 재현율 (recall)이 향상됨
...
임베딩 모델이 벡터를 정규화 (normalize)하는 경우 (OpenAI와 Voyage 모두 해당), <=> 연산자와 함께 vector_cosine_ops를 사용하십시오. 벡터가 정규화되지 않은 상태에서 가공되지 않은 유클리드 거리 (Euclidean distance)를 구하려면 <->와 함께 vector_l2_ops를 사용하십시오. 내적 (inner product)을 위해서는 <#>와 함께 vector_ip_ops를 사용하십시오. 이는 정규화된 벡터에서 코사인 유사도 (cosine similarity)와 동일하며 정규화 단계를 하나 줄여줍니다.
3. TypeScript를 이용한 인제스트 (Ingest) 파이프라인
인제스트 (ingest) 함수는 문서를 청크 (chunk)로 나누고, 임베딩 API를 호출하며, 행을 대량 삽입 (bulk insert)합니다. 태그 템플릿 SQL (tagged-template SQL)과 네이티브 배열 지원을 위해 postgres (npm 패키지, pg 아님)를 사용하십시오.
import postgres from "postgres";
import OpenAI from "openai";
...
청크 크기 (chunk size)에 관한 참고 사항: 512단어는 시작점으로 적절합니다. 적절한 크기는 소스 자료에 따라 다릅니다. 문단이 밀집된 법률 문서는 256단어에서 더 좋은 성능을 보입니다. 코드 파일은 최소 300행이 필요하며, 그렇지 않으면 함수의 문맥 (context)을 놓치게 됩니다. 오버랩 (overlap)은 임베딩이 청크 경계에 걸쳐 있는 문장을 놓치는 것을 방지합니다.
4. TypeScript를 이용한 쿼리 (Query) 파이프라인
사용자의 질문을 임베딩하고, top-k 코사인 유사도 검색을 실행하여 일치하는 청크를 반환합니다.
export async function queryDocuments(
question: string,
topK = 5,
...
<=> 연산자는 코사인 거리 (cosine distance, 0 = 동일, 2 = 반대)를 반환합니다. 숫자가 낮을수록 우선순위가 높습니다. 메타데이터 필터 (metadata filters)를 추가하는 경우, 플래너 (planner)가 0.8.0 버전에서 도입된 HNSW 반복 스캔 (HNSW iterative scan)을 사용할 수 있도록 ORDER BY 이전에 WHERE 절에 추가하십시오.
// 필터링된 쿼리 예시 — 동일한 모델이 이 소스에 대한 결과를 반환했어야 함
const rows = await sql<{ source: string; content: string; distance: number }[]>`
SELECT source, content, (embedding <=> ${embeddingStr}::vector) AS distance
...
5. 검색된 문서를 LLM 호출에 연결하기
검색된 청크 (chunks)들을 하나의 컨텍스트 블록 (context block)으로 연결한 다음, 원하는 모델을 호출합니다. Claude 3.5 Sonnet 또는 GPT-4o 모두 긴 컨텍스트 (long contexts)를 잘 처리합니다. 비용 문제를 고려하여 컨텍스트 블록을 80,000 토큰 (tokens) 미만으로 유지하십시오.
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
...
"제공된 컨텍스트만을 사용하여 답변하세요"라는 지침은 매우 중요합니다 (load-bearing). 이 지침이 없으면 모델이 검색 결과와 파라미터 메모리 (parametric memory)를 혼합하여 사용하게 되며, 어떤 것이 무엇인지 구분할 수 없게 됩니다. 답변이 컨텍스트에서 나온 것이라면 인용 (citations)이 작동하지만, 학습 데이터 (training data)에서 나온 것이라면 작동하지 않습니다. 프롬프트 (prompt) 수준에서 이 둘을 명확히 구분하도록 강제하십시오.
한 가지 더 주목할 점은, LLM으로 보내기 전에 리랭크 (rerank)를 수행하라는 것입니다. 빠른 코사인 검색 (cosine search)은 벡터 거리 기준으로 가장 가까운 5개의 청크를 반환하지만, 거리가 항상 유용성을 의미하지는 않습니다. 크로스 인코더 리랭커 (cross-encoder reranker, Cohere Rerank는 1,000회 쿼리당 약 1달러 소요)는 상위 20개의 후보를 가져와 5개로 압축하기 전에 실제 관련성을 점수화합니다. 품질의 도약이 눈에 띄게 나타납니다. 프로토타이핑 단계에서는 리랭커를 건너뛰더라도, 프로덕션 (production)에 적용하기 전에는 반드시 추가하십시오.
6. 모두가 겪게 되는 두 가지 주의사항
인덱스 파라미터보다 청크 크기가 재현율 (recall)을 더 크게 좌우함
대부분의 팀은 HNSW의 m과 ef_construction 파라미터를 튜닝하는 데 수 시간을 소비하지만, 미미한 이득만을 얻습니다. 실제 핵심 레버(lever)는 청크 크기(chunk size)와 오버랩(overlap)입니다. 너무 짧은 청크는 문맥을 잃어버리며(모델이 문장 간의 관계를 묻는 질문에 답할 수 없음), 너무 긴 청크는 노이즈를 끌어들이고 임베딩(embedding)을 희석시키며 LLM 호출 시 컨텍스트 윈도우(context window)를 낭비합니다. 빠른 평가(eval)를 수행해 보십시오. 대표적인 질문 20개를 선정하여 상위 5개(top-5)를 검색한 다음, 답변이 반환된 청크에 포함되어 있는지 수동으로 점수를 매깁니다. 재현율(recall)이 85%를 넘을 때까지 100단어 단위로 청크 크기를 조정하십시오. 그 후에 인덱스를 튜닝하십시오.
대량 로드(bulk loading) 후에 인덱스를 생성하십시오, 그 전이 아니라
삽입(insert) 시점에 HNSW 인덱싱을 수행하는 것은 느립니다. 만약 500,000개의 문서를 로드하는데 HNSW 인덱스가 이미 존재한다면, 모든 INSERT 작업마다 그래프 업데이트 비용이 발생합니다. 빠른 방법은 인덱스를 삭제한 상태로 모든 행을 로드한 다음, CREATE INDEX를 통해 한 번에 생성하는 것입니다. 1,536차원 임베딩을 가진 500,000행 테이블의 경우, 4 vCPU 환경에서 콜드(cold) HNSW 빌드는 대략 8분에서 12분이 소요됩니다. 이는 누적된 삽입 오버헤드보다 훨씬 저렴합니다.
-- 대량 로드 전에 인덱스를 삭제합니다
DROP INDEX IF EXISTS documents_embedding_idx;
...
결론
전체 파이프라인은 약 120줄의 TypeScript와 3개의 SQL 문으로 구성됩니다. pgvector 0.8.x는 프로덕션(production) 환경에 충분히 안정적이며, HNSW는 대부분의 팀에게 적절한 기본 인덱스입니다. 답변 품질에 가장 중요한 두 가지는 청크 크기와 삽입 시점의 임베딩(embed-at-ingesting) 및 쿼리 시점의 임베딩(embed-at-querying) 사이의 일관성을 유지하는 것(동일한 모델, 동일한 전처리)입니다. 전용 벡터 DB(vector DB)가 틀린 것은 아니지만, 행 수가 5,000만 개를 넘거나 재현율 요구 사항이 튜닝 팀을 고용해야 할 정도로 엄격해지기 전까지는 굳이 필요하지 않은 레이어일 뿐입니다.
여러분의 유스케이스(use case)에서는 어떤 청크 크기가 가장 효과적이었나요? 댓글로 알려주세요.
GDS K S · thegdsks.com · X에서 팔로우 @thegdsks
훌륭한 검색(retrieval)은 언제나 더 나은 모델을 이깁니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기