모델별 실행 레시피
최종 업데이트
한국어 PDF 검색에 임베딩과 리랭커 붙이기
먼저 임베딩으로 후보를 좁히고, 느린 리랭커는 상위 후보에만 적용해요.
BAAI/bge-m3 임베딩 모델은 PDF에서 추출한 단락을 검색하고, BAAI/bge-reranker-v2-m3는 상위 후보를 다시 점수화해 순서를 바꿔요. 두 모델을 로컬에서 실행해 고정된 한국어 질의로 기본 순위와 재정렬 결과를 비교할 수 있어요. 첫 실행에는 모델 가중치를 내려받을 인터넷과 디스크 공간이 필요해요.
PDF에 답이 있어도 검색 결과의 위쪽에 나오지 않을 수 있어요. 먼저 임베딩으로 후보를 찾고, 리랭커가 질문과 각 단락을 함께 읽어 순서를 다시 정하게 해요. 아래 예제는 BGE-M3와 BGE 리랭커 v2-m3로 내 PDF를 읽고 단락의 순위·페이지·처리 시간을 출력해요.
후보 검색과 재정렬은 다른 계산이에요
임베딩 모델은 질문과 각 단락을 벡터로 바꾸고 유사한 단락을 빠르게 찾아요. 리랭커는 이미 검색된 후보마다 질문과 본문을 함께 읽고 관련성 점수를 계산해 순서를 바꿔요. 그래서 리랭커만으로 PDF 전체를 검색하는 것이 아니라, 임베딩 검색 후 상위 K개를 다시 정렬하는 구성으로 비교해요.

다국어 모델과 라이선스를 맞춰요
BAAI/bge-m3 모델 카드는 다국어 임베딩을 제공하고 라이선스를 MIT로 표시해요. BAAI/bge-reranker-v2-m3 모델 카드는 다국어 cross-encoder이며 Apache-2.0 라이선스를 표시해요. 두 모델 모두에서 한국어 지원을 실험할 수 있지만, 모델 카드의 다국어 표시만으로 특정 도메인의 검색 정확도가 보장되지는 않아요.
각 모델 카드에서 저장소의 license, 지원 언어, 입력 길이, 실행 예제를 확인해요. 임베딩 모델은 query: ...처럼 질의 접두사를 요구할 수 있지만 BGE-M3 예제는 같은 encode 인터페이스를 사용해요. 두 모델을 처음 로드할 때는 모델 파일이 내려받아져 로컬 캐시에 저장돼요.

PDF에서 텍스트를 뽑고 검색 후보를 만들어요
스캔 PDF인지 텍스트 PDF인지 먼저 확인하고, 표·머리글·페이지 번호를 유지할 파서를 선택해요. 같은 PDF 집합과 질문, 사람이 확인한 정답 페이지를 준비해야 임베딩 비교가 OCR1 추출 차이에 흔들리지 않습니다.
문단 ID에는 원본 파일과 페이지를 연결해 둡니다. 잘못 추출된 표가 정답처럼 보이면 검색 모델의 순위보다 파서 출력을 먼저 고쳐야 해요.
같은 검색 질의로 전후 순위와 시간을 기록해요
sample.pdf에서 pypdf로 페이지별 텍스트를 읽고 700자 단위로 나눠 source, page, chunk_id를 붙여요. 스캔 이미지뿐인 PDF라면 추출 텍스트가 비어 OCR이 필요하다는 오류를 보여줘요. 검색 예제는 실제 PDF 단락을 대상으로 하지만, 정답 라벨이 없어 품질 평가 자료는 아니에요. Sentence Transformers와 pypdf를 설치하고 모델 가중치는 첫 실행 때 내려받아요.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U sentence-transformers pypdf검색할 때는 문서 벡터를 한 번만 계산해 보관하고, 질문 벡터 계산과 코사인 유사도 순위를 baseline 검색으로 측정해요. 같은 상위 후보만 CrossEncoder에 넘겨 재정렬해요. 코드는 기본 순위와 재정렬 순위, 검색 시간, 리랭커만의 시간, 두 단계 합계를 보여 줘요.
from pathlib import Path
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer, CrossEncoder
from sentence_transformers.util import cos_sim
from time import perf_counter
pdf_path = Path("sample.pdf")
reader = PdfReader(pdf_path)
passages = []
for page_number, page in enumerate(reader.pages, start=1):
text = " ".join((page.extract_text() or "").split())
for start_char in range(0, len(text), 700):
chunk = text[start_char:start_char + 700].strip()
if chunk:
passages.append({"chunk_id": f"p{page_number}-{start_char}", "text": chunk, "source": pdf_path.name, "page": page_number})
if not passages:
raise ValueError("No selectable PDF text found; use a text PDF or run OCR first.")
query = "환불은 며칠 안에 신청해야 하나요?"
embedder = SentenceTransformer("BAAI/bge-m3")
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
# One-time indexing is outside both per-query timers.
doc_vectors = embedder.encode([p["text"] for p in passages], normalize_embeddings=True)
start = perf_counter()
query_vector = embedder.encode([query], normalize_embeddings=True)
candidate_scores = cos_sim(query_vector, doc_vectors)[0].tolist()
candidates = sorted(zip(passages, candidate_scores), key=lambda x: x[1], reverse=True)[:3]
baseline_ms = (perf_counter() - start) * 1000
print("baseline ranks:")
for rank, (p, score) in enumerate(candidates, 1):
print(rank, p["source"], p["page"], p["chunk_id"], round(float(score), 4))
start = perf_counter()
r_scores = reranker.predict([(query, p["text"]) for p, _ in candidates])
rerank_ms = (perf_counter() - start) * 1000
reranked = sorted(zip([p for p, _ in candidates], r_scores), key=lambda x: float(x[1]), reverse=True)
print("reranked ranks:")
for rank, (p, score) in enumerate(reranked, 1):
print(rank, p["source"], p["page"], p["chunk_id"], round(float(score), 4))
print("baseline_query_ms", round(baseline_ms, 2))
print("reranker_only_ms", round(rerank_ms, 2))
print("query_total_ms", round(baseline_ms + rerank_ms, 2))python rag_compare.py이 코드에는 정답 라벨이 없어 Recall@5나 MRR@10을 계산하지 않아요. 다음으로 실제 문서의 관련 단락을 표시한 한국어 질문 세트를 만들어 같은 질문에서 baseline과 reranked 순위를 비교해요. 10~20개 질문은 첫 점검용이에요. p50·p95 지연은 워밍업 뒤 같은 질문을 여러 번 측정하고, 첫 모델 로드와 문서 색인 시간은 빼요.
검색 품질과 비용을 함께 보고 적용 여부를 정해요
임베딩만 쓴 결과와 리랭킹 후 결과에서 정답 문서 순위를 나란히 놓고 비교해요. 10~20개 질의로 Recall@5와 MRR@10을 계산하고 p50·p95 지연도 기록하면 품질 개선과 응답 추가 시간을 함께 볼 수 있어요. 모델마다 배치 크기2와 하드웨어를 같게 맞추고 동일한 PDF 추출 텍스트를 사용해요.
모델을 로컬에서 실행해도 첫 가중치 다운로드와 라이브러리 설치에는 네트워크가 사용돼요. BGE-M3 저장소와 리랭커 저장소 라이선스를 각각 확인하고, 문서 임베딩을 재사용할 때에는 모델 버전·chunk 설정이 바뀌면 색인을 다시 만들어야 해요. 리랭커는 후보 검색에서 놓친 문서를 새로 찾아내지 못하므로 Recall@K가 낮으면 먼저 청킹이나 임베딩 검색을 조정해요.

BGE-M3 dense 검색과 reranker를 분리해요
BGE-M3는 dense, sparse, multi-vector 경로를 지원하지만 첫 비교에서는 dense 검색 한 경로만 고정해요. 상위 20개 후보가 정해진 뒤 BGE reranker를 적용하면 1단계 검색의 누락과 재정렬 효과를 따로 볼 수 있습니다.
질의마다 Hit@5와 정답 순위를 남기고, 평가 집합 전체의 Hit@5·MRR@10을 집계해요. 리랭킹 시간은 질의별로 기록하고, 색인 폴더 크기는 질의 수치가 아니라 해당 모델·청크·색인 설정 한 행에 적습니다. 첫 검색 후보에 든 정답 비율도 함께 계산하세요.
용어 각주
자주 묻는 질문
- 이 글에서 가장 먼저 확인할 것은 무엇인가요?
- `BAAI/bge-m3` 임베딩 모델은 PDF에서 추출한 단락을 검색하고, `BAAI/bge-reranker-v2-m3`는 상위 후보를 다시 점수화해 순서를 바꿔요. 두 모델을 로컬에서 실행해 고정된 한국어 질의로 기본 순위와 재정렬 결과를 비교할 수 있어요. 첫 실행에는 모델 가중치를 내려받을 인터넷과 디스크 공간이 필요해요.
- 조건과 출처는 어디서 확인하나요?
- 각 모델 카드에서 저장소의 license, 지원 언어, 입력 길이, 실행 예제를 확인해요. 임베딩 모델은 `query: ...`처럼 질의 접두사를 요구할 수 있지만 BGE-M3 예제는 같은 encode 인터페이스를 사용해요. 두 모델을 처음 로드할 때는 모델 파일이 내려받아져 로컬 캐시에 저장돼요.
