임베딩 기술을 활용한 지능형 정보 검색
문서를 분할한 후에는 RAG(Retrieval-Augmented Generation) 아키텍처의 핵심 단계인 벡터화와 검색이 필요하다. 이 과정은 사용자의 질문에 대해 가장 관련성 높은 컨텍스트를 찾아내는 역할을 하며, 전체 시스템 성능에 결정적인 영향을 미친다.
텍스트를 숫자로 변환: 벡터 임베딩의 원리
컴퓨터는 자연어를 직접 이해하지 못하므로, 의미 정보를 보존하면서 텍스트를 고차원 벡터 공간에 매핑하는 과정이 필요하다. 예를 들어:
입력 문장: "손오공이 금고봉을 사용함"
임베딩 결과: [0.14, -0.31, 0.58, ..., 0.76] // 1024차원 실수 배열
이러한 수치 표현 덕분에 다음과 같은 작업이 가능해진다:
- 두 문장 간의 의미적 유사도 계산
- 대규모 문서 집합 내에서 관련 콘텐츠 탐색
- 비슷한 주제의 자료 자동 군집화
중요한 점은 유사한 의미를 가진 문장은 벡터 공간에서도 가까운 위치에 존재한다는 것이다. 예컨대 "불권을 날림"과 "화염 기술 사용"은 어휘는 다르지만 임베딩 결과는 서로 근접하게 나타난다.
주요 검색 전략 비교
현재까지 개발된 대표적인 정보 검색 방식은 세 가지로 나눌 수 있다.
1. 키워드 기반 검색 (BM25)
통계 기반 방법으로, 단어 출현 빈도와 역문서 빈도(IDF)를 기반으로 관련성을 평가한다. 별도의 딥러닝 모델 없이도 빠르게 구현 가능하다.
쿼리: "불권"
결과:
- "원숭이가 불권으로 요괴를 물리침" → 정확히 일치 → 높은 점수
- "손오공이 화염 공격을 시전함" → 동의어 포함 → 낮은 점수
장점은 속도와 정밀 매칭 능력이며, 전문 용어나 코드 검색에 적합하다. 하지만 어휘 다양성 문제(동의어/유의어)를 해결하지 못한다.
2. 의미 기반 검색 (Dense Retrieval)
임베딩 모델을 사용하여 쿼리와 문서를 동일한 벡터 공간에 매핑하고, 코사인 유사도 등을 통해 관련성을 판단한다.
쿼리: "불권"
결과:
- "원숭이가 불권으로 요괴를 물리침" → 어휘 일치 → 높은 유사도
- "손오공이 화염 공격을 시전함" → 의미 유사 → 높은 유사도
자연어의 맥락을 반영할 수 있어 일반 질의응답 시스템에 효과적이나, 계산 비용이 다소 크고 정확한 용어 매칭에는 약점이 있다.
3. 하이브리드 검색 (Hybrid Approach)
키워드와 의미 검색의 장점을 결합하여 더 안정적인 검색 성능을 제공한다. 일반적으로 각 결과에 가중치를 적용해 통합 점수를 산출한다.
최종 점수 = α × BM25_점수 + (1−α) × 벡터_유사도
예: BM25 점수가 0.8이고 벡터 유사도가 0.9이며 α=0.6이라면 최종 점수는 0.84가 된다. 이 방식은 다양한 유형의 질의에 강건하며, 실제 서비스 배포 시 추천되는 전략이다.
BGE-M3: 다기능 오픈소스 임베딩 모델
BAAI에서 개발한 BGE-M3은 다음 세 가지 특징을 갖춘 강력한 임베딩 솔루션이다:
- 다기능성: 밀집, 희소, 다중 벡터 방식 동시 지원
- 다국어: 100개 이상 언어 처리 가능
- 다단위: 문장, 단락 등 다양한 길이의 텍스트 처리
1. 밀집 임베딩 (Dense Embedding)
전체 텍스트를 하나의 고정 차원 벡터로 압축한다. 의미 기반 검색 및 유사도 분석에 최적화되어 있으며, 대부분의 RAG 시스템에서 기본으로 사용된다.
from FlagEmbedding import BGEM3FlagModel
model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=False)
text = ["원숭이가 불권을 펼쳐 요괴를 물리침"]
embedding = model.encode(text, return_dense=True)['dense_vecs']
print(f"임베딩 차원: {embedding.shape}") # (1, 1024)
2. 희소 임베딩 (Sparse Embedding)
중요한 토큰만 선택하고 그 중요도(가중치)를 부여하는 방식으로, BM25와 유사한 동작을 한다. 정확한 키워드 매칭이 필요한 경우 유리하다.
sparse_result = model.encode(text, return_sparse=True)['lexical_weights']
print("상위 가중치 토큰:")
for token, weight in sorted(sparse_result[0].items(), key=lambda x: -x[1])[:5]:
print(f"토큰 ID {token}: {weight:.4f}")
3. 다중 벡터 임베딩 (ColBERT 스타일)
각 토큰마다 독립된 벡터를 생성하여 세밀한 정렬이 가능하다. 질의와 문서 간의 "부분 일치"도 효과적으로 포착할 수 있지만, 저장 및 계산 비용이 크다.
colbert_vecs = model.encode(text, return_colbert_vecs=True)['colbert_vecs']
print(f"토큰 수: {colbert_vecs[0].shape[0]}")
print(f"각 토큰 벡터 차원: {colbert_vecs[0].shape[1]}")
이미지와 텍스트를 연결하는 멀티모달 임베딩
CLIP 계열 모델은 이미지와 텍스트를 동일한 임베딩 공간에 정렬함으로써, "이미지로 텍스트 찾기" 또는 "텍스트로 이미지 검색" 기능을 가능하게 한다.
로컬 CLIP 모델 사용
Hugging Face에서 제공하는 openai/clip-vit-base-patch32 등의 모델을 로컬에 로드하면 완전한 오프라인 운영이 가능하다. 초기 다운로드 크기는 약 600MB 수준이며, GPU 없이도 CPU에서 실행 가능하다.
Jina AI API 기반 클라우드 접근
자체 인프라를 유지관리하고 싶지 않은 경우, Jina AI의 REST API를 활용할 수 있다. base64 인코딩된 이미지를 전송하면 해당 이미지의 임베딩 벡터를 반환받을 수 있다.
import requests
import numpy as np
headers = {"Authorization": "Bearer YOUR_API_KEY"}
payload = {
"model": "jina-clip-v1",
"input": [{"image": "data:image/jpeg;base64,..."}]
}
response = requests.post("https://api.jina.ai/v1/embeddings", json=payload, headers=headers)
img_embedding = np.array(response.json()["data"][0]["embedding"])
무료 티어로 월 100만 토큰까지 이용 가능하며, 국내에서도 접근성이 우수하다.
실전 적용: 하이브리드 검색 파이프라인 구현
다음은 BM25와 밀집 임베딩을 조합한 검색 시스템 예시이다.
from langchain_core.documents import Document
from langchain_community.retrievers import BM25Retriever
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
# 문서 준비
raw_texts = [
"원숭이는 쇠창살방패를 장비함",
"무회곡에서 요괴와 전투 시작",
"불권으로 요괴를 격퇴하고 금강신체 발동"
]
docs = [Document(page_content=t) for t in raw_texts]
# BM25 리트리버 생성
bm25 = BM25Retriever.from_documents(docs, k=3)
# 벡터 리트리버 생성
embed_model = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = InMemoryVectorStore(embed_model)
vectorstore.add_documents(docs)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 쿼리 처리
query = "원숭이의 무기와 기술은 무엇인가?"
bm25_results = bm25.invoke(query)
vector_results = vector_retriever.invoke(query)
# 중복 제거 후 통합
combined_content = list({doc.page_content for doc in bm25_results + vector_results})
print("통합 검색 결과:")
for content in combined_content:
print(f"• {content}")
추가로 reranking(재정렬) 단계를 도입하면, 처음에 광범위하게 문서를 추출한 후 의미 모델로 다시 순위를 매겨 최종 결과를 선별할 수 있다.