11장. 폐쇄망 문서 파이프라인과 폐쇄형 RAG
벡터DB 고르기
반찬은 밀폐 용기에 담을 수도 있고, 집 냉장고에 정리할 수도 있고, 가게라면 공동 물류 창고를 빌릴 수도 있습니다. 담을 양과 쓰는 사람 수에 따라 고릅니다. 벡터DB 도 라이브러리, 내 프로그램 안에서 도는 내장형 DB, 따로 띄우는 서버형 DB 로 나뉩니다.
왼쪽 라이브러리(FAISS)는 벡터와 번호만 다루고 원문과 라벨은 우리가 따로 들고 있습니다. 가운데 내장형 DB(Chroma, Qdrant 의 로컬 모드)는 원문과 메타데이터까지 파일로 저장하고 서버가 필요 없습니다. 오른쪽 서버형 DB 는 따로 띄운 서버에 여러 프로그램이 접속해 씁니다. 여기서 밀폐 용기가 라이브러리, 집 냉장고가 내장형, 공동 물류 창고가 서버형입니다.
견주는 기준
| 기준 | FAISS | Chroma | Qdrant(로컬 모드) | 서버형(Qdrant 서버, Milvus, pgvector) |
|---|---|---|---|---|
| 서버 | 없음 | 없음(서버로도 띄울 수 있다) | 없음 | 있다. 컨테이너나 DB 로 띄운다 |
| 원문과 메타데이터 | 우리가 따로 | 함께 저장 | 함께 저장(payload) | 함께 저장 |
| 메타데이터로 거르기 | 번호 목록으로만 | where 조건 | Filter 조건 | 조건, SQL(pgvector) |
| 지우기와 고치기 | 번호를 붙여 넣었을 때만 | id 와 조건으로 | id 와 조건으로 | id 와 조건으로 |
| 큰 규모(수천만 개 이상) | 빠른 인덱스가 많다 | 작은~중간 규모 | 작은 규모(서버로 옮겨 키운다) | 여러 대로 나눠 키운다 |
| 폐쇄망 반입 | 휠 하나 | 휠 여럿 | 휠 여럿 | 컨테이너 이미지나 DB 패키지 |
이 장에서는 서버 없이 도는 셋(FAISS, Chroma, Qdrant 로컬 모드)을 실습 4에서 같은 조각으로 견줍니다. 서버형은 Docker 나 DB 서버가 있어야 해서 이 PC 에서 돌리지 않았습니다.
Qdrant 의 생김새
Qdrant 는 QdrantClient(":memory:") 나 QdrantClient(path="폴더") 로 부르면 서버 없이 돕니다. 점(point)마다 id, 벡터, payload(메타데이터)를 넣습니다.
from qdrant_client import QdrantClient, models
qc = QdrantClient(":memory:") # 서버 없이 메모리 안에서 돈다
qc.create_collection("demo", vectors_config=models.VectorParams(size=3, distance=models.Distance.COSINE))
qc.upsert("demo", points=[
models.PointStruct(id=1, vector=[1, 0, 0], payload={"제목": "휴가 규정", "등급": 1}),
models.PointStruct(id=2, vector=[0.9, 0.1, 0], payload={"제목": "인사평가 기준", "등급": 3}),
])
# 등급 1 이하만 보며 [1, 0, 0] 과 가까운 것을 찾는다
hits = qc.query_points("demo", query=[1, 0, 0], limit=2,
query_filter=models.Filter(must=[models.FieldCondition(key="등급", range=models.Range(lte=1))])).points
print([(h.id, h.payload["제목"], round(h.score, 3)) for h in hits])
qc.close()코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
QdrantClient(":memory:") | Qdrant 를 서버 없이 메모리 안에서 띄운다. 프로그램이 끝나면 내용이 사라진다 |
create_collection("demo", vectors_config=…) | 컬렉션(collection, 모음)을 만든다. 표 하나에 해당한다 |
models.VectorParams(size=3, distance=…) | 벡터의 설정이다. size 는 벡터의 길이(차원), distance 는 가까움을 재는 법이다 |
models.Distance.COSINE | 코사인(cosine) 유사도로 잰다. 두 벡터의 방향이 같을수록 1 에 가깝다 |
upsert("demo", points=[…]) | update(고치기)와 insert(넣기)를 합친 말이다. 같은 id 가 있으면 고치고 없으면 넣는다 |
models.PointStruct(id=…, vector=…, payload=…) | 점(point) 하나다. payload 는 "실은 짐"이라는 뜻으로, 벡터에 딸린 메타데이터다 |
query_points("demo", query=…, limit=2, query_filter=…) | 질문 벡터와 가까운 점을 최대 limit 개 찾는다. query_filter 로 조건을 건다 |
models.Filter(must=[…]) | must(반드시)에 적은 조건을 모두 지키는 점만 남긴다 |
models.FieldCondition(key="등급", range=models.Range(lte=1)) | "등급" 칸이 범위 안인지 본다. lte 는 less than or equal(작거나 같다)이다 |
.points, h.id, h.payload, h.score | 찾은 점들의 목록과, 점마다의 id, 메타데이터, 점수다 |
qc.close() | 쓴 뒤 클라이언트를 닫아 자원을 돌려준다 |
이 코드가 뜻하는 것
집 냉장고에 반찬통을 넣는 일로 읽어 봅니다. 반찬통마다 "휴가 규정, 누구나", "인사평가 기준, 어른만" 같은 라벨을 붙여 넣습니다. 아이가 "이것과 비슷한 반찬 두 개 줘"라고 하면, 먼저 "누구나" 라벨이 붙은 통만 고르고 그 가운데 맛이 비슷한 것을 꺼냅니다. 여기서 반찬통이 점, 라벨이 payload, 맛이 벡터, "누구나만"이 Filter 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
qc = QdrantClient(":memory:") | 서버 없는 Qdrant 를 띄운다 | 냉장고 전원을 켠다 |
qc.create_collection(...) | 길이 3, 코사인으로 재는 모음을 만든다 | 반찬 칸을 하나 비운다 |
qc.upsert(...) | 점 두 개를 라벨과 함께 넣는다 | 라벨 붙인 반찬통 두 개를 넣는다 |
qc.query_points(..., query_filter=...) | 등급 1 이하만 보며 가까운 것을 찾는다 | "누구나" 통 가운데 비슷한 맛 |
print([...]) | 찾은 점의 id, 제목, 점수를 찍는다 | 꺼낸 반찬통 |
qc.close() | 클라이언트를 닫는다 | 냉장고 문을 닫는다 |
교안을 만든 PC 에서는 [(1, '휴가 규정', 1.0)] 하나만 나왔습니다. limit=2 로 두 개를 달라고 했지만, 둘째 점은 등급이 3 이라 조건에서 빠졌습니다. 거르기가 검색 안에서 먼저 일어나서, 조건에 맞는 점이 모자라면 limit 보다 적게 돌아온다는 뜻입니다.
작게 줄여서 눈으로 보기
조건이 없었다면 둘째 점은 얼마나 가까웠을지, 코사인 유사도를 NumPy 로 직접 재 봅니다.
import numpy as np
q = np.array([1, 0, 0])
for name, v in [("휴가 규정", [1, 0, 0]), ("인사평가 기준", [0.9, 0.1, 0])]:
v = np.array(v)
cos = q @ v / (np.linalg.norm(q) * np.linalg.norm(v))
print(name, round(float(cos), 3))휴가 규정 1.0
인사평가 기준 0.994| 결과 | 읽는 법 |
|---|---|
휴가 규정 1.0 | 질문과 방향이 똑같다. Qdrant 가 준 점수와 같다 |
인사평가 기준 0.994 | 거의 같은 방향이라 조건이 없었다면 둘째로 나왔을 것이다 |
np.linalg.norm 은 linear algebra(선형대수)의 노름(norm), 곧 벡터의 길이를 잽니다. 두 벡터의 곱을 두 길이의 곱으로 나누면 코사인 유사도입니다. 0.994 처럼 아주 가까운 비밀 문서도 등급 조건 하나로 결과에서 빠졌습니다. 권한은 점수가 아니라 조건으로 지킨다는 뜻입니다.
주의할 점
- 처음에는 작게 시작합니다. 문서 수만 개까지는 내장형으로 충분하고, 여러 서비스가 함께 쓰거나 수백만 개를 넘을 때 서버형으로 옮깁니다. 같은 임베딩을 다시 넣으면 되므로 옮기는 일은 어렵지 않습니다.
- 서버형 DB 는 폐쇄망에 컨테이너 이미지와 운영 방법(백업, 권한, 감시)까지 들여와야 합니다. 들여올 것과 지킬 것이 늘어납니다.