본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로AI › 언어 모델 › 응용

12. 벡터 데이터베이스로 확장하기 : RAG 구현하기

목차

벡터 데이터베이스로 확장하기 : RAG 구현하기

문서가 세 개라면 모든 벡터와 질문을 비교해도 된다. 문서가 많아지고 갱신이 잦아지면 “어떤 청크가 최신인가”, “누가 읽을 수 있는가”, “상위 후보를 얼마나 빨리 찾는가”를 함께 다뤄야 한다. 벡터 데이터베이스는 이 검색 단계를 맡는다. RAG(Retrieval-Augmented Generation)는 찾아온 문서를 언어 모델에 근거로 전달해 답을 만드는 흐름이다.

flowchart TD
  A[원본 문서] --> B[청크 생성 및 메타데이터]
  B --> C[임베딩 모델]
  C --> D[(벡터 색인)]
  Q[사용자 질문] --> E[접근 권한 확인]
  E --> F[질문 임베딩]
  F --> G[권한 필터를 적용한 검색]
  D --> G
  G --> H[원문 확인 및 재정렬]
  H --> I[근거와 질문을 LLM에 전달]
  I --> J[답변과 출처]

저장할 것은 벡터만이 아니다

청크마다 document_id, chunk_id, 원문, 제목, 문서 버전, 갱신 시각, 권한 범위, 임베딩 모델 버전을 함께 보관한다. 같은 문서를 다시 수집할 때는 기존 청크를 교체하거나 삭제해야 한다. 오래된 청크와 새 청크가 동시에 검색되는 문제는 색인 알고리즘을 바꿔도 해결되지 않는다.

다음은 PostgreSQL과 pgvector의 흐름을 보여주는 3차원 장난감 데이터다. 실제 모델의 출력 차원과 테이블 선언의 차원은 일치해야 하며, 예시 벡터는 의미 학습의 결과가 아니다.

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE knowledge_chunks (
    chunk_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id bigint NOT NULL,
    document_id text NOT NULL,
    title text NOT NULL,
    content text NOT NULL,
    embedding vector(3) NOT NULL,
    model_version text NOT NULL,
    updated_at timestamptz NOT NULL
);

INSERT INTO knowledge_chunks
    (tenant_id, document_id, title, content, embedding, model_version, updated_at)
VALUES
    (42, 'refund-v2', '환불 정책', '결제 후 7일 이내 취소할 수 있다.', '[0.9,0.1,0.0]', 'demo-v1', now()),
    (42, 'shipping-v1', '배송 정책', '출고 전 배송지를 바꿀 수 있다.', '[0.1,0.9,0.0]', 'demo-v1', now());

-- 애플리케이션에서는 tenant_id와 질문 벡터를 바인딩 매개변수로 전달한다.
SELECT chunk_id, document_id, title, content,
       embedding <=> '[1,0,0]'::vector AS cosine_distance
FROM knowledge_chunks
WHERE tenant_id = 42 AND model_version = 'demo-v1'
ORDER BY embedding <=> '[1,0,0]'::vector
LIMIT 5;

<=>는 코사인 거리이므로 작을수록 가깝다. 위 숫자 벡터는 연산자를 설명할 뿐 검색 품질을 보여주지 않는다. 운영에서는 사용자에게 허용된 tenant_id와 문서 권한을 서버가 검증한 뒤 쿼리에 넣어야 한다. 질문 문자열이나 권한 ID를 SQL 문자열에 이어 붙이지 말고 매개변수로 전달한다.

정확 검색부터 근사 색인까지

pgvector는 기본적으로 정확한 최근접 검색을 하고, HNSW와 IVFFlat 색인을 추가해 빠른 근사 검색을 할 수 있다. 데이터가 작을 때는 정확 검색을 기준선으로 삼는다. 데이터와 지연 목표가 커졌을 때 아래와 같이 같은 거리 연산에 맞는 색인을 만든다.

CREATE INDEX knowledge_chunks_tenant_idx ON knowledge_chunks (tenant_id);
CREATE INDEX knowledge_chunks_embedding_hnsw_idx
    ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);

HNSW는 검색 속도와 재현율의 균형을 조정하는 매개변수가 있지만, 색인 생성 시간과 메모리를 더 쓴다. IVFFlat은 구축 비용이 상대적으로 낮지만 목록 수와 탐색 범위를 조절해야 한다. 필터가 많은 데이터에서는 근사 검색 후 필터링 때문에 LIMIT 5를 요청해도 결과가 적을 수 있다. pgvector 공식 문서는 필터 열 색인, 부분 색인, 분할, 반복 색인 탐색을 제시한다. 기능과 매개변수는 설치 버전의 pgvector README를 확인한다.

근사 검색을 도입하기 전후에 같은 질문 집합으로 Recall@5와 p95 검색 시간을 함께 측정한다. “빠르다”는 이유만으로 정답 문서가 사라지는 설정을 선택하면 RAG의 답변 품질이 떨어진다.

검색 결과를 답변 근거로 전달하기

상위 청크를 그대로 모델에 모두 넣기보다 중복을 제거하고 질문에 직접 답하는 부분을 선택한다. 오래되거나 권한이 없는 문서는 여기까지 오면 안 된다. 생성 모델에는 다음처럼 역할을 분명히 준다.

질문: 결제 후 며칠 안에 취소할 수 있나요?

참고 문서:
[1] 환불 정책 (refund-v2): 결제 후 7일 이내 취소할 수 있다.
[2] 배송 정책 (shipping-v1): 출고 전 배송지를 바꿀 수 있다.

지시: 참고 문서에 있는 사실로만 답하고, 사용한 문서 번호를 표시한다.
문서에서 답을 찾을 수 없으면 확인이 필요하다고 말한다.

이 예시에서는 “결제 후 7일 이내입니다. [1]”가 근거 있는 답이다. [2]를 인용하면 문서가 검색되었더라도 인용 품질은 실패다. 인용 번호는 모델의 문장만 믿지 말고 서버에서 실제 검색 결과 ID와 연결한다.

운영 시 점검할 실패 지점

증상검사할 단계
새 정책이 반영되지 않음수집 시각, 청크 교체, 색인 갱신
검색은 성공했는데 답이 틀림재정렬, 프롬프트에 전달한 원문, 인용 검증
다른 조직의 문서가 보임서버 권한 검증과 DB 접근 정책
정확 검색과 근사 검색의 결과가 다름색인의 재현율, 필터 선택도, 검색 매개변수
문서가 없는데 단정적으로 답함근거 부족 시 보류하는 규칙과 평가 질문

RAG는 모델의 지식을 자동으로 교정하는 장치가 아니다. 검색, 권한, 근거 확인, 생성 중 어느 한 단계라도 실패할 수 있다. 운영 대시보드에서도 각 단계를 나누어 관찰해야 한다.

문서가 바뀔 때 색인은 어떻게 맞출까?

환불 기한이 7일에서 14일로 바뀌었다면 새 문서를 넣는 것만으로는 부족하다. 이전 청크가 남아 있으면 검색 결과에 7일과 14일이 같이 나오고 모델이 어느 쪽을 선택할지 불확실해진다. 새 원문을 먼저 검증하고, 같은 임베딩 모델 버전으로 새 청크의 벡터를 만든다. 그다음 문서 ID와 버전을 기준으로 오래된 청크를 제거하고 새 청크를 추가하는 작업을 하나의 트랜잭션으로 묶는다. 색인이 갱신될 때까지의 지연과 실패 재시도도 기록한다.

BEGIN;
DELETE FROM knowledge_chunks
WHERE tenant_id = 42 AND document_id = 'refund-v2';
INSERT INTO knowledge_chunks
    (tenant_id, document_id, title, content, embedding, model_version, updated_at)
VALUES
    (42, 'refund-v2', '환불 정책', '결제 후 14일 이내 취소할 수 있다.',
     '[0.95,0.05,0.0]', 'demo-v1', now());
COMMIT;

위 SQL의 벡터는 앞 절과 같은 설명용 3차원 값이며 새 규정을 실제로 인코딩한 결과가 아니다. 실제 서비스에서는 refund-v2의 이전 버전과 새 버전을 어떻게 식별하는지 정하고, 새 청크 생성에 실패하면 기존 상태를 유지하도록 한다. 삭제와 삽입을 별도 트랜잭션으로 실행하면 잠시 답변 근거가 사라질 수 있다. 문서 삭제 요청을 받았을 때도 원문 저장소와 벡터 색인에서 모두 제거됐는지 확인한다.

권한 필터는 검색 품질의 일부다

질문 “우리 팀의 장애 대응 시간은?”을 임베딩하면 다른 팀의 문서도 의미상 가깝다. 사용자의 권한을 검사한 뒤 검색 범위를 좁혀야 다른 팀 문서가 모델 입력에 들어가지 않는다. 위 예시의 tenant_id = 42 값은 브라우저가 보낸 임의 숫자가 아니라 인증된 사용자와 서버의 권한 정책에서 결정해야 한다. 조직 안에 문서별 공개 범위가 있다면 tenant만으로도 충분하지 않다. 애플리케이션 필터와 데이터베이스 행 수준 보안 등 중복 방어를 검토한다.

근사 색인은 내부적으로 후보를 먼저 찾고 필터를 적용할 수 있다. 따라서 한 조직의 문서가 전체의 1%밖에 없다면 LIMIT 5를 요청해도 허용 문서가 5개보다 적게 남을 수 있다. 이때 모델에게 “관련 문서가 없다”고 답하게 하기 전에 정확 검색 기준선과 비교한다. 필터 열 색인, 부분 색인, 파티션 또는 pgvector의 반복 스캔 설정이 필요한지 판단한다. pgvector 필터링 문서는 이런 선택지를 설명한다.

거리와 답변 품질을 함께 읽기

가상의 검색에서 환불 청크의 코사인 거리가 0.12, 배송 청크가 0.36이라고 하자. 거리가 작은 환불 청크가 더 가깝지만, 0.12가 정답일 확률 88%라는 뜻은 아니다. 모델 버전과 코퍼스가 바뀌면 거리 분포도 바뀐다. 고정된 거리 문턱값으로 무조건 답하게 하기보다, 관련 문서가 없는 질문을 포함한 평가셋에서 문턱값과 재정렬 규칙을 고른다.

가상의 100개 질문에서 정확 검색은 정답 문서를 90개, HNSW는 87개 찾고 검색 p95가 각각 140ms와 25ms라고 하자. 이 세 건의 손실이 허용되는지는 도메인에 달려 있다. 정답이 없는 문서로 그럴듯한 답을 만들 위험이 큰 법무·운영 규정이라면 빠른 검색보다 정답 청크 보존을 우선할 수 있다. 선택한 검색 결과가 최종 답변에서 올바르게 인용됐는지도 별도 측정한다.

참고 자료

같은 카테고리의 글