# RAG

임베딩 모델을 e5-large, bge-m3로 키우면 검색이 좋아질까?

@Hong2026-10-03

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
임베딩 모델을 e5-small에서 e5-large나 bge-m3로 키우면 검색 정확도가 얼마나 오를까?
답
정답 문장을 상위 5개 청크에 넣은 질문은 e5-small 25, e5-large 27, bge-m3 25개로 30개 중 많아야 2개 차이였고, 큰 두 모델은 질문 하나의 임베딩이 23~24ms에서 277~296ms로 늘었다.
조건
Qdrant 1.19.1, sentence-transformers 6.1.0, Docker CPU 2.5개(PyTorch 스레드 2개), 정답 글 30편(문단 청크 562개), 질문 30개

환경

벡터 DB
Docker의 Qdrant 1.19.1(CPU 0.5개, 메모리 512MB), 거리 함수 Cosine, exact=True 전체 비교
임베딩
sentence-transformers 6.1.0, intfloat/multilingual-e5-small, intfloat/multilingual-e5-large, BAAI/bge-m3
클라이언트
Docker 컨테이너 CPU 2.5개, 메모리 3.5GB, PyTorch 스레드 2개(OMP_NUM_THREADS)
데이터
정답 글 30편(문단 청크 562개)과 질문 30개
반복
비용 측정은 세 모델을 번갈아 세 번
목차
  1. 세 모델
  2. 실습 준비
  3. 실습 1: 같은 문서, 같은 질문으로 비교
  4. 실습 2: 시간과 메모리
  5. 정리

이번 시간에는 임베딩 모델을 작은 모델에서 큰 모델로 바꾸면 검색이 얼마나 좋아지는지, Docker 위의 Qdrant와 sentence-transformers로 세 모델을 나란히 측정하며 배워가는 시간입니다.

예를 들어서 이런 상황을 생각해 볼게요. 이 사이트의 글로 만든 RAG 검색(질문과 가까운 글 조각을 찾아 LLM에 넘겨 답하게 하는 방식)은 지금까지 다국어 소형 임베딩 모델 intfloat/multilingual-e5-small을 써 왔어요.

여기에 "더 큰 모델로 바꾸면 검색이 좋아지지 않을까요?"라는 제안이 들어왔다고 해 볼게요. 후보는 같은 계열의 큰 모델 intfloat/multilingual-e5-large와, 최대 8192토큰까지 읽는 BAAI/bge-m3입니다. 그래서 세 모델로 같은 문서를 색인하고 같은 질문으로 검색해서, 품질과 비용을 차례로 재 볼게요.

세 모델

e5-small은 12층에 384차원, e5-large와 bge-m3는 24층에 1024차원 벡터를 만들어요. 두 e5 모델은 최대 512토큰까지만 읽고, bge-m3는 8192토큰까지 읽습니다. e5 두 모델은 질문에 query: , 문서에 passage: 를 붙여야 하고, bge-m3는 모델 카드에 "질문에 지시문을 붙일 필요가 없다"고 적혀 있어서 접두어를 붙이지 않았어요. 모델 디렉터리 크기는 471MB, 2.2GB, 2.2GB예요.

실습 준비

이 글의 명령은 모두 실습 코드의 embedding-model-compare/ 디렉터리에서 실행합니다.

  • compose.yaml: Qdrant와 Python 클라이언트 컨테이너, 모델 파일(hf)·Qdrant 저장소 볼륨
  • requirements.txt: 클라이언트에 설치할 패키지 버전 6개
  • prep.py: 글 목록 파일(매니페스트)에 등록된 글만 읽어 out/docs.jsonl로 모으는 스크립트
  • cmp.py: 색인·검색·평가·비용 명령을 모은 실습 스크립트
  • questions.jsonl: 검색 질문 30개와 정답 글·정답 문장

compose.yaml로 Qdrant와 Python 클라이언트 컨테이너를 띄울게요. 문서 디렉터리는 ARTICLES_DIR로 받아 읽기 전용으로 붙이고, 모델 파일은 hf 볼륨에 받습니다. Qdrant 저장소 볼륨은 클라이언트에도 읽기 전용으로 붙어 있지만 이 글에서는 쓰지 않아요. 자원은 일부러 작게 잡았어요.

  • 메모리: 클라이언트 3.5GB(3584m), Qdrant 512MB예요. 1024차원 모델은 float32 가중치만 2GB가 넘어서 클라이언트에 대부분을 줬습니다.
  • CPU: 클라이언트 2.5개, Qdrant 0.5개예요.
  • 스레드: OMP_NUM_THREADS로 PyTorch 스레드를 2개로 고정했어요. 실행마다 스레드 수가 달라져서 시간 비교가 흔들리지 않게 하려는 겁니다.

패키지 버전은 requirements.txt에 고정했어요. 메모리를 잴 psutil도 넣었습니다.

--extra-index-url https://download.pytorch.org/whl/cpu
torch==2.14.1+cpu
sentence-transformers==6.1.0
transformers==5.18.0
qdrant-client==1.19.1
numpy==2.5.3
psutil==7.2.2

이제 명령을 짧게 쓸 수 있도록 셸 함수 두 개를 만들고 컨테이너를 띄울게요. dc는 이 실습의 docker compose를, py는 클라이언트 컨테이너에서 Python 스크립트를 실행하는 함수예요.

prep.py는 매니페스트에 등록된 글만 읽어 out/docs.jsonl로 모읍니다. 색인할 문서는 이 사이트에 발행된 글 64편(1,635,094자)이라 글에 따로 싣지 않았어요. 그러니 ARTICLES_DIR에는 같은 매니페스트 형식으로 만든 여러분 문서의 디렉터리를 주시면 됩니다.

아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

export ARTICLES_DIR=/path/to/articles
mkdir -p out

dc() { docker compose -p embcmp "$@"; }

py() { dc exec -T client python "$@"; }

dc up -d
dc exec -T client pip install --no-cache-dir -q -r requirements.txt
py prep.py
문서 64편
전체 1,635,094자, 코드 블록을 뺀 산문 681,011자
가장 짧은 글 11,301자, 가장 긴 글 48,315자

cmp.py는 세 모델을 e5s, e5l, m3라는 짧은 이름으로 부르고, 모델마다 질문과 청크(검색 단위로 잘라 둔 글 조각) 앞에 붙일 접두어를 함께 적어 뒀어요. py cmp.py <명령> [인자...]로 부릅니다.

실습 1: 같은 문서, 같은 질문으로 비교

평가 세트는 questions.jsonl이에요. 질문 30개마다 정답 글과 정답 문장(gold_passage)이 정해져 있습니다.

유형은 개념(concept) 15개, 식별자(identifier, 설정 이름·오류 번호 같은 고유 이름이 든 질문) 8개, 헷갈림(confusable, 비슷한 주제의 다른 글과 헷갈리기 쉬운 질문) 7개예요. 질문마다 상위 20개 청크를 가져와 세 지표를 계산합니다.

  • 문서 recall@5: 중복을 지운 글 순서의 앞 5개 안에 정답 글이 있는 질문 수
  • 문단 적중@5: 상위 5개 청크 가운데 정답 문장을 온전히 담은 청크가 있는 질문 수
  • MRR@10: 정답 글이 n번째면 1/n을 주고, 10위 밖이면 0을 줘서 평균한 값

분할은 문단을 800자까지 묶고, 코드 블록을 빼고, 글 제목과 소제목을 붙여요. 검색은 Qdrant에 exact=True를 줘서 전체 비교로 했어요. 근사 검색의 오차가 섞이지 않게 하려는 겁니다.

색인할 문서 범위는 줄였어요. 이 머신에서 큰 두 모델은 e5-small보다 10배 넘게 느렸고, 64편 전체(청크 1,228개)를 색인하면 모델 하나에 1시간이 넘게 걸릴 것으로 보였거든요. 그래서 질문 30개의 정답 글 30편(청크 562개)만 색인하고, 이 범위를 para-gold라고 부를게요. Qdrant 컬렉션 이름은 <모델>-<분할>입니다(예: e5s-para-gold).

py cmp.py index e5s para-gold
py cmp.py index e5l para-gold
py cmp.py index m3 para-gold
e5s-para-gold  청크   562개  평균  619자  최대  500토큰  512토큰 초과    0개(0.0%)  임베딩 151초
e5l-para-gold  청크   562개  평균  619자  최대  500토큰  512토큰 초과    0개(0.0%)  임베딩 1725초
m3-para-gold   청크   562개  평균  619자  최대  498토큰  512토큰 초과    0개(0.0%)  임베딩 1604초

세 모델 모두 512토큰을 넘는 청크가 없어요. 가장 긴 청크가 500토큰 안팎이라, 이 분할에서는 세 모델이 같은 글을 끝까지 읽고 비교됩니다.

질문 30개로 검색하고 지표를 유형별로 나눠 출력해 볼게요. pairs는 모델 쌍마다 한쪽만 문단 적중에 성공한 질문 id를 보여 줍니다.

py cmp.py search e5s e5s-para-gold
py cmp.py search e5l e5l-para-gold
py cmp.py search m3 m3-para-gold
py cmp.py eval e5s-para-gold e5l-para-gold m3-para-gold
py cmp.py pairs e5s-para-gold e5l-para-gold m3-para-gold
e5s-para-gold  recall@5 30/30  문단적중@5 25/30  MRR@10 0.975
  concept     15개  recall@5 15/15  문단적중@5 11/15  MRR@10 1.000
  identifier   8개  recall@5 8/8  문단적중@5 8/8  MRR@10 1.000
  confusable   7개  recall@5 7/7  문단적중@5 6/7  MRR@10 0.893
e5l-para-gold  recall@5 30/30  문단적중@5 27/30  MRR@10 0.983
  concept     15개  recall@5 15/15  문단적중@5 14/15  MRR@10 1.000
  identifier   8개  recall@5 8/8  문단적중@5 8/8  MRR@10 1.000
  confusable   7개  recall@5 7/7  문단적중@5 5/7  MRR@10 0.929
m3-para-gold   recall@5 30/30  문단적중@5 25/30  MRR@10 0.983
  concept     15개  recall@5 15/15  문단적중@5 12/15  MRR@10 1.000
  identifier   8개  recall@5 8/8  문단적중@5 8/8  MRR@10 1.000
  confusable   7개  recall@5 7/7  문단적중@5 5/7  MRR@10 0.929
e5s-para-gold 만 [26]  e5l-para-gold 만 [7, 9, 12]
e5s-para-gold 만 [14, 26]  m3-para-gold 만 [1, 9]
e5l-para-gold 만 [7, 12, 14]  m3-para-gold 만 [1]
모두 놓친 질문 [30]

세 모델을 나란히 놓으면 이렇습니다.

  • 문서 recall@5: 세 모델 모두 30/30이에요. 정답 글을 찾는 데는 차이가 없습니다.
  • 문단 적중@5: e5-small 25, e5-large 27, bge-m3 25예요. e5-large가 질문 2개 앞섰고 bge-m3는 e5-small과 같습니다.
  • 유형별: 식별자 8개는 세 모델 모두 문단 적중 8/8이에요. 개념 유형에서 큰 모델이 1~3개 더 맞혔고, 헷갈림 유형에서는 오히려 1개씩 덜 맞혔습니다.

비율로 보면 질문 하나가 0.033이니 2개 차이는 0.067이고, 질문 30개로는 이 정도 차이가 우연인지 가리기 어려워요.

pairs 출력을 보면 e5-large는 3개(7, 9, 12)를 새로 맞히고 1개(26)를 놓쳤고, bge-m3는 2개(1, 9)를 새로 맞히고 2개(14, 26)를 놓쳤어요. 모델을 바꾸면 합계는 비슷해도 맞히는 질문이 바뀌는 거죠.

실습 2: 시간과 메모리

비용은 모델마다 새 프로세스를 띄워서 잴게요. 한 프로세스에서 모델을 차례로 바꾸면 앞 모델이 남긴 메모리가 뒤 모델의 측정에 섞이기 때문이에요. cost가 재는 값은 다음과 같습니다.

  • 로드: 모델을 불러오는 데 걸린 시간과 직후의 프로세스 RSS(실제로 물리 메모리에 올라온 크기)
  • 청크 임베딩: 정답 글 30편의 청크 가운데 고정 시드로 고른 32개를 배치 크기 32로 임베딩하는 시간, 그동안 0.05초마다 잰 RSS의 최댓값, 끝난 뒤 RSS
  • 질문 임베딩: 질문 30개를 하나씩 임베딩했을 때 걸린 시간의 중앙값. 검색 요청 하나가 질문을 벡터로 바꾸는 데 쓰는 시간이에요.

세 모델을 번갈아 세 번 돌립니다.

for r in 1 2 3; do for k in e5s e5l m3; do py cmp.py cost $k; done; done
e5s  스레드 2  시작   229MB  로드  6.4초 후   793MB  청크 32개  10.0초 최대  1207MB 끝난 뒤  1180MB  질문 1개 중앙값   24ms
e5l  스레드 2  시작   229MB  로드  7.1초 후   798MB  청크 32개 118.1초 최대  2749MB 끝난 뒤  2629MB  질문 1개 중앙값  296ms
m3   스레드 2  시작   229MB  로드  6.3초 후   757MB  청크 32개 124.8초 최대  2718MB 끝난 뒤  2600MB  질문 1개 중앙값  282ms
e5s  스레드 2  시작   229MB  로드  6.0초 후   792MB  청크 32개   9.0초 최대  1222MB 끝난 뒤  1186MB  질문 1개 중앙값   23ms
e5l  스레드 2  시작   229MB  로드  6.5초 후   798MB  청크 32개 115.9초 최대  2749MB 끝난 뒤  2630MB  질문 1개 중앙값  283ms
m3   스레드 2  시작   229MB  로드  5.9초 후   789MB  청크 32개 111.2초 최대  2747MB 끝난 뒤  2630MB  질문 1개 중앙값  277ms
e5s  스레드 2  시작   229MB  로드  6.8초 후   793MB  청크 32개   8.6초 최대  1207MB 끝난 뒤  1187MB  질문 1개 중앙값   24ms
e5l  스레드 2  시작   229MB  로드  5.3초 후   798MB  청크 32개 123.2초 최대  2748MB 끝난 뒤  2629MB  질문 1개 중앙값  296ms
m3   스레드 2  시작   229MB  로드  5.9초 후   758MB  청크 32개 115.8초 최대  2718MB 끝난 뒤  2601MB  질문 1개 중앙값  290ms

세 번 잰 값의 범위로 읽어 볼게요.

  • 청크 32개 임베딩: e5-small 8.6~10.0초, e5-large 115.9~123.2초, bge-m3 111.2~124.8초예요. 중앙값끼리 나누면 e5-large는 13.1배, bge-m3는 12.9배입니다.
  • 질문 하나: e5-small 23~24ms, e5-large 283~296ms, bge-m3 277~290ms로 약 12배예요. 검색 요청마다 질문 임베딩에 0.3초 가까이 더 듭니다.
  • 메모리: 최대 RSS는 e5-small 1.2GB, 큰 두 모델 2.7GB예요. 큰 모델을 서비스 프로세스 안에서 돌린다면 프로세스 하나에 2.7GB를 잡아야 해요.
  • 로드 직후 RSS: 가중치 크기가 4배 넘게 다른데도 세 모델 모두 760~800MB로 비슷해요. 그러니 모델 메모리는 로드 직후가 아니라 실제로 임베딩을 돌린 뒤의 값으로 판단해 주세요.

벡터 저장 크기는 계산으로 바로 나와요. float32 하나가 4바이트이니 벡터 하나가 1,536바이트에서 4,096바이트로 1024/384배, 약 2.67배 커지고, 청크 100만 개라면 1.5GB가 4.1GB가 됩니다.

정리

범위: Qdrant 1.19.1, sentence-transformers 6.1.0, CPU 2.5개(PyTorch 스레드 2개), multilingual-e5-small, multilingual-e5-large, bge-m3, 정답 글 30편(문단 청크 562개)과 질문 30개.

  • 문단 분할에서 문단 적중@5는 e5-small 25, e5-large 27, bge-m3 25였고 문서 recall@5는 셋 다 30/30이었으며, e5-large는 3개를 새로 맞히고 1개를 놓쳤습니다. 질문 30개로는 2개 차이가 우연인지 가리기 어려우니 합계가 아니라 질문 단위로 비교하세요.
  • 큰 두 모델은 청크 32개 임베딩이 약 13배(e5-small 8.6~10.0초, e5-large 115.9~123.2초), 질문 하나가 23~24ms에서 277~296ms였습니다. 질문 임베딩이 검색 요청마다 0.3초 가까이 더해지는 것을 지연 예산에 넣어 주세요.
  • 최대 RSS는 e5-small 1.2GB, 큰 두 모델 2.7GB였고, 로드 직후 RSS는 세 모델 모두 760~800MB로 비슷했습니다. 모델 메모리는 로드 직후가 아니라 임베딩을 돌린 뒤의 값으로 잡으세요.
내 링크 만들기