멀티모달 RAG - 이미지와 표가 섞인 문서 검색하기
지난 글에서 GraphRAG로 개체 간 관계를 다루는 방법을 살펴봤습니다. 그런데 지금까지 다룬 모든 예시는 순수 텍스트 문서를 전제로 했습니다. 실제 사내 문서는 표, 다이어그램, 스크린샷이 섞여 있는 경우가 훨씬 많습니다. 이번 글에서는 텍스트뿐 아니라 이미지까지 함께 검색 대상으로 삼는 멀티모달 RAG를 다룹니다.
1. 왜 텍스트만으로는 부족한가
PDF나 발표 자료에는 표나 다이어그램에만 담긴 정보가 많습니다. 이 정보를 텍스트로만 추출하면 의미가 손실되거나 아예 누락됩니다.
[원본 문서 - 표]
| 직급 | 연차 |
|------|------|
| 사원 | 15일 |
| 대리 | 16일 |
| 과장 | 18일 |
[텍스트만 추출한 경우]
"직급 연차 사원 15일 대리 16일 과장 18일"
→ 어떤 직급이 몇 일인지 구조가 깨져서 검색·답변에 활용하기 어려움
표뿐 아니라 조직도, 아키텍처 다이어그램, 스크린샷에 포함된 UI 설명 같은 정보도 텍스트 추출만으로는 놓치기 쉽습니다.
2. 멀티모달 RAG의 두 가지 접근 방식
이미지가 포함된 문서를 다루는 방식은 크게 두 갈래로 나뉩니다.
| 방식 | 설명 |
|---|---|
| 이미지 → 텍스트 변환 후 기존 파이프라인 재사용 | 이미지를 설명 텍스트로 바꿔서 지금까지의 임베딩·검색 구조를 그대로 사용 |
| 멀티모달 임베딩 직접 사용 | 이미지 자체를 벡터로 변환해서 텍스트 벡터와 같은 공간에서 검색 |
실무에서는 구현 난이도와 기존 파이프라인 재사용성 때문에 첫 번째 방식을 훨씬 많이 사용합니다. 이번 글도 이 방식을 중심으로 다룹니다.
3. 이미지·표를 텍스트로 변환하기 - Vision LLM 활용
이미지가 포함된 페이지를 Vision을 지원하는 LLM에 넘겨서, 이미지 내용을 설명하는 텍스트로 미리 변환해둡니다.
from openai import OpenAI
import base64
client = OpenAI()
def describe_image(image_path):
with open(image_path, "rb") as f:
image_base64 = base64.b64encode(f.read()).decode("utf-8")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "이 이미지에 포함된 표나 다이어그램의 내용을 "
"검색 가능한 텍스트로 상세하게 서술해줘. "
"표라면 행과 열 관계가 드러나도록 문장으로 풀어써줘."},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}
]
}]
)
return response.choices[0].message.content
[Vision LLM 변환 결과 예시]
"이 표는 직급별 연차 일수를 보여준다. 사원은 15일, 대리는 16일, 과장은 18일의
연차가 부여된다. 직급이 높아질수록 연차 일수도 함께 증가하는 구조다."
이렇게 변환된 텍스트는 지난 글들에서 다룬 청킹·임베딩·벡터 저장 파이프라인에 그대로 태울 수 있습니다.
4. PDF에서 이미지·표 영역 추출하기
Vision LLM에 넘기기 전에, 먼저 PDF에서 이미지와 표가 있는 영역을 분리해야 합니다.
import fitz # PyMuPDF
def extract_images_from_pdf(pdf_path):
doc = fitz.open(pdf_path)
extracted = []
for page_num, page in enumerate(doc):
image_list = page.get_images(full=True)
for img_index, img in enumerate(image_list):
xref = img[0]
base_image = doc.extract_image(xref)
image_path = f"page{page_num}_img{img_index}.png"
with open(image_path, "wb") as f:
f.write(base_image["image"])
extracted.append({"page": page_num, "path": image_path})
return extracted
표는 이미지가 아니라 텍스트 레이어로 존재하는 PDF도 많은데, 이 경우 레이아웃이 깨지지 않도록 표 인식에 특화된 라이브러리(camelot, pdfplumber 등)를 함께 사용하는 것이 정확도가 더 높습니다.
5. 텍스트 청크와 이미지 설명 청크 함께 저장하기
원본 문서의 순수 텍스트와, 이미지에서 변환된 설명 텍스트를 같은 벡터 DB에 함께 저장하되, 메타데이터로 출처 유형을 구분해둡니다.
@Service
@RequiredArgsConstructor
public class MultimodalIngestService {
private final VectorStore vectorStore;
public void ingestTextChunk(String content, String source, int pageNum) {
Document doc = new Document(content, Map.of(
"source", source, "page", pageNum, "content_type", "text"
));
vectorStore.add(List.of(doc));
}
public void ingestImageDescription(String description, String source, int pageNum, String imagePath) {
Document doc = new Document(description, Map.of(
"source", source, "page", pageNum,
"content_type", "image_description", "image_path", imagePath
));
vectorStore.add(List.of(doc));
}
}
content_type 메타데이터를 넣어두면, 검색 결과가 원문 텍스트에서 온 것인지 이미지 설명에서 온 것인지 구분할 수 있고, 답변에 원본 이미지를 함께 보여주고 싶을 때 image_path로 바로 참조할 수 있습니다.
6. 검색된 결과에 원본 이미지 함께 반환하기
사용자 질문이 표 내용과 관련 있다면, 검색된 청크가 이미지 설명임을 확인하고 원본 이미지 경로도 함께 응답에 포함시킵니다.
public ChatResponseWithImages answer(String question) {
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5));
List<String> imagePaths = results.stream()
.filter(doc -> "image_description".equals(doc.getMetadata().get("content_type")))
.map(doc -> (String) doc.getMetadata().get("image_path"))
.distinct()
.toList();
String context = results.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
String answer = generateAnswer(context, question);
return new ChatResponseWithImages(answer, imagePaths);
}
질문: "과장 연차는 며칠이야?"
답변: "과장은 18일의 연차가 부여됩니다."
첨부 이미지: ["page3_img1.png"] ← 원본 표 이미지도 함께 반환
사용자는 텍스트 답변뿐 아니라 근거가 된 원본 표 이미지를 직접 눈으로 확인할 수 있어, 지난 글에서 다룬 "답변에 출처를 표기하는" 원칙이 이미지 영역까지 자연스럽게 확장됩니다.
7. 비용과 처리 시간 고려하기
Vision LLM 호출은 일반 텍스트 임베딩보다 비용과 처리 시간이 더 큽니다. 문서 적재 시점에 모든 이미지를 무조건 처리하기보다, 다음 기준으로 선별하는 것이 현실적입니다.
- 장식용 로고나 배경 이미지는 제외하고, 표·다이어그램·차트처럼 정보 밀도가 높은 이미지만 처리
- 지난 글에서 다룬 비동기 처리(
@Async)로 이미지 설명 변환 작업을 백그라운드로 분리 - 이미지 크기가 작은 아이콘류는 사전에 필터링해서 불필요한 API 호출 차단
8. 정리
- 표나 다이어그램에 담긴 정보는 단순 텍스트 추출만으로는 손실되기 쉬워, 별도의 처리가 필요합니다.
- Vision LLM으로 이미지를 검색 가능한 설명 텍스트로 변환하면, 기존 텍스트 기반 RAG 파이프라인을 그대로 재사용할 수 있습니다.
- 메타데이터로 콘텐츠 유형을 구분해두면, 답변과 함께 원본 이미지를 근거로 제시하는 것까지 자연스럽게 확장할 수 있습니다.
다음 글에서는 모든 질문에 같은 검색 파이프라인을 쓰는 대신, 질문 유형에 따라 벡터 검색·그래프 검색·SQL 조회 중 적절한 경로로 자동 분기하는 Query Routing을 다뤄보겠습니다.
'RAG를 활용한 LLM 서비스 구축' 카테고리의 다른 글
| STEP 23 - 구조화된 데이터(엑셀·테이블) RAG에 통합하기 (0) | 2026.07.21 |
|---|---|
| STEP 22 - Query Routing (0) | 2026.07.20 |
| STEP 20 - GraphRAG (0) | 2026.07.15 |
| STEP 19 - RAG 시스템 부하 테스트와 모니터링 (0) | 2026.07.15 |
| STEP 18 - 권한 기반 RAG 접근 제어 (0) | 2026.07.14 |