포스트

임베딩 모델 3파전: OpenAI vs Cohere vs BGE — “내 도메인”에서 이기는 선택 가이드

임베딩(embedding)은 “텍스트(혹은 이미지/문서)를 벡터로 압축”해 검색(retrieval), 추천, 클러스터링, 중복 제거, RAG 품질을 좌우합니다.

임베딩 모델 3파전: OpenAI vs Cohere vs BGE — “내 도메인”에서 이기는 선택 가이드

들어가며

임베딩(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으로 알려져 있고, Cohere embed-v4.0는 256/512/1024/1536을 지원합니다(이른바 Matryoshka Embeddings).3
    • 차원이 커지면 보통 성능이 오르지만:
      • 인덱스 크기 증가(메모리/디스크)
      • ANN 검색 지연 증가
      • HNSW/IVF 튜닝 난이도 상승
    • “조금 덜 좋은 embedding + 더 공격적인 rerank”가 전체 품질/비용에서 이기는 경우도 흔합니다.
  • (C) 멀티링구얼/멀티모달 요구
    • Cohere는 Embed 4를 멀티링구얼/멀티모달(텍스트+이미지/PDF류)까지 확장하는 방향을 명확히 하고 있습니다.4
    • BGE는 오픈소스 생태계에서 강력하며, 예를 들어 bge-large-en-v1.5 같은 모델 카드가 널리 사용됩니다.5

2) 내부 작동 흐름(검색 파이프라인 관점)

실무 RAG/검색에서 embedding은 보통 이렇게 들어갑니다.

  1. 문서 전처리/청킹
    • HTML/PDF/코드/위키 → 텍스트 정규화 → chunk(예: 300~800 tokens)
  2. 문서 embedding 생성 → Vector DB 인덱싱
    • (선택) “search_document” 타입으로 문서 벡터 생성(Cohere처럼)
  3. 질의 embedding 생성
    • (선택) “search_query” 타입으로 질의 벡터 생성
  4. ANN 검색(top-k)
    • cosine/dot + HNSW/IVF
  5. (권장) reranker로 top-k 재정렬
  6. 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
    • 장점: API 품질/운영 단순성, 다국어에서도 무난한 성능 포지션(공식 문서상 “most capable”).3
    • 단점: API 의존, 고차원으로 인덱스 비용이 커질 수 있음(3072).9
  • Cohere
    • 장점: 검색 지향 기능(타입 분리), Matryoshka 차원 옵션으로 시스템 예산에 맞추기 쉬움.2
    • 단점: 상용 API 의존, 멀티모달/배포 옵션은 좋지만 “내 환경/지역”에서의 지연/라우팅은 사전 확인 필요(커뮤니티에서 라우팅 이슈 언급 사례도 있음).10
  • BGE (오픈소스)
    • 장점: self-host 가능(보안/온프렘), 커스터마이즈/파인튜닝 여지, 특정 버전은 MTEB에서 강한 평가를 받은 이력.5
    • 단점: 운영 난이도(서빙/스케일링/모니터링), 다국어/도메인/버전별 편차가 큼(특히 영어 특화 모델 vs multilingual 모델 구분 필요).11

🚀 마무리

핵심은 “누가 MTEB 1등이냐”가 아니라, 내 도메인/언어/배포 제약/인덱스 예산/운영 인력에 맞는 선택입니다.

  • 도메인별 빠른 선택 가이드
    • 빠르게 상용 RAG 출시 + 운영 단순성: OpenAI text-embedding-3-*로 시작 → 비용/지연이 문제면 차선 검토3
    • 검색 품질(특히 query/doc 비대칭) + 차원 튜닝 유연성: Cohere embed-v4.0 + search_query/search_document 패턴을 기본 설계로2
    • 온프렘/데이터 주권/커스텀 튜닝: BGE 계열 self-host + “내 데이터 벤치마크”로 승부5

다음 단계로는 (1) 내 도메인 Qrels(정답 문서) 300개만 구축해서 mini-benchmark를 돌리고, (2) top-k 후보를 reranker로 재정렬하는 구성까지 포함해 end-to-end로 측정하는 것을 권합니다. (embedding만 바꿔도 좋아지는 영역과, rerank/청킹이 더 큰 레버리지인 영역이 명확히 갈립니다.)6

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.