임베딩 모델 3파전: OpenAI vs Cohere vs BGE — “내 도메인”에서 이기는 선택 가이드
임베딩(embedding)은 “텍스트(혹은 이미지/문서)를 벡터로 압축”해 검색(retrieval), 추천, 클러스터링, 중복 제거, RAG 품질을 좌우합니다.
들어가며
임베딩(embedding)은 “텍스트(혹은 이미지/문서)를 벡터로 압축”해 검색(retrieval), 추천, 클러스터링, 중복 제거, RAG 품질을 좌우합니다. 문제는 2026년 현재 임베딩 시장이 성숙하면서, 단순 리더보드(MTEB) 점수만 보고 고르면 실제 프로젝트에서 비용/지연/안정성/언어/도메인 적합성에서 크게 손해를 볼 수 있다는 점입니다.
- 언제 쓰면 좋나
- RAG/사내검색/고객지원: “질문-문서” 유사도 기반 top-k 후보를 안정적으로 뽑아야 할 때
- 데이터 파이프라인: 중복 문서 병합, 유사 이슈 티켓 묶기, 추천 후보군 생성
- 언제 쓰면 안 되나(혹은 보완이 필수)
- “정답은 단 하나”에 가까운 복잡 추론 QA: embedding만으로는 한계 → reranker/LLM 검증 필요
- 보안/온프레미스 강제: API-only 모델(OpenAI/Cohere) 단독으론 곤란 → BGE 같은 self-host 대안 고려
- 도메인 특화(의료/법률/화학 등): 범용 embedding은 recall은 나오지만 precision이 흔들릴 수 있음 → 도메인 평가/파인튜닝/특화 모델 필요(예: ChemTEB 같은 벤치마크 연구가 이를 시사).1
🔧 핵심 개념
1) “좋은 embedding”의 의미: 점수보다 파이프라인 적합성
임베딩 모델 비교에서 흔히 MTEB 평균 점수 같은 단일 수치로 판단하지만, 실제로는 아래 축이 더 중요합니다.
- (A) Query/Document 비대칭 최적화
- 검색은 query(짧고 모호) vs document(길고 정보 많음)가 분포가 다릅니다.
- Cohere는 embed 계열에서 “search_query / search_document” 같은 타입 분리를 강하게 밀고 있고(검색 최적화 포지션), 실제 문서 인덱싱과 질의 임베딩을 다르게 뽑는 설계를 제공합니다.2
- 이게 중요한 이유: 같은 모델/코사인 유사도라도 인덱싱 벡터의 “문서성”과 질의 벡터의 “의도성”을 분리하면 top-k 후보의 recall/precision이 같이 좋아지는 경우가 많습니다.
- (B) 차원(dimension)과 “정보 밀도”
- OpenAI
text-embedding-3-large는 3072-dim으로 알려져 있고, Cohereembed-v4.0는 256/512/1024/1536을 지원합니다(이른바 Matryoshka Embeddings).3 - 차원이 커지면 보통 성능이 오르지만:
- 인덱스 크기 증가(메모리/디스크)
- ANN 검색 지연 증가
- HNSW/IVF 튜닝 난이도 상승
- “조금 덜 좋은 embedding + 더 공격적인 rerank”가 전체 품질/비용에서 이기는 경우도 흔합니다.
- OpenAI
- (C) 멀티링구얼/멀티모달 요구
2) 내부 작동 흐름(검색 파이프라인 관점)
실무 RAG/검색에서 embedding은 보통 이렇게 들어갑니다.
- 문서 전처리/청킹
- HTML/PDF/코드/위키 → 텍스트 정규화 → chunk(예: 300~800 tokens)
- 문서 embedding 생성 → Vector DB 인덱싱
- (선택) “search_document” 타입으로 문서 벡터 생성(Cohere처럼)
- 질의 embedding 생성
- (선택) “search_query” 타입으로 질의 벡터 생성
- ANN 검색(top-k)
- cosine/dot + HNSW/IVF
- (권장) reranker로 top-k 재정렬
- LLM 컨텍스트 구성 → 답변 생성
여기서 모델 선택은 2)~4)의 비용/지연/품질을 결정하고, 도메인 특성은 1)과 5)에서 보정되는 경우가 많습니다.
💻 실전 코드
아래 예제는 “실제 서비스에 가까운” 형태로 (1) 문서 인덱싱 (2) 질의 검색 (3) 모델별 교체가 가능하도록 구성합니다.
Vector DB는 로컬에서 재현 가능한 faiss 기반으로 작성합니다(운영에선 Pinecone/Qdrant/Weaviate/pgvector 등으로 치환).
0) 설치/환경
1
2
3
4
python -m venv .venv
source .venv/bin/activate
pip install -U openai cohere numpy faiss-cpu tiktoken python-dotenv
.env
1
2
OPENAI_API_KEY=...
COHERE_API_KEY=...
1) 인덱싱 + 검색 파이프라인 (OpenAI/Cohere 공통 인터페이스)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import os
import numpy as np
import faiss
from dotenv import load_dotenv
load_dotenv()
# ---------- 공통 유틸 ----------
def normalize_l2(x: np.ndarray) -> np.ndarray:
# cosine 유사도를 dot으로 쓰려면 L2 normalize가 실전에서 자주 편함
return x / (np.linalg.norm(x, axis=1, keepdims=True) + 1e-12)
class VectorIndex:
def __init__(self, dim: int):
self.dim = dim
self.index = faiss.IndexHNSWFlat(dim, 32) # 실전에서 무난한 시작점
self.texts = [] # doc store (운영에선 DB/오브젝트 스토리지)
def add(self, vectors: np.ndarray, texts: list[str]):
vectors = normalize_l2(vectors.astype("float32"))
self.index.add(vectors)
self.texts.extend(texts)
def search(self, qvec: np.ndarray, k: int = 5):
qvec = normalize_l2(qvec.astype("float32"))
D, I = self.index.search(qvec, k)
return [(float(D[0][i]), self.texts[int(I[0][i])]) for i in range(k)]
# ---------- OpenAI embedder ----------
def embed_openai(texts: list[str], model: str = "text-embedding-3-large") -> np.ndarray:
# OpenAI 공식 모델 문서 기준으로 embeddings API 사용
# (SDK 버전별 호출 방식은 변할 수 있으니, 실제론 공식 문서를 확인)
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
resp = client.embeddings.create(model=model, input=texts)
vecs = np.array([d.embedding for d in resp.data], dtype="float32")
return vecs
# ---------- Cohere embedder ----------
def embed_cohere(
texts: list[str],
model: str = "embed-v4.0",
input_type: str = "search_document",
embedding_types: list[str] = ["float"],
dimensions: int = 1024,
) -> np.ndarray:
import cohere
co = cohere.Client(os.environ["COHERE_API_KEY"])
# Cohere Embed API는 dimensions(256/512/1024/1536) 및 input_type을 지원
resp = co.embed(
model=model,
texts=texts,
input_type=input_type,
embedding_types=embedding_types,
dimensions=dimensions,
)
# resp.embeddings.float: list[list[float]]
vecs = np.array(resp.embeddings.float, dtype="float32")
return vecs
# ---------- 데이터(현실적 시나리오) ----------
DOCS = [
"Incident #1842: Redis latency spike after enabling AOF rewrite. Mitigation: increase io-threads, tune appendfsync, add monitoring on disk iowait.",
"Runbook: Kubernetes HPA scale oscillation. Check metrics-server lag, set stabilizationWindowSeconds, avoid CPU-only signals for bursty workloads.",
"Postmortem: Vector DB recall drop after changing chunking from 800->200 tokens. Root cause: lost cross-sentence context; fix with overlap and reranker.",
"Guide: PostgreSQL pgvector index types (IVFFlat vs HNSW). Choose HNSW for low-latency, IVFFlat for batch; tune lists/probes.",
"Security: PII redaction pipeline. Use deterministic hashing for stable entity linking; keep raw text in vault; embeddings can still leak info.",
]
QUERY = "HPA가 갑자기 스케일이 왔다갔다 하는데 어디부터 봐야 해?"
def demo(provider: str):
if provider == "openai":
doc_vecs = embed_openai(DOCS, model="text-embedding-3-large")
q_vec = embed_openai([QUERY], model="text-embedding-3-large")
dim = doc_vecs.shape[1]
elif provider == "cohere":
# 문서/질의 타입 분리: Cohere가 특히 강조하는 패턴
doc_vecs = embed_cohere(DOCS, input_type="search_document", dimensions=1024)
q_vec = embed_cohere([QUERY], input_type="search_query", dimensions=1024)
dim = doc_vecs.shape[1]
else:
raise ValueError("provider must be 'openai' or 'cohere'")
vi = VectorIndex(dim=dim)
vi.add(doc_vecs, DOCS)
hits = vi.search(q_vec, k=3)
print(f"\n=== provider={provider} dim={dim} ===")
for score, text in hits:
print(f"{score:.4f} | {text[:90]}...")
if __name__ == "__main__":
demo("openai")
demo("cohere")
예상 출력(형태)
- “Kubernetes HPA scale oscillation…” 같은 런북 문서가 상위에 나오고,
- 벡터 차원/모델에 따라 2~3위가 “pgvector/HNSW” 같은 인접 주제로 바뀌는 식의 차이가 납니다.
2) BGE는 어디에 끼나? (self-host 전제)
BGE 계열(예: bge-large-en-v1.5, bge-m3)은 Hugging Face 기반으로 자체 호스팅이 일반적입니다.5
이 글의 코드에 BGE를 붙이려면 sentence-transformers 또는 transformers + (가능하면) ONNX/TensorRT로 추론을 최적화해 embed_* 함수만 교체하면 됩니다. 운영 포인트는 “성능”보다 GPU/CPU 비용 + 배포/업데이트/재현성 + 보안입니다.
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) 도메인별로 “내 데이터”로 미니 벤치마크를 만들어라
- 공개 벤치마크(MTEB)는 방향성만 줍니다.
- 내 도메인(예: 커머스 상품, 고객센터 티켓, 개발 런북)에서
- (query, relevant_doc) 쌍 200~1000개만 만들어도 모델 선택이 명확해집니다.
- 최근 임베딩 선택 프레임워크를 제안하는 연구도 “실무형 벤치마킹”을 강조합니다.6
2) Cohere를 쓴다면 input_type 분리를 기본값으로
search_document로 인덱싱,search_query로 질의 → 이 패턴이 “검색”에서 꽤 자주 이득을 줍니다. (그리고 Cohere 문서/비교표에서도 이 지점이 차별점으로 언급됩니다.)2
3) 차원은 “성능”이 아니라 “시스템 예산”으로 정하라
- Cohere
embed-v4.0는 256~1536 차원을 지원합니다.7 - 1024로 시작해보고, (a) 인덱스 메모리/지연이 빡세면 512로 내리고 (b) recall이 부족하면 1536로 올리는 식이 실무적입니다.
- OpenAI
text-embedding-3-large는 고차원(3072) 특성상 “성능은 좋지만 인덱스 비용”을 감안해야 합니다.3
흔한 함정/안티패턴
- 함정 1: chunk를 너무 잘게 쪼개서 recall이 떨어짐
- 작은 chunk는 “정확해 보이지만” 질의가 문맥을 필요로 하면 top-k가 망가집니다(문장 간 정보가 분리됨).
- 해결: overlap, 섹션 기반 청킹, 혹은 reranker 도입.
- 함정 2: 모델/버전 교체 시 재색인 안 함
- embedding 공간이 바뀌면 이전 벡터와 섞으면 안 됩니다.
- “점진적 이관(dual-write + shadow index)” 전략이 안전.
- 함정 3: 리더보드 상위 = 내 도메인 상위라고 믿기
- 실제로 “특정 공격적/적대적(adversarial) 조건”이나 데이터 노이즈에서 모델 순위가 뒤집힌다는 사용자 벤치마크 공유도 존재합니다(신뢰도는 낮을 수 있으나, ‘내 데이터로 확인’의 필요성을 보여줌).8
비용/성능/안정성 트레이드오프(현실 결론)
- OpenAI
- Cohere
- BGE (오픈소스)
🚀 마무리
핵심은 “누가 MTEB 1등이냐”가 아니라, 내 도메인/언어/배포 제약/인덱스 예산/운영 인력에 맞는 선택입니다.
- 도메인별 빠른 선택 가이드
다음 단계로는 (1) 내 도메인 Qrels(정답 문서) 300개만 구축해서 mini-benchmark를 돌리고, (2) top-k 후보를 reranker로 재정렬하는 구성까지 포함해 end-to-end로 측정하는 것을 권합니다. (embedding만 바꿔도 좋아지는 영역과, rerank/청킹이 더 큰 레버리지인 영역이 명확히 갈립니다.)6
https://developers.openai.com/api/docs/models/text-embedding-3-large ↩︎ ↩︎2 ↩︎3 ↩︎4
https://huggingface.co/BAAI/bge-large-en-v1.5 ↩︎ ↩︎2 ↩︎3 ↩︎4
https://www.reddit.com/r/Rag/comments/1rvyq87/updated_adversarial_embedding_benchmark_14_models/ ↩︎
https://pecollective.com/tools/text-embedding-models-compared/ ↩︎
https://www.reddit.com/r/openrouter/comments/1soucem/are_all_embedding_models_except_openai_down/ ↩︎