RAG 챗봇 만들기 7 - PostgreSQL + pgvector

learning by Seven Fingers Studio 15분
RAGPostgreSQLpgvector벡터DBLangChainDocker

블로그 대표 이미지

회사에서 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 챗봇 만들기 시리즈:

← 블로그 목록으로