RAG 챗봇 만들기 7 - PostgreSQL + pgvector
회사에서 RAG 시스템을 구축할 때 처음엔 ChromaDB를 사용했다. 하지만 나중에 다른 데이터와 조인이 필요했고, 트랜잭션 관리도 필요해서 PostgreSQL로 교체했다. 이미 PostgreSQL을 사용하고 있다면 pgvector 확장만 추가하면 돼서 편리하다.
오늘은 실무에서 많이 사용하는 PostgreSQL + pgvector 조합을 다룬다.
pgvector가 뭔가?
PostgreSQL의 확장(extension)이다. 벡터 데이터를 저장하고 유사도 검색을 할 수 있게 해준다.
장점
- 기존 PostgreSQL과 통합: 다른 테이블이랑 JOIN 가능
- 트랜잭션 지원: ACID 보장
- 익숙한 SQL: 새로운 쿼리 언어 안 배워도 됨
- 프로덕션 검증: 많은 회사에서 사용 중
Docker로 PostgreSQL + pgvector 설치
가장 쉬운 방법이다.
docker-compose.yml 작성
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
container_name: rag-postgres
environment:
POSTGRES_USER: rag_user
POSTGRES_PASSWORD: rag_password
POSTGRES_DB: rag_db
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
실행
docker-compose up -d
실행 결과:
[+] Running 2/2
✔ Volume "rag_pgdata" Created
✔ Container rag-postgres Started
pgvector 확장 활성화
PostgreSQL에 접속해서 확장을 활성화한다:
docker exec -it rag-postgres psql -U rag_user -d rag_db
CREATE EXTENSION IF NOT EXISTS vector;
Python에서 사용하기
필요한 패키지 설치
pip install psycopg2-binary pgvector
LangChain으로 연결
from langchain_community.vectorstores import PGVector
from langchain_openai import OpenAIEmbeddings
from langchain.schema import Document
# 연결 문자열
CONNECTION_STRING = "postgresql+psycopg2://rag_user:rag_password@localhost:5432/rag_db"
# 임베딩 모델
embeddings = OpenAIEmbeddings()
# 테스트 문서
documents = [
Document(page_content="연차는 매년 15일 부여됩니다.", metadata={"source": "규정집", "page": 1}),
Document(page_content="병가는 연간 60일까지 사용 가능합니다.", metadata={"source": "규정집", "page": 2}),
Document(page_content="야근 시 저녁 식대 1만원이 지급됩니다.", metadata={"source": "복지", "page": 1}),
]
# 벡터 저장소 생성
vectorstore = PGVector.from_documents(
documents=documents,
embedding=embeddings,
connection_string=CONNECTION_STRING,
collection_name="company_docs" # 테이블 이름
)
print("✓ PostgreSQL에 벡터 저장 완료!")
실행 결과:
✓ PostgreSQL에 벡터 저장 완료!
유사 문서 검색
# 기존 벡터 저장소 연결
vectorstore = PGVector(
connection_string=CONNECTION_STRING,
embedding_function=embeddings,
collection_name="company_docs"
)
# 검색
query = "휴가 규정 알려줘"
results = vectorstore.similarity_search(query, k=2)
print(f"질문: {query}\n")
for doc in results:
print(f"- {doc.page_content}")
print(f" 출처: {doc.metadata}")
실행 결과:
질문: 휴가 규정 알려줘
- 연차는 매년 15일 부여됩니다.
출처: {'source': '규정집', 'page': 1}
- 병가는 연간 60일까지 사용 가능합니다.
출처: {'source': '규정집', 'page': 2}
메타데이터로 필터링
특정 조건의 문서만 검색할 수 있다:
# source가 "규정집"인 문서만 검색
results = vectorstore.similarity_search(
query="식대",
k=3,
filter={"source": "규정집"}
)
SQL로 직접 확인하기
PostgreSQL에서 저장된 데이터를 직접 확인할 수 있다:
-- 테이블 확인
SELECT * FROM langchain_pg_embedding LIMIT 5;
-- 벡터 차원 확인
SELECT vector_dims(embedding) FROM langchain_pg_embedding LIMIT 1;
-- 유사도 검색 (코사인 거리)
SELECT
document,
1 - (embedding <=> '[0.1, 0.2, ...]'::vector) as similarity
FROM langchain_pg_embedding
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 5;
인덱스 추가하기
데이터가 많아지면 인덱스가 필요하다:
-- HNSW 인덱스 (권장)
CREATE INDEX ON langchain_pg_embedding
USING hnsw (embedding vector_cosine_ops);
-- IVFFlat 인덱스 (대용량용)
CREATE INDEX ON langchain_pg_embedding
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
| 인덱스 | 특징 | 용도 |
|---|---|---|
| HNSW | 빠르고 정확함 | 일반적인 경우 |
| IVFFlat | 메모리 적게 씀 | 대용량 데이터 |
전체 파이프라인
PDF에서 PostgreSQL까지 전체 흐름:
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import PGVector
from langchain_openai import OpenAIEmbeddings
CONNECTION_STRING = "postgresql+psycopg2://rag_user:rag_password@localhost:5432/rag_db"
def ingest_pdf(pdf_path: str, collection_name: str):
"""PDF를 PostgreSQL에 저장"""
# 1. 로드
print(f"1. {pdf_path} 로드 중...")
loader = PyPDFLoader(pdf_path)
documents = loader.load()
# 2. 청킹
print("2. 텍스트 청킹 중...")
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
print(f" {len(chunks)}개 청크 생성")
# 3. PostgreSQL에 저장
print("3. PostgreSQL에 저장 중...")
embeddings = OpenAIEmbeddings()
vectorstore = PGVector.from_documents(
documents=chunks,
embedding=embeddings,
connection_string=CONNECTION_STRING,
collection_name=collection_name
)
print(f"✓ 완료! 컬렉션: {collection_name}")
return vectorstore
# 실행
vectorstore = ingest_pdf("docs/handbook.pdf", "employee_handbook")
# 검색 테스트
results = vectorstore.similarity_search("출장 규정", k=2)
for doc in results:
print(doc.page_content[:100])
기존 PostgreSQL에 pgvector 추가하기
이미 PostgreSQL을 사용하고 있다면 pgvector만 추가하면 된다:
-- pgvector 설치 (PostgreSQL 서버에서)
-- Ubuntu/Debian
sudo apt install postgresql-16-pgvector
-- 확장 활성화
CREATE EXTENSION vector;
운영자 실전 노트
실제 프로젝트 진행하며 겪은 문제
- Docker 포트 충돌: 5432 포트가 이미 사용 중이어서 에러 발생 →
docker-compose.yml에서5433:5432로 변경 - 연결 문자열 오류:
postgresql://대신postgresql+psycopg2://사용해야 LangChain에서 정상 작동 → 공식 문서 주의 깊게 확인 필요 - Extension 미활성화: 컨테이너 재시작 후
CREATE EXTENSION vector다시 실행해야 함 → 초기화 스크립트로 자동화 - 인덱스 없이 검색 느려짐: 1만 건 이상 데이터에서 검색이 5초 이상 소요 → HNSW 인덱스 추가하니 0.2초로 단축
이 경험을 통해 알게 된 점
- HNSW vs IVFFlat 선택 기준: 10만 건 이하는 HNSW, 그 이상은 IVFFlat이 메모리 효율적
- pgvector는 프로덕션 필수: 다른 테이블과 JOIN 가능하고 트랜잭션 지원으로 데이터 무결성 보장
- connection_string은 환경 변수로 관리: 코드에 하드코딩 금지,
.env파일 사용 필수
마무리
PostgreSQL + pgvector 조합이 실무에서 많이 쓰이는 이유가 있다. 기존 DB 인프라를 그대로 활용할 수 있고, SQL로 다른 데이터와 조인도 가능하며, 트랜잭션 관리도 된다. ChromaDB로 프로토타입을 만들고, 프로덕션은 pgvector로 전환하는 패턴이 일반적이다.
다음 편에서는 검색한 문서를 기반으로 LLM이 답변을 생성하는 부분을 다룬다. 드디어 챗봇이 대화할 수 있게 된다.
RAG 챗봇 만들기 시리즈:
← 블로그 목록으로