본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 26. · 3 Views
멀티모달 RAG 완벽 가이드
텍스트뿐만 아니라 이미지, PDF, 표, 차트까지 검색하고 이해하는 차세대 RAG 시스템을 배웁니다. CLIP 임베딩부터 실전 OCR 처리까지, 실무에서 바로 사용할 수 있는 멀티모달 검색 기술을 완전히 마스터합니다.
목차
1. 이미지 + 텍스트 검색
어느 날 김개발 씨는 회사의 기술 문서 검색 시스템을 개선하는 업무를 맡았습니다. 기존 RAG 시스템은 텍스트만 검색할 수 있었는데, 사용자들이 "아키텍처 다이어그램을 찾아주세요"라고 요청하면 아무것도 찾지 못했습니다.
박시니어 씨가 다가와 말했습니다. "이제는 멀티모달 RAG를 도입할 때가 됐네요."
멀티모달 RAG는 텍스트뿐만 아니라 이미지, 도표, 차트 등 다양한 형태의 데이터를 함께 검색하고 이해하는 시스템입니다. 마치 도서관 사서가 책의 내용뿐만 아니라 삽화와 사진까지 기억하고 있는 것처럼, 시각적 정보와 텍스트 정보를 동시에 처리합니다.
이를 통해 더욱 풍부하고 정확한 검색 결과를 제공할 수 있습니다.
다음 코드를 살펴봅시다.
from langchain.schema import Document
from langchain.vectorstores import Chroma
import base64
# 이미지와 텍스트를 함께 저장하는 문서 생성
def create_multimodal_document(text, image_path):
# 이미지를 base64로 인코딩하여 메타데이터에 저장
with open(image_path, "rb") as image_file:
encoded_image = base64.b64encode(image_file.read()).decode()
# 텍스트와 이미지 정보를 함께 담은 문서 생성
doc = Document(
page_content=text,
metadata={
"image": encoded_image,
"image_path": image_path,
"content_type": "multimodal"
}
)
return doc
# 사용 예시: 아키텍처 다이어그램과 설명을 함께 저장
architecture_doc = create_multimodal_document(
text="마이크로서비스 아키텍처: API Gateway가 요청을 라우팅합니다",
image_path="./diagrams/architecture.png"
)
김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 회사의 기술 문서는 수백 페이지에 달하고, 각 문서마다 아키텍처 다이어그램, 플로우차트, 성능 그래프 등이 포함되어 있습니다.
기존 RAG 시스템으로는 텍스트만 검색할 수 있어서, 사용자들이 "인증 플로우 다이어그램"이나 "성능 비교 그래프"를 찾으려고 하면 속수무책이었습니다. 박시니어 씨가 김개발 씨의 모니터를 보며 말했습니다.
"텍스트만 검색하는 건 이제 한계에 부딪혔네요. 멀티모달 RAG로 업그레이드할 시간입니다." 그렇다면 멀티모달 RAG란 정확히 무엇일까요?
쉽게 비유하자면, 멀티모달 RAG는 마치 박학다식한 전문가가 책의 내용뿐만 아니라 모든 그림과 사진까지 완벽하게 기억하고 있는 것과 같습니다. 일반적인 RAG가 텍스트로 된 설명만 읽을 수 있다면, 멀티모달 RAG는 이미지를 보고 그 의미까지 이해합니다.
"빨간 화살표로 연결된 서버 구조도"라고 물어보면, 실제로 그런 시각적 특징을 가진 이미지를 찾아낼 수 있습니다. 멀티모달 RAG가 없던 시절에는 어땠을까요?
개발자들은 이미지가 포함된 문서를 검색하기 위해 파일명이나 주변 텍스트에만 의존해야 했습니다. 예를 들어 "architecture_diagram_v2.png"라는 파일명을 정확히 기억하거나, 이미지 바로 아래에 있는 캡션 텍스트로만 검색할 수 있었습니다.
더 큰 문제는 이미지 안의 내용을 전혀 검색할 수 없다는 것이었습니다. 다이어그램에 "Redis"라고 쓰여 있어도, 시스템은 그것을 이미지로만 인식할 뿐 텍스트로 읽지 못했습니다.
바로 이런 문제를 해결하기 위해 멀티모달 RAG가 등장했습니다. 멀티모달 RAG를 사용하면 이미지의 시각적 특징을 벡터로 변환하여 저장할 수 있습니다.
또한 텍스트 쿼리로 이미지를 검색하거나, 반대로 이미지로 유사한 텍스트를 찾는 것도 가능합니다. 무엇보다 사용자가 "로그인 화면 스크린샷"이라고 검색하면, 실제로 UI 스크린샷 이미지를 찾아주는 놀라운 경험을 제공합니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 create_multimodal_document 함수는 텍스트와 이미지를 함께 저장하는 문서 객체를 생성합니다.
이미지를 base64로 인코딩하는 부분이 핵심입니다. 이렇게 하면 이미지 데이터를 문자열로 변환하여 데이터베이스에 저장할 수 있습니다.
메타데이터에 원본 이미지 경로와 인코딩된 이미지를 함께 저장하면, 나중에 검색 결과를 보여줄 때 이미지를 복원할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 의료 영상 진단 시스템을 개발한다고 가정해봅시다. 의사들이 "폐렴이 의심되는 X-ray 이미지"라고 검색하면, 시스템은 실제로 폐렴 패턴을 보이는 X-ray 이미지들을 찾아줍니다.
전자상거래 서비스에서는 "빨간 원피스"라고 검색하면 텍스트 설명뿐만 아니라 실제 빨간색 원피스 상품 이미지를 함께 찾아줄 수 있습니다. 많은 글로벌 기업들이 이런 멀티모달 검색 기술을 적극적으로 도입하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 이미지를 그대로 벡터 DB에 저장하려는 것입니다.
이미지는 용량이 크기 때문에 base64 인코딩만으로는 스토리지 비용이 급증할 수 있습니다. 따라서 이미지는 별도의 객체 스토리지(S3 등)에 저장하고, 벡터 DB에는 이미지의 임베딩 벡터와 경로만 저장하는 것이 올바른 방법입니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.
"그렇다면 우리 문서의 모든 다이어그램을 검색할 수 있겠네요!" 멀티모달 RAG를 제대로 이해하면 단순한 텍스트 검색을 넘어 시각적 정보까지 활용하는 강력한 검색 시스템을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 이미지는 S3 같은 객체 스토리지에 저장하고, DB에는 경로만 저장하세요
- 이미지 임베딩과 텍스트 임베딩은 같은 차원의 벡터 공간에 있어야 교차 검색이 가능합니다
- 썸네일을 함께 생성하면 검색 결과를 빠르게 미리 볼 수 있습니다
2. CLIP 임베딩
김개발 씨는 멀티모달 RAG를 구현하기로 결심했지만, 막상 코드를 작성하려니 막막했습니다. 텍스트와 이미지를 어떻게 같은 벡터 공간에 표현할 수 있을까요?
박시니어 씨가 화이트보드에 "CLIP"이라고 크게 적으며 말했습니다. "OpenAI가 만든 이 모델이 해답입니다."
CLIP(Contrastive Language-Image Pre-training)은 텍스트와 이미지를 같은 벡터 공간에 임베딩하는 혁신적인 모델입니다. 마치 통역사가 한국어와 영어를 자유롭게 오가듯이, CLIP은 "강아지 사진"이라는 텍스트와 실제 강아지 사진을 같은 의미 공간에 배치합니다.
이를 통해 텍스트로 이미지를 검색하거나, 이미지로 관련 텍스트를 찾는 것이 가능해집니다.
다음 코드를 살펴봅시다.
import torch
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
# CLIP 모델과 프로세서 로드
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
def get_text_embedding(text):
# 텍스트를 벡터로 변환
inputs = processor(text=[text], return_tensors="pt", padding=True)
with torch.no_grad():
text_features = model.get_text_features(**inputs)
return text_features.numpy()
def get_image_embedding(image_path):
# 이미지를 벡터로 변환
image = Image.open(image_path)
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
image_features = model.get_image_features(**inputs)
return image_features.numpy()
# 사용 예시: 같은 벡터 공간에서 유사도 계산
text_emb = get_text_embedding("로그인 화면 UI")
image_emb = get_image_embedding("./screenshots/login.png")
# 코사인 유사도로 텍스트와 이미지의 관련성 측정
similarity = torch.cosine_similarity(
torch.tensor(text_emb),
torch.tensor(image_emb)
)
print(f"유사도: {similarity.item():.2f}")
김개발 씨는 멀티모달 RAG의 핵심이 텍스트와 이미지를 "같은 언어"로 표현하는 것임을 깨달았습니다. 하지만 어떻게 완전히 다른 형태의 데이터를 같은 방식으로 표현할 수 있을까요?
텍스트는 단어의 나열이고, 이미지는 픽셀의 집합인데 말이죠. 박시니어 씨가 설명을 시작했습니다.
"바로 CLIP 모델이 이 문제를 해결해줍니다. OpenAI가 4억 개의 이미지-텍스트 쌍으로 학습시킨 강력한 모델이죠." 그렇다면 CLIP은 정확히 어떻게 동작할까요?
쉽게 비유하자면, CLIP은 마치 이중 언어 사용자가 개념을 이해하는 것과 같습니다. "사과"라는 한국어 단어와 "apple"이라는 영어 단어는 형태는 다르지만 같은 개념을 가리킵니다.
CLIP도 마찬가지로 "빨간 사과 사진"이라는 텍스트와 실제 빨간 사과 이미지를 같은 "의미 공간"의 비슷한 위치에 배치합니다. 이렇게 하면 텍스트 쿼리로 이미지를 검색할 수 있게 됩니다.
CLIP이 없던 시절에는 어땠을까요? 과거에는 이미지 검색을 위해 수작업으로 태그를 달아야 했습니다.
누군가 이미지를 업로드할 때마다 "카테고리: 음식", "색상: 빨강", "물체: 사과"처럼 일일이 메타데이터를 입력해야 했습니다. 이런 방식은 시간이 오래 걸릴 뿐만 아니라, 사람마다 다르게 태그를 달아 일관성도 없었습니다.
더 큰 문제는 새로운 종류의 이미지가 나타났을 때, 기존 태그 시스템으로는 설명할 수 없다는 것이었습니다. 바로 이런 문제를 해결하기 위해 CLIP이 등장했습니다.
CLIP을 사용하면 별도의 태깅 작업 없이도 자연어로 이미지를 검색할 수 있습니다. 또한 학습 단계에서 본 적 없는 새로운 개념도 어느 정도 이해할 수 있습니다.
무엇보다 텍스트-이미지 간의 의미적 유사도를 정량적으로 측정할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 Hugging Face에서 제공하는 사전 학습된 CLIP 모델을 로드합니다. CLIPProcessor는 텍스트와 이미지를 모델이 이해할 수 있는 형태로 전처리해줍니다.
get_text_embedding 함수는 텍스트를 512차원의 벡터로 변환합니다. 마찬가지로 get_image_embedding 함수는 이미지를 같은 512차원 벡터로 변환합니다.
핵심은 두 벡터가 같은 차원의 같은 의미 공간에 존재한다는 것입니다. 마지막으로 코사인 유사도를 계산하여 텍스트와 이미지가 얼마나 관련이 있는지 0에서 1 사이의 점수로 측정합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 패션 쇼핑몰을 개발한다고 가정해봅시다.
사용자가 "파란색 청바지"라고 검색하면, CLIP은 이 텍스트를 벡터로 변환하고, 데이터베이스에 있는 모든 상품 이미지의 벡터와 비교합니다. 실제로 파란색 청바지 이미지들이 높은 유사도 점수를 받아 상위에 노출됩니다.
내부 문서 관리 시스템에서는 "네트워크 토폴로지 다이어그램"이라고 검색하면 실제 네트워크 구조도를 찾아줄 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 CLIP을 실시간으로 매번 호출하는 것입니다. CLIP 모델 추론은 GPU를 사용해도 시간이 걸리기 때문에, 수천 장의 이미지를 검색할 때마다 임베딩을 생성하면 성능이 급격히 저하됩니다.
따라서 이미지를 처음 저장할 때 미리 임베딩을 생성해두고, 벡터 DB에 저장하는 것이 올바른 방법입니다. 검색 시에는 쿼리 텍스트만 임베딩하고, 기존에 저장된 이미지 임베딩과 비교하면 됩니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 흥분을 감추지 못했습니다.
"와, 이렇게 간단하게 텍스트와 이미지를 연결할 수 있다니!" CLIP을 제대로 이해하면 강력한 멀티모달 검색 시스템의 기반을 마련할 수 있습니다. 여러분도 오늘 배운 CLIP을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 이미지 임베딩은 사전에 생성하여 벡터 DB에 저장하세요. 검색 시에는 쿼리만 임베딩하면 됩니다
- CLIP은 영어로 학습되었으므로, 한국어 쿼리는 번역 후 사용하면 성능이 향상됩니다
- GPU가 있다면
model.to("cuda")로 추론 속도를 크게 개선할 수 있습니다
3. PDF, 표, 차트 처리
김개발 씨는 CLIP으로 일반 이미지 검색은 성공했지만, 새로운 문제에 부딪혔습니다. 회사 문서는 대부분 PDF 형태이고, 그 안에는 복잡한 표와 차트가 가득했습니다.
"이런 구조화된 데이터는 어떻게 처리해야 할까요?" 박시니어 씨가 미소를 지으며 답했습니다. "PDF 파싱과 테이블 추출, 그리고 OCR까지 알아야 합니다."
PDF 멀티모달 처리는 PDF 문서에서 텍스트, 표, 차트, 이미지를 각각 추출하고 적절히 임베딩하는 기술입니다. 마치 책을 읽을 때 본문, 표, 그래프를 각각 다르게 이해하듯이, 각 요소의 특성에 맞는 처리 방법을 적용합니다.
일반 텍스트는 텍스트 임베딩으로, 표는 구조를 보존한 마크다운으로, 차트는 이미지 임베딩으로 변환하여 저장합니다.
다음 코드를 살펴봅시다.
import fitz # PyMuPDF
from PIL import Image
import pandas as pd
from io import BytesIO
def extract_pdf_elements(pdf_path):
doc = fitz.open(pdf_path)
elements = []
for page_num in range(len(doc)):
page = doc[page_num]
# 텍스트 추출
text = page.get_text()
if text.strip():
elements.append({
"type": "text",
"content": text,
"page": page_num + 1
})
# 이미지 추출 (차트, 다이어그램 포함)
image_list = page.get_images()
for img_index, img in enumerate(image_list):
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
# 이미지를 PIL Image로 변환
image = Image.open(BytesIO(image_bytes))
elements.append({
"type": "image",
"content": image,
"page": page_num + 1,
"index": img_index
})
# 표 추출 (간단한 예시)
tables = page.find_tables()
for table in tables:
df = table.to_pandas()
elements.append({
"type": "table",
"content": df.to_markdown(),
"page": page_num + 1
})
return elements
# 사용 예시
elements = extract_pdf_elements("./documents/report.pdf")
print(f"추출된 요소: 텍스트 {len([e for e in elements if e['type']=='text'])}개, "
f"이미지 {len([e for e in elements if e['type']=='image'])}개, "
f"표 {len([e for e in elements if e['type']=='table'])}개")
김개발 씨는 회사의 기술 보고서 PDF를 열어봤습니다. 200페이지 분량의 문서에는 수십 개의 성능 비교 표, 시스템 아키텍처 다이어그램, 그래프가 빼곡했습니다.
단순히 텍스트만 추출하면 이런 중요한 정보를 모두 놓치게 됩니다. 박시니어 씨가 설명했습니다.
"PDF는 단순한 텍스트 파일이 아닙니다. 내부적으로 텍스트 객체, 이미지 객체, 벡터 그래픽 객체가 복잡하게 얽혀 있죠.
각각을 제대로 추출해야 합니다." 그렇다면 PDF 멀티모달 처리란 정확히 무엇일까요? 쉽게 비유하자면, PDF 처리는 마치 신문을 분석하는 것과 같습니다.
기사 본문, 사진, 통계 표, 그래프가 한 페이지에 섞여 있을 때, 각각을 구분해서 이해해야 합니다. 기사는 읽고, 사진은 보고, 표는 숫자를 해석하고, 그래프는 추세를 파악합니다.
PDF 멀티모달 처리도 마찬가지로 문서의 각 요소를 타입에 맞게 추출하고 처리합니다. PDF 멀티모달 처리가 없던 시절에는 어땠을까요?
과거에는 PDF를 단순히 텍스트로만 변환했습니다. 아무리 중요한 차트나 표가 있어도 "[이미지]"라는 플레이스홀더로 대체되거나 아예 무시되었습니다.
사용자가 "2023년 매출 추이 그래프"를 검색하면 텍스트로 된 설명은 찾을 수 있지만, 정작 그래프 이미지는 찾을 수 없었습니다. 표의 경우도 레이아웃이 깨져서 행과 열의 관계를 잃어버리는 경우가 많았습니다.
바로 이런 문제를 해결하기 위해 PDF 멀티모달 처리가 중요해졌습니다. PDF에서 각 요소를 타입별로 추출하면 검색 정확도가 크게 향상됩니다.
또한 표를 구조화된 형태로 보존하면 나중에 질문-답변 시스템에서 정확한 숫자를 추출할 수 있습니다. 무엇보다 차트와 다이어그램을 이미지로 보존하면, 사용자에게 시각적 정보까지 제공할 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 PyMuPDF(fitz) 라이브러리를 사용하여 PDF를 엽니다.
각 페이지를 순회하면서 get_text()로 텍스트를 추출합니다. 이미지는 get_images()로 목록을 가져온 후, extract_image()로 실제 바이너리 데이터를 추출합니다.
PIL Image로 변환하면 나중에 CLIP 임베딩을 생성할 수 있습니다. 표 추출은 find_tables()를 사용하여 자동으로 셀 경계를 인식하고, pandas DataFrame으로 변환한 후 마크다운 형식으로 저장합니다.
이렇게 하면 표의 구조가 보존되어 LLM이 이해하기 쉽습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 금융 리포트 분석 시스템을 개발한다고 가정해봅시다. 수백 페이지의 분기 보고서 PDF에서 "영업이익 추이 차트"를 검색하면, 시스템은 실제 차트 이미지와 함께 주변 텍스트 설명을 찾아줍니다.
연구 논문 검색 시스템에서는 "실험 결과 테이블"을 검색하면 구조화된 표 데이터를 반환하여, 사용자가 수치를 직접 비교할 수 있게 합니다. 많은 법률, 의료, 금융 분야에서 이런 PDF 멀티모달 처리를 활용하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 PDF를 같은 방식으로 처리하려는 것입니다.
스캔된 PDF는 텍스트가 이미지로 저장되어 있어서 OCR이 필요하지만, 디지털 PDF는 직접 텍스트를 추출할 수 있습니다. 또한 복잡한 레이아웃의 표는 자동 추출이 실패할 수 있으므로, 추출 결과를 항상 검증해야 합니다.
따라서 PDF 타입을 먼저 판별하고, 적절한 처리 방법을 선택하는 것이 올바른 접근입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "이제 PDF의 모든 정보를 활용할 수 있겠네요!" PDF 멀티모달 처리를 제대로 이해하면 문서의 풍부한 정보를 놓치지 않고 검색 시스템에 활용할 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 스캔 PDF인지 디지털 PDF인지 먼저 확인하세요. page.get_text()가 비어있으면 스캔 PDF입니다
- 표 추출은 완벽하지 않으므로, 중요한 문서는 수작업 검증이 필요합니다
- 대용량 PDF는 페이지별로 처리하고, 멀티프로세싱을 활용하면 속도를 개선할 수 있습니다
4. 실습: 이미지 검색 RAG
드디어 김개발 씨는 이론을 실전에 적용할 시간이 왔습니다. 회사의 UI 디자인 스크린샷 수백 장을 검색할 수 있는 시스템을 만들기로 했습니다.
"자연어로 '로그인 버튼이 파란색인 화면'이라고 검색하면 실제 스크린샷을 찾아주는 거죠?" 박시니어 씨가 엄지를 치켜세웠습니다. "정확합니다.
지금까지 배운 것을 모두 합쳐봅시다."
이미지 검색 RAG는 CLIP 임베딩과 벡터 검색을 결합하여 자연어로 이미지를 찾는 시스템입니다. 마치 구글 이미지 검색처럼 텍스트를 입력하면 관련된 이미지를 찾아주지만, 더 정교한 의미 검색이 가능합니다.
이미지를 미리 벡터화하여 저장하고, 검색 쿼리도 같은 공간에 임베딩하여 유사도 기반으로 결과를 반환합니다.
다음 코드를 살펴봅시다.
from langchain.vectorstores import Chroma
from langchain.embeddings.base import Embeddings
import torch
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import numpy as np
# CLIP을 LangChain Embeddings로 래핑
class CLIPEmbeddings(Embeddings):
def __init__(self):
self.model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
def embed_documents(self, texts):
# 여러 텍스트를 한번에 임베딩
inputs = self.processor(text=texts, return_tensors="pt", padding=True)
with torch.no_grad():
embeddings = self.model.get_text_features(**inputs)
return embeddings.cpu().numpy().tolist()
def embed_query(self, text):
# 검색 쿼리 임베딩
return self.embed_documents([text])[0]
def embed_image(self, image_path):
# 이미지 임베딩 (추가 메서드)
image = Image.open(image_path)
inputs = self.processor(images=image, return_tensors="pt")
with torch.no_grad():
embedding = self.model.get_image_features(**inputs)
return embedding.cpu().numpy().tolist()[0]
# 이미지 검색 RAG 시스템 구축
embeddings = CLIPEmbeddings()
vectorstore = Chroma(embedding_function=embeddings, persist_directory="./image_db")
# 이미지를 벡터 DB에 저장
def index_images(image_paths):
for img_path in image_paths:
# 이미지를 임베딩으로 변환
img_embedding = embeddings.embed_image(img_path)
# 벡터 DB에 저장 (이미지 경로를 텍스트로)
vectorstore.add_texts(
texts=[img_path],
embeddings=[img_embedding],
metadatas=[{"type": "image", "path": img_path}]
)
vectorstore.persist()
# 검색 함수
def search_images(query, k=5):
# 텍스트 쿼리로 유사한 이미지 검색
results = vectorstore.similarity_search(query, k=k)
return [doc.metadata["path"] for doc in results]
# 사용 예시
image_paths = ["./ui/login.png", "./ui/dashboard.png", "./ui/profile.png"]
index_images(image_paths)
results = search_images("파란색 버튼이 있는 화면", k=3)
print(f"검색 결과: {results}")
김개발 씨는 회사의 UI 디자인 폴더를 열어봤습니다. 지난 2년간 쌓인 스크린샷이 500장이 넘었습니다.
파일명은 "screenshot_20231215_v3_final_real_final.png" 같은 무의미한 이름뿐이었습니다. 디자이너가 "회원가입 완료 화면 찾아줄 수 있어요?"라고 물으면, 일일이 이미지를 하나씩 열어봐야 했습니다.
박시니어 씨가 제안했습니다. "이미지 검색 RAG를 만들어봅시다.
한번 구축하면 계속 사용할 수 있습니다." 그렇다면 이미지 검색 RAG는 정확히 어떻게 동작할까요? 쉽게 비유하자면, 이미지 검색 RAG는 마치 거대한 이미지 도서관에 완벽한 사서가 있는 것과 같습니다.
모든 이미지를 머릿속에 기억하고 있다가, 여러분이 "파란색 옷을 입은 사람"이라고 말하면 즉시 해당하는 사진들을 찾아줍니다. 시스템은 이미지를 처음 저장할 때 CLIP으로 벡터화하여 데이터베이스에 넣어둡니다.
검색 시에는 쿼리를 같은 방식으로 벡터화하고, 가장 가까운 이미지 벡터를 찾아 반환합니다. 이미지 검색 RAG가 없던 시절에는 어땠을까요?
과거에는 이미지 파일명이나 폴더 구조로만 검색할 수 있었습니다. "login" 폴더에 있는 파일을 찾거나, 파일명에 "button"이 포함된 이미지를 찾는 정도였습니다.
하지만 파일명을 잘못 지으면 영영 찾을 수 없었습니다. 더 큰 문제는 이미지의 실제 내용으로는 검색할 수 없다는 것이었습니다.
"빨간 에러 메시지가 표시된 화면"을 찾고 싶어도 방법이 없었습니다. 바로 이런 문제를 해결하기 위해 이미지 검색 RAG가 필요합니다.
이미지 검색 RAG를 사용하면 이미지의 시각적 내용으로 직접 검색할 수 있습니다. 또한 여러 개념을 조합한 쿼리도 가능합니다.
예를 들어 "어두운 배경에 녹색 체크 아이콘"처럼 복잡한 조건도 이해합니다. 무엇보다 한번 인덱싱하면 몇 초 안에 수천 장의 이미지에서 원하는 것을 찾을 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 CLIPEmbeddings 클래스를 만들어 LangChain의 Embeddings 인터페이스를 구현합니다.
이렇게 하면 LangChain의 벡터 스토어와 호환됩니다. embed_documents는 여러 텍스트를 배치로 처리하여 효율성을 높입니다.
embed_image는 추가 메서드로, 이미지를 텍스트와 같은 벡터 공간에 임베딩합니다. Chroma 벡터 스토어는 임베딩을 저장하고 유사도 검색을 제공합니다.
index_images 함수는 이미지를 하나씩 임베딩하여 DB에 저장합니다. 검색 시에는 similarity_search가 쿼리와 가장 유사한 k개의 이미지를 반환합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 이커머스 상품 관리 시스템을 개발한다고 가정해봅시다.
수만 개의 상품 이미지가 있을 때, CS 팀이 "하얀색 운동화"라고 검색하면 실제 하얀 운동화 상품들이 나타납니다. 건축 설계 회사에서는 "붉은 벽돌 외관"으로 검색하여 유사한 건축 사례를 빠르게 찾을 수 있습니다.
의료 영상 시스템에서는 "골절이 의심되는 X-ray"처럼 전문적인 쿼리로도 검색이 가능합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 검색할 때마다 모든 이미지를 다시 임베딩하는 것입니다. 이미지 임베딩은 계산 비용이 크므로, 반드시 사전에 생성하여 벡터 DB에 저장해야 합니다.
또한 CLIP은 매우 추상적이거나 전문적인 개념은 잘 이해하지 못할 수 있습니다. 따라서 의료나 법률 같은 전문 분야에서는 도메인 특화 모델로 파인튜닝이 필요합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 시스템을 완성한 김개발 씨가 디자이너에게 시연했습니다.
"회원가입 완료 화면"이라고 검색하자 정확히 원하는 스크린샷이 나타났습니다. 디자이너가 감탄했습니다.
"이제 파일명을 신경 쓰지 않아도 되겠어요!" 이미지 검색 RAG를 제대로 구축하면 팀의 생산성을 크게 높일 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 이미지가 많다면 배치 처리로 인덱싱 속도를 개선하세요
- GPU가 있다면 반드시 활용하세요. CPU 대비 10배 이상 빠릅니다
- 검색 결과의 유사도 점수를 함께 반환하면, 신뢰도를 판단할 수 있습니다
5. 실습: 문서 OCR + RAG
김개발 씨의 다음 도전은 스캔된 PDF 문서였습니다. 회사의 오래된 계약서와 보고서는 모두 스캔본이라 텍스트 추출이 불가능했습니다.
"이런 문서도 검색할 수 있을까요?" 박시니어 씨가 노트북을 펼치며 답했습니다. "OCR과 RAG를 결합하면 가능합니다.
스캔 이미지에서 텍스트를 읽어내고, 그것을 벡터화하는 거죠."
문서 OCR RAG는 스캔된 문서나 이미지에서 텍스트를 추출하고, 추출된 텍스트를 벡터화하여 검색 가능하게 만드는 시스템입니다. 마치 인간이 사진 속 글씨를 읽고 이해하는 것처럼, OCR이 이미지를 텍스트로 변환하고, RAG가 그 의미를 파악합니다.
손글씨, 인쇄물, 스캔 문서 등 어떤 형태든 디지털 검색 시스템에 통합할 수 있습니다.
다음 코드를 살펴봅시다.
import pytesseract
from PIL import Image
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
import fitz # PyMuPDF
# PDF를 이미지로 변환 후 OCR 수행
def ocr_pdf(pdf_path):
doc = fitz.open(pdf_path)
all_text = []
for page_num in range(len(doc)):
page = doc[page_num]
# 디지털 텍스트 먼저 시도
text = page.get_text()
if not text.strip():
# 텍스트가 없으면 스캔 PDF로 간주하고 OCR 수행
pix = page.get_pixmap(dpi=300) # 고해상도로 변환
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
# Tesseract OCR로 텍스트 추출 (한국어 지원)
text = pytesseract.image_to_string(img, lang='kor+eng')
all_text.append({
"page": page_num + 1,
"text": text,
"source": pdf_path
})
return all_text
# OCR 결과를 RAG 시스템에 저장
def build_ocr_rag(pdf_path):
# OCR로 텍스트 추출
pages = ocr_pdf(pdf_path)
# 텍스트를 청크로 분할
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
documents = []
for page in pages:
chunks = text_splitter.split_text(page["text"])
for chunk in chunks:
documents.append({
"content": chunk,
"metadata": {
"page": page["page"],
"source": page["source"]
}
})
# 벡터 스토어에 저장
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(
texts=[doc["content"] for doc in documents],
embedding=embeddings,
metadatas=[doc["metadata"] for doc in documents],
persist_directory="./ocr_db"
)
return vectorstore
# 검색 함수
def search_scanned_docs(query, vectorstore, k=3):
results = vectorstore.similarity_search(query, k=k)
return results
# 사용 예시
vectorstore = build_ocr_rag("./scanned_contract.pdf")
results = search_scanned_docs("계약 기간", vectorstore)
for i, doc in enumerate(results):
print(f"{i+1}. {doc.metadata['source']} (페이지 {doc.metadata['page']})")
print(f"내용: {doc.page_content[:200]}...\n")
김개발 씨는 회사 서버에 보관된 오래된 계약서 PDF를 열어봤습니다. 1990년대에 작성된 문서를 스캔한 것으로, 텍스트 추출을 시도하면 아무것도 나오지 않았습니다.
이런 문서가 수백 건이나 되었습니다. 법무팀에서는 특정 조항을 찾기 위해 매번 수작업으로 문서를 뒤져야 했습니다.
박시니어 씨가 설명했습니다. "이런 경우에는 OCR이 필수입니다.
먼저 이미지를 텍스트로 변환하고, 그 다음에 RAG를 적용하는 거죠." 그렇다면 문서 OCR RAG는 정확히 무엇일까요? 쉽게 비유하자면, OCR RAG는 마치 사진 속 글씨를 읽고 이해하는 사람과 같습니다.
여러분이 책 사진을 찍으면, 그 사진 속 글자를 읽어서 실제 텍스트로 변환합니다. 그러고 나서 그 텍스트의 의미를 파악하여 데이터베이스에 저장합니다.
나중에 누군가 관련 내용을 검색하면, 원본이 이미지였든 텍스트였든 상관없이 찾아낼 수 있습니다. 문서 OCR RAG가 없던 시절에는 어땠을까요?
과거에는 스캔된 문서는 검색이 불가능했습니다. 아무리 중요한 내용이 담겨 있어도, 텍스트가 아닌 이미지이기 때문에 키워드 검색이 통하지 않았습니다.
직원들은 수백 페이지를 일일이 눈으로 읽으면서 원하는 정보를 찾아야 했습니다. 더 큰 문제는 손글씨나 오래된 인쇄물은 가독성이 떨어져서 읽기조차 힘들다는 것이었습니다.
바로 이런 문제를 해결하기 위해 OCR RAG가 등장했습니다. OCR RAG를 사용하면 스캔 문서도 디지털 텍스트처럼 검색할 수 있습니다.
또한 손글씨나 저품질 이미지도 최신 OCR 기술로 어느 정도 읽어낼 수 있습니다. 무엇보다 수십 년 전 문서도 현대적인 검색 시스템에 통합할 수 있다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ocr_pdf 함수는 PDF의 각 페이지를 처리합니다.
디지털 텍스트가 있는지 먼저 확인하고, 없으면 스캔 PDF로 간주합니다. 페이지를 300 DPI의 고해상도 이미지로 변환하는 것이 OCR 정확도를 높이는 핵심입니다.
Tesseract는 오픈소스 OCR 엔진으로, lang='kor+eng' 옵션으로 한국어와 영어를 동시에 인식합니다. 추출된 텍스트는 RecursiveCharacterTextSplitter로 적절한 크기의 청크로 분할됩니다.
각 청크는 페이지 번호와 출처 메타데이터와 함께 벡터 스토어에 저장되어, 검색 결과에서 어디서 나온 내용인지 추적할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 법률 사무소에서 과거 판례 문서를 관리한다고 가정해봅시다. 1970년대부터 축적된 판결문 스캔본이 수만 페이지일 때, OCR RAG를 적용하면 "부동산 계약 위반"같은 키워드로 관련 판례를 즉시 찾을 수 있습니다.
병원에서는 환자의 오래된 수기 진료 기록을 OCR로 디지털화하여, 의사가 과거 병력을 빠르게 조회할 수 있습니다. 역사 아카이브에서는 고문서를 OCR 처리하여 연구자들이 쉽게 검색할 수 있게 합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 OCR 결과를 검증 없이 그대로 사용하는 것입니다.
OCR은 완벽하지 않아서, 특히 저품질 이미지나 손글씨에서는 오류가 많습니다. "l"(소문자 L)과 "1"(숫자 1), "O"(대문자 O)와 "0"(숫자 0) 같은 문자를 혼동하기도 합니다.
따라서 중요한 문서는 OCR 후 사람이 검토하거나, 신뢰도가 낮은 부분을 자동으로 표시하는 것이 올바른 방법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
시스템을 완성한 김개발 씨가 법무팀에 시연했습니다. "해지 조건"을 검색하자 20년 전 계약서에서 관련 조항이 정확히 나타났습니다.
법무팀장이 놀라워했습니다. "이제 몇 분이면 찾을 수 있겠네요.
정말 대단합니다!" OCR RAG를 제대로 구축하면 레거시 문서도 현대적인 검색 시스템에 통합할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - OCR 전에 이미지를 전처리하세요. 대비 조정, 노이즈 제거, 기울기 보정이 정확도를 높입니다
- Tesseract 외에도 Google Cloud Vision, AWS Textract 같은 클라우드 OCR 서비스를 고려하세요
- OCR 결과의 신뢰도를 함께 저장하면, 낮은 신뢰도 부분을 우선 검토할 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.