1부에서는 Pixel RAG가 무엇인지 알아봤고, 2부에서는 기존 Text RAG 시스템에 Pixel RAG를 추가하면 아키텍처가 어떻게 달라지는지 살펴봤다.
이번에는 한 단계 더 들어가 보자.
실제로 Pixel RAG의 Visual Retrieval은 어떻게 구현할까?
여기서 중요한 기술들이 등장한다.
- ColPali
- ColQwen 계열 Visual Retriever
- Multi-Vector Embedding
- Late Interaction
- Qdrant
- Vision-Language Model(VLM)
처음 보면 꽤 복잡해 보인다.
하지만 전체 흐름부터 보면 의외로 단순하다.
PDF
│
▼
Page Rendering
│
▼
Page Image
│
▼
Visual Document Retriever
│
▼
Visual Embedding
│
▼
Vector DB
│
▼
User Question
│
▼
Query Embedding
│
▼
Visual Retrieval
│
▼
Relevant Page
│
▼
VLM
│
▼
Answer
문제는 중간에 있는 Visual Embedding이 일반적인 Text Embedding과 조금 다르다는 것이다.
이 차이를 이해하면 Pixel RAG의 핵심 구조가 보이기 시작한다.
1. 일반적인 이미지 검색과 Pixel RAG는 조금 다르다
먼저 가장 단순한 이미지 검색을 생각해보자.
이미지 한 장을 Embedding Model에 넣는다.
Image
│
▼
Image Encoder
│
▼
[0.12, -0.31, 0.87, ...]
이미지 한 장이 하나의 Vector가 된다.
질문도 Vector로 만든다.
"매출이 가장 크게 증가한 제품은?"
│
▼
[0.32, -0.11, 0.72, ...]
그리고 Vector Similarity를 계산한다.
이 방식은 이미지 검색에는 꽤 유용하다.
하지만 문서 검색에서는 문제가 생긴다.
2. 문서 한 페이지에는 너무 많은 정보가 있다
다음과 같은 보고서 한 페이지를 생각해보자.
┌─────────────────────────────────────┐
│ 2026년 사업 실적 보고서 │
│ │
│ 제품별 매출 │
│ │
│ 2025 2026 │
│ Product A 100 180 │
│ Product B 200 150 │
│ Product C 80 160 │
│ │
│ ┌───────────────────┐ │
│ │ 매출 그래프 │ │
│ │ ▂ ▅ █ ▆ │ │
│ └───────────────────┘ │
│ │
│ ※ 해외 매출 제외 │
└─────────────────────────────────────┘
이 페이지에는 동시에 여러 정보가 존재한다.
- 제목
- 제품명
- 숫자
- 표의 행/열 관계
- 그래프
- 범례
- 각주
- 페이지 레이아웃
이 모든 정보를 하나의 Vector로 압축하면 정보 손실이 발생할 수 있다.
그래서 ColPali 계열의 Visual Document Retrieval은 조금 다른 접근을 사용한다.
3. 페이지 하나에서 Vector 하나만 만들지 않는다
핵심 개념은 Multi-Vector Representation이다.
페이지 이미지를 여러 영역 또는 Visual Token으로 표현한다.
개념적으로 보면 다음과 같다.
Page Image
┌─────────────────────────────────────┐
│ [A] Title │
│ │
│ [B] Table │
│ │
│ [C] Chart │
│ │
│ [D] Footnote │
└─────────────────────────────────────┘
│
▼
Visual Encoder
│
▼
┌─────────┼─────────┐
▼ ▼ ▼
Vector Vector Vector ...
#1 #2 #3
실제로는 단순히 제목·표·그래프처럼 사람이 영역을 직접 나누는 것이 아니다.
모델 내부의 Visual Token들이 페이지의 여러 특징을 표현한다.
즉,
Page
→ Vector 1
→ Vector 2
→ Vector 3
→ ...
→ Vector N
이 된다.
이것이 일반적인 Single-Vector Image Retrieval과 중요한 차이다.
4. 그런데 질문도 하나의 Vector가 아니다
사용자가 다음과 같이 질문했다고 하자.
2026년에 매출이 가장 많이 증가한 제품은?
질문도 Token 단위로 표현할 수 있다.
2026년
매출
가장 많이
증가한
제품
개념적으로 각각에 대응하는 Vector가 만들어진다.
Query
├── q1
├── q2
├── q3
├── q4
└── q5
문서 페이지에도 여러 Vector가 있다.
Document Page
├── d1
├── d2
├── d3
├── ...
└── dn
그렇다면 질문과 문서의 유사도를 어떻게 계산해야 할까?
여기서 Late Interaction이 등장한다.
5. Late Interaction이란?
이 개념은 처음 보면 어려워 보이지만 아이디어는 꽤 직관적이다.
Query의 각 Vector에 대해 Document의 Vector 중 가장 비슷한 것을 찾는다.
Query Vector q1
│
├── d1
├── d2
├── d3 ← 가장 유사
└── d4
Query Vector q2
│
├── d1 ← 가장 유사
├── d2
├── d3
└── d4
각 Query Token마다 최고의 Document Token을 찾는다.
수식으로 단순화하면 다음과 같은 개념이다.
Score(Q, D)
= Σ max Similarity(qᵢ, dⱼ)
즉,
질문의 각각의 의미 요소가 페이지 어디와 가장 잘 대응하는가?
를 계산한다.
이 방식의 장점은 페이지 전체를 하나의 Vector로 압축하지 않아도 된다는 것이다.
6. 왜 이름이 Late Interaction일까?
일반적인 Embedding 검색에서는 문서 전체를 먼저 하나의 Vector로 만든다.
Document
│
▼
Embedding
│
▼
Single Vector
검색 시점에는 이 Vector끼리 비교한다.
하지만 Late Interaction 방식에서는 Query와 Document가 각각 여러 Vector를 유지한다.
Query Tokens
│
│
│ 검색 시점
│ ↓
├────────────── Document Tokens
│
▼
Interaction
즉 Query와 Document의 세부 표현을 검색 단계까지 유지한 후 나중에 상호작용시킨다.
그래서 Late Interaction이라고 부른다.
7. ColPali는 무엇인가?
ColPali는 이런 방식의 Visual Document Retrieval을 가능하게 만든 대표적인 모델 계열이다.
핵심 아이디어를 단순화하면 다음과 같다.
Document Page Image
│
▼
Vision-Language Model
│
▼
Visual Token Representation
│
▼
Projection
│
▼
Multi-Vector Embedding
질문 역시 같은 검색 공간에서 표현된다.
Question
│
▼
Language Representation
│
▼
Multi-Vector Embedding
그리고 Late Interaction을 사용해 관련 페이지를 찾는다.
중요한 것은 PDF를 먼저 OCR해서 완벽한 텍스트로 복원할 필요가 없다는 것이다.
페이지 자체를 검색 대상으로 사용할 수 있다.
8. ColQwen 계열은?
ColQwen 계열 역시 같은 Visual Document Retrieval 방향에 있다.
기본 철학은 비슷하다.
Page Image
│
▼
Vision-Language Model
│
▼
Visual Tokens
│
▼
Multi-Vector
차이는 기반 Vision-Language Model과 모델 규모, 지원 기능, 성능 등에 있다.
Pixel RAG 시스템을 설계할 때 중요한 것은 특정 모델 이름 자체보다는 다음 구조를 이해하는 것이다.
페이지 → Visual Multi-Vector → Late Interaction → Page Retrieval
모델은 앞으로 계속 바뀔 수 있다.
하지만 이 구조를 이해하면 새로운 모델이 나와도 시스템 전체를 이해하기 쉽다.
9. 여기서 굉장히 중요한 사실
ColPali가 답변까지 만들어주는 것은 아니다.
역할을 분리해서 생각해야 한다.
Retriever
ColPali / ColQwen
"어떤 페이지가 질문과 관련 있는가?"
Generator
GPT 계열 Vision Model
Gemini
Claude
Qwen-VL
기타 VLM
"이 페이지를 보고 질문에 답해라."
전체 구조는 다음과 같다.
User Question
│
▼
Visual Retriever
│
▼
Relevant Pages
│
▼
Vision-Language Model
│
▼
Answer
이 역할 분리는 상당히 중요하다.
Retriever에게 답변까지 맡기는 것이 아니다.
10. Qdrant에는 어떻게 저장할까?
여기서 기존 Text RAG와 차이가 커진다.
일반 Text Embedding은 보통 다음처럼 저장한다.
Chunk
vector:
[0.13, 0.72, -0.11, ...]
즉 Point 하나에 하나의 Dense Vector를 저장한다.
하지만 Visual Document Retriever에서는 페이지 하나가 여러 Vector를 가진다.
개념적으로는 다음과 같다.
Page 27
[
[0.13, 0.72, ...],
[0.42, 0.11, ...],
[0.83, 0.22, ...],
...
]
즉,
Page
↓
Multi-Vector
구조가 필요하다.
따라서 Vector DB를 선택할 때도 단순한 Dense Vector 검색만 생각하면 안 된다.
Multi-Vector와 Late Interaction 검색을 어떻게 지원할 것인가가 중요하다.
11. Metadata는 반드시 함께 저장해야 한다
Vector만 저장하면 실제 서비스에서는 거의 쓸 수 없다.
최소한 다음 정보가 필요하다.
{
"tenant_id": 100,
"cabinet_id": 25,
"document_id": 918,
"page_no": 27,
"file_name": "2026_sales_report.pdf",
"image_path": "documents/918/pages/027.webp"
}
특히 멀티테넌트 SaaS라면
tenant_id
cabinet_id
document_id
같은 접근 범위가 반드시 검색 Filter에 포함되어야 한다.
예를 들어 사용자가 A 문서함에서 질문했다면 Visual Search 역시 A 문서함에 속한 Page만 검색해야 한다.
12. 페이지 이미지는 Vector DB에 넣지 않는다
페이지 이미지 자체까지 Qdrant에 저장할 필요는 없다.
보통 다음처럼 역할을 나누는 것이 좋다.
Object Storage
────────────────────────
original.pdf
pages/
├── 001.webp
├── 002.webp
├── 003.webp
└── ...
Vector DB
────────────────────────
page_id
document_id
page_no
visual_vectors
image_path
metadata
실제 이미지는
S3
MinIO
Object Storage
등에 저장한다.
Vector DB에는 이미지 위치만 저장한다.
13. 실제 적재 파이프라인
이제 전체 Ingestion Pipeline을 만들어보자.
PDF Upload
│
▼
Original Storage
│
▼
PDF Renderer
│
┌───────────┴───────────┐
│ │
▼ ▼
Text Parser Page Images
│ │
▼ ▼
Chunking Visual Retriever
│ │
▼ ▼
Text Embedding Multi-Vectors
│ │
▼ ▼
Text Vector DB Visual Vector DB
결국 문서 하나에서 두 종류의 Representation이 생성된다.
Document
├── Semantic Text Representation
│
└── Visual Document Representation
이것이 Hybrid Pixel RAG의 기본 구조다.
14. Python 코드 구조는 어떻게 될까?
실제 라이브러리와 모델별 API는 달라질 수 있지만 전체 코드는 대략 이런 형태가 된다.
from pathlib import Path
def ingest_document(pdf_path):
document = create_document(pdf_path)
pages = render_pdf(pdf_path)
for page_no, image in enumerate(pages, start=1):
image_path = save_page_image(
document.id,
page_no,
image
)
visual_vectors = visual_encoder.encode(image)
save_visual_vectors(
document_id=document.id,
page_no=page_no,
image_path=image_path,
vectors=visual_vectors
)
기존 Text Pipeline도 그대로 유지한다.
text = parse_document(pdf_path)
chunks = chunk_document(text)
for chunk in chunks:
vector = text_embedding_model.embed(chunk.text)
save_text_vector(
document_id=document.id,
page_no=chunk.page_no,
chunk=chunk,
vector=vector
)
결국 하나의 문서 적재 작업에서 두 Pipeline이 실행된다.
15. 검색 코드는 어떻게 될까?
사용자 질문이 들어온다.
query = "2026년에 매출 증가율이 가장 높은 제품은?"
먼저 Text Search를 한다.
text_results = text_search(
query=query,
top_k=10
)
Visual Search도 수행한다.
visual_results = visual_search(
query=query,
top_k=10
)
결과는 대략 다음과 같다.
TEXT
page 27 score ...
page 31 score ...
page 12 score ...
VISUAL
page 27 score ...
page 18 score ...
page 31 score ...
이 둘을 합친다.
candidates = fuse_results(
text_results,
visual_results
)
필요하면 Reranker를 적용한다.
results = rerank(
query,
candidates
)
그리고 최종 Page를 가져온다.
pages = load_page_images(
results[:3]
)
마지막으로 VLM에게 전달한다.
answer = vlm.generate(
question=query,
images=pages
)
전체 흐름은 이것이 전부다.
16. 그런데 모든 질문에 VLM을 호출하면 비싸지 않을까?
맞다.
Pixel RAG에서 실제 서비스 운영 시 가장 먼저 부딪히는 문제가 비용이다.
다음 질문을 생각해보자.
회사의 휴가 규정은?
관련 Chunk가 정확하게 검색됐다면 굳이 페이지 이미지까지 Vision Model에게 보낼 이유가 없다.
반면
다음 그래프에서 2025년 대비 가장 크게 성장한 사업부는?
이미지를 봐야 한다.
그래서 Query Router를 둘 수 있다.
Question
│
▼
Query Router
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
TEXT VISUAL HYBRID
│ │ │
▼ ▼ ▼
Text RAG Pixel RAG Both RAG
Router의 출력은 간단하게 만들 수 있다.
{
"mode": "HYBRID",
"reason": "question requires interpretation of table data"
}
17. 하지만 처음부터 Router를 만들 필요는 없다
개발 초기에는 시스템을 너무 복잡하게 만들 필요가 없다.
오히려 다음과 같은 순서가 현실적이다.
Version 1
Text Retrieval
↓
Relevant Page
↓
VLM
Version 2
Text Retrieval
+
Visual Retrieval
↓
Fusion
↓
VLM
Version 3
Query Router
↓
TEXT / VISUAL / HYBRID
Version 4
Adaptive Retrieval
실제 사용자 질문 데이터가 쌓인 뒤 Router를 만드는 편이 훨씬 낫다.
