딱 맞는 선택 찾기: OSS, VoyageAI 및 OpenAI의 Zilliz Cloud Pipelines에서 AI 검색(RAG)을 위한 임베딩 생성
이 게시물은 Christy Bergman과 Jiang Chen이 작성했습니다.
AI Retrieval Augmented Generation (RAG) 애플리케이션에 맞춤화된 임베딩 모델에 대한 저희 블로그 게시물에 오신 것을 환영합니다. 여기서 다룰 내용은 다음과 같습니다:
RAG(검색을 포함한 생성형 AI)에서 임베딩 모델이 사용되는 방식 소개
가장 일반적인 유형의 임베딩 모델인 SBERT 소개
임베딩 모델의 MTEB 리더보드와 사용 방법
Zilliz Cloud Pipelines에 자동으로 포함된 여섯 가지 임베딩 모델
각 모델에 대한 설명과 필요에 가장 적합한 임베딩 모델을 선택하는 팁
시작해 보겠습니다!
Rag로 AI에 데이터 추가하기: 임베딩 모델 + LLM이 사용되는 방식
Retrieval Augmented Generation (RAG)은 질의응답 봇을 위한 대표적인 접근 방식으로 부상했습니다. 그러나 모든 모델에 내재된 지식 학습 컷오프 날짜로 인해 최신 데이터에 대한 인식이 부족할 수 있습니다. 대부분의 프로덕션 사용 사례는 이러한 간극을 메우기 위해 모델에 특정 지식을 보완합니다.
AI 답변이 과거에 머물러 있나요? 학습된 내용만 아는 봇에 지치셨나요?
RAG는 데이터를 AI의 지식에 통합함으로써 해결책을 제시합니다. 이 패턴은 임베딩 모델과 Large Language Models (LLMs)를 사용합니다. 작동 방식은 다음과 같습니다:
Embedding Model을 사용한 데이터 준비: 임베딩 모델을 사용해 모든 문서의 텍스트 청크에 대한 vector embeddings를 생성하여 RAG를 시작합니다. 이는 YOUR 도메인 지식을 나타내는 YOUR 벡터 공간이 되며, 각 정보 조각은 벡터로 표현됩니다.
동일한 Embedding Model을 사용한 인덱싱 및 검색: 지식이 벡터로 인코딩되면 컴퓨터는 벡터 검색을 빠르게 실행할 수 있습니다. 검색 알고리즘을 위해 벡터를 데이터 구조로 구성하는 것을 인덱싱이라고 합니다. 인덱스는 Milvus와 같은 Vector Databases에서 사용됩니다. 질문을 하면 데이터베이스는 인덱스를 사용해 도메인 지식 벡터 공간에서 질문 벡터와 가장 가까운 벡터(문장 또는 단락을 나타냄)를 찾습니다. 데이터를 임베딩하는 데 사용한 동일한 임베딩 모델을 사용해 질문 벡터를 생성합니다.
LLM Model을 통한 답변 생성: 이것이 “Generative AI” 부분입니다. 이 단계에서는 ChatGPT와 같은 LLM 모델이 관련 도메인 지식을 활용해 질문에 답합니다. RAG 데이터는 검색된 상위 K개의 텍스트를 프롬프트에 주입하여 LLM 모델에 전달되며, 이 프롬프트에는 질문, 컨텍스트(상위 K개 텍스트), 그리고 “이 프롬프트의 컨텍스트에 있는 지식만 사용하여 질문에 답하라”와 같은 지시가 포함됩니다.
RAG를 사용하면 LLM은 제공된 도메인 지식을 기반으로 답변을 생성하여 최신 데이터에 즉시 접근하고 질문을 더 잘 이해합니다.
이 RAG 패턴은 연구로 뒷받침됩니다. 논문 Lost in the Middle을 참조하세요. 이 논문은 LLM이 생성한 답변의 Recall 정확도가 모델의 Context 프롬프트에 넣는 검색된 텍스트 수가 늘어날수록 감소한다는 것을 보여줍니다. 추가로:
LLM에는 컨텍스트 크기에 한계가 있습니다(현재 GPT-4 Turbo의 경우 128K).
토큰당 비용이 있으므로 항상 모든 정보를 전달하는 것은 더 비쌉니다.
Sentence-BERT 임베딩 모델
최신 임베딩 모델은 Encoder-part of transformers에서 파생됩니다. 반면 LLM 모델(예: ChatGPT)은 트랜스포머의 Decoder-part에서 구축됩니다. 가장 일반적인 임베딩 모델 클래스는 SBERT (Sentence-BERT)로, BERT를 기반으로 하지만 완전한 문장을 이해하는 데 특화되어 있습니다. 따라서 SBERT는 "The cat sat on the mat"와 "The mat sat on the cat"의 차이를 구분할 수 있습니다. 이는 기본 BERT라면 하지 못할 일입니다!
의미론은 단어 이면의 의미를 가리킵니다. 이는 LLM의 맥락에서 특히 중요한데, 같은 단어라도 문맥, 순서 또는 사용 방식에 따라 서로 다른 의미를 가질 수 있기 때문입니다. SBERT에 대한 자세한 내용은 관심 있는 독자라면 해당 모델에 관한 훌륭한 배경 설명 글을 여기에서 확인해 보시기 바랍니다.
RAG 애플리케이션을 처음부터 코딩한다고 가정해 보겠습니다. 시작하기에 매우 좋은 방법은 HuggingFace MTEB Leaderboard에서 임베딩 모델을 선택하는 것이며, RAG와 가장 관련성이 높으므로 "Retrieval Average" 열을 기준으로 (내림차순) 정렬합니다. 그런 다음 가장 작으면서 순위가 높은 임베딩 모델을 선택합니다. 리더보드는 계속해서 바뀝니다! 하지만 Python에서 HuggingFace로 임베딩 모델을 전환하는 것은 단일 변수를 조정하는 것만큼 간단합니다.
MTEB Retrieval 성능은 10에서의 정규화 할인 누적 이득(NDCG@10)으로 측정됩니다. 이 지표는 이상적인 순위 순서에 비례하여 사용자에게 반환된 결과에서, 낮은 순위의 항목보다 높은 순위의 항목에 더 큰 가중치를 부여하는 비율 합계를 계산함으로써 top-K 목록의 품질을 측정합니다.
이미지 출처: HuggingFace MTEB Leaderboard, 2024년 2월 20일 접속.
Massive Text Embedding Benchmark (MTEB)는 8개 작업과 58개 데이터셋(10개 다국어 112개 언어)에 걸쳐 임베딩 모델을 평가합니다. 여덟 가지 작업은 bitext mining, classification, clustering, pair classification, reranking, retrieval, semantic textual similarity (STS), summarization입니다.
Zilliz Cloud Pipelines에 내장된 6가지 주요 임베딩 모델
Zilliz Cloud Pipelines는 최근 다양한 임베딩 모델 선택지에 대한 지원을 출시했습니다.
| 제작자 | 모델 | 임베딩 차원 | 컨텍스트 길이 | 사용 사례 작업 | 오픈 소스 | *MTEB 점수 |
| BAAI | bge-base-en-v1.5 | 768 | 512 | 일반 EN 텍스트 | 예 | 53 |
| BAAI | bge-base-zh-v1.5 | 768 | 512 | 일반 ZH 텍스트 | 예 | 69 |
| VoyageAI | voyage-2 | 1024 | 4K | 고품질 RAG 챗봇 | 아니요 | 제공되지 않음 |
| VoyageAI | voyage-code-2 | 1536 | 16K | 높은 재현율의 코드 완성 | 아니요 | 제공되지 않음 |
| OpenAI | text-embedding-3-small | 512-1536 | 8K | 실시간 다국어 텍스트 챗봇 | 아니요 | 62 (512) 62 (1536) |
| OpenAI | text-embedding-3-large | 256-3072 | 8K | 실시간 다국어 텍스트 챗봇 | 아니요 | 65 (3072) 62 (256) |
*HuggingFace MTEB Leaderboard, Retrieval 기준 내림차순 정렬, 2024년 2월 26일 접속.
임베딩 차원 = 모델이 생성하는 벡터의 길이; 더 큰 벡터는 더 많은 의미를 포착할 수 있지만 저장 효율이 낮을 수 있습니다.
컨텍스트 길이 = 모델이 단일 시간 단계에서 한 번에 처리할 수 있는 최대 토큰 수. 대부분의 임베딩 모델의 출력 벡터는 정규화되어 있으므로 dot-product와 cosine similarity는 동일합니다.
⚠️ 참고: MTEB 벤치마크가 유용한 지침을 제공하더라도, 일부 모델은 과적합된 것으로 알려져 있습니다! 항상 자체 평가를 수행하세요!
**Zilliz Cloud Pipelines를 사용하면 가입하고 DevOps 또는 ML 인프라 문제 없이 RAG 애플리케이션을 구축하여 무료로 시작할 수 있습니다.
이미지 출처: Zilliz Cloud Pipelines에 내장된 Embedding Models 발표 블로그
BAAI/bge-base-en(or zh)-v1.5 임베딩 모델
이 오픈 소스 SBERT 모델은 HuggingFace에서 사용할 수 있습니다. 이러한 소형 모델의 몇 가지 장점:
작고, 오픈 소스이며, CPU 친화적입니다.
노트북 또는/또는 작은 클라우드 리소스에서 작업하기에 좋은 선택입니다.
데이터를 청킹하는 동안의 수집과 질문이 있을 때마다의 쿼리 지연 시간에서 가장 빠릅니다.
API 호출이 무료이고 GPU가 필요하지 않아 비용 효율적입니다.
중국어와 영어 모두로 학습되었습니다.
| Creator | Model | Embedding****Dim | Context Length | Use Case Tasks | Open Source | *MTEB Score |
| BAAI | bge-base-en-v1.5 | 768 | 512 | 일반 EN 텍스트 | Yes | 53 |
| BAAI | bge-base-zh-v1.5 | 768 | 512 | 일반 ZH 텍스트 | Yes | 69 |
*HuggingFace MTEB Leaderboard, Retrieval 기준 내림차순 정렬, 2024년 2월 26일 접속.
VoyageAI의 voyage-2 및 voyage-code-2 임베딩 모델
이 독점 모델은 서로 다른 데이터에 대해 대조 학습과 중요도 재샘플링을 사용해 학습되었으며, 서로 다른 작업에 맞게 파인튜닝되었습니다. Voyage-2는 대화 데이터로 학습되었고 대화 의도에 맞게 파인튜닝되었습니다. Voyage-code-2는 코드 데이터로 학습되었고 코드 완성에 맞게 파인튜닝되었습니다.
참고: MTEB 리더보드에 나열된 Voyage-lite-02-instruct(STS 또는 “diverse corpora” 범주 기준 내림차순 정렬)는 여기서 설명한 프로덕션 모델인 voyage-2 및 voyage-code-2와 다르며 혼동해서는 안 됩니다.
이러한 모델의 몇 가지 장점:
기술 문서 RAG 챗봇의 경우, voyage-01(지원 중단됨)은 더 높은 검색 품질을 보이는 것으로 나타났습니다(NDCG@10으로 측정). Voyage-2는 더 최신 버전입니다.
코드 작업의 경우, voyage-code-2는 코드 집약적 텍스트에 대해 14% 더 높은 recall 비율을 가집니다.
| Creator | Model | Embedding****Dim | Context Length | Use Case Tasks | Open Source | MTEB Score |
| VoyageAI | voyage-2 | 1024 | 4K | 고품질 RAG 챗봇 | No | 사용할 수 없음 |
| VoyageAI | voyage-code-2 | 1536 | 16K | 높은 recall 비율의 코드 완성 | No | 사용할 수 없음 |
OpenAI의 text-embedding-3-small(or large) 임베딩 모델
OpenAI의 최신 임베딩 모델은 MTEB 리더보드에서 이전 ada-002보다 더 높은 순위를 기록합니다. 이 새로운 임베딩 모델들은 더 높은 다국어 성능(MIRACL)과 더 낮은 가격도 제공합니다.
OpenAI의 블로그에 따르면, 임베딩을 생성하기 위해 압축 인식 학습이 사용되었습니다. 양자화와 같은 전통적인 차원 축소 기법은 공간을 절약하지만, 임베딩이 학습된 후 “사후적으로” 압축이 적용되기 때문에 정확도 손실이 매우 큽니다. Matryoshka Representation Learning과 같은 압축 인식 학습은 서로 다른 크기(차원)의 임베딩을 학습하며, 일부 정확도 손실이 있지만 PCA만큼 크지는 않습니다.
인상적인 점은 두 모델 모두 검색 품질을 크게 희생하지 않으면서 더 작은 임베딩을 지원한다는 것입니다. 예를 들어, 벡터 차원을 3072에서 256으로 줄이면 MTEB 점수는 65%에서 62%로만 감소합니다. 하지만 이는 메모리 요구량이 12배 낮아진다는 뜻입니다! 낮은 차원성과 관련된 정확도-비용 절충이 있기는 하지만, AI 기반 Chatbot 시대에는 빠른 응답이 답변 정확도보다 우선시되는 경우도 있습니다.
이 모델들의 몇 가지 장점:
향상된 다국어 기능.
기존의 이진 또는 곱 양자화보다 추론 오버헤드가 가장 작고 정확도 손실이 훨씬 적은 저차원 벡터.
| Creator | Model | Embedding****Dim | Context Length | Use Case Tasks | Open Source | *MTEB Score |
| OpenAI | text-embedding-3-small | 512-1536 | 8K | 실시간 다국어 텍스트 chatbots | 아니요 | 62 (512) 62 (1536) |
| OpenAI | text-embedding-3-large | 256-3072 | 8K | 실시간 다국어 텍스트 chatbots | 아니요 | 65 (3072) 62 (256) |
*HuggingFace MTEB Leaderboard, 검색 기준 내림차순 정렬, 2024년 2월 26일 접속.
아래는 새로운 임베딩 모델을 호출하는 코드 예시입니다. 전체 코드는 당사의 bootcamp github에 있습니다.
# STEP 1. CONNECT TO MILVUS
# !pip install pymilvus
from pymilvus import connections, utility
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("ZILLIZ_API_KEY")
# Connect to Zilliz cloud using endpoint URI and API key TOKEN.
CLUSTER_ENDPOINT="https://in03-xxxx.api.gcp-us-west1.zillizcloud.com:443"
connections.connect(
alias='default',
uri=CLUSTER_ENDPOINT,
token=TOKEN,
)
다음으로 OpenAI 임베딩 모델을 지정합니다.
# STEP 2. EMBEDDING MODEL.
import openai, pprint
from openai import OpenAI
# OpenAI embedding model name, `text-embedding-3-large` or `ext-embedding-3-small`.
EMBEDDING_MODEL = "text-embedding-3-small"
EMBEDDING_DIM = 512
다음으로 no-schema Milvus collection을 생성하고 index를 지정합니다.
# STEP 3. CREATE A NO-SCHEMA MILVUS COLLECTION AND USE AUTOINDEX.
from pymilvus import MilvusClient
COLLECTION_NAME = "MilvusDocs_text_embedding_3_small"
# https://milvus.io/docs/using_milvusclient.md
mc = MilvusClient(
uri=CLUSTER_ENDPOINT,
token=TOKEN)
# Check if collection already exists, if so drop it.
has = utility.has_collection(COLLECTION_NAME)
if has:
drop_result = utility.drop_collection(COLLECTION_NAME)
print(f"Successfully dropped collection: `{COLLECTION_NAME}`")
# 컬렉션을 생성합니다.
mc.create_collection(COLLECTION_NAME,
EMBEDDING_DIM,
consistency_level="Eventually",
auto_id=True,
overwrite=True)
다음으로, 폴더에 다운로드된 LangChain 기술 문서를 .html 파일로 읽습니다. 문서를 청크로 나누고 임베딩합니다. 아래 예시는 LangChain의 내장 HTML 파서를 청킹 전략으로 사용합니다. 청킹 전략은 훨씬 더 단순할 수도 있습니다.
# STEP 4. PREPARE DATA: CHUNK AND EMBED
# Read docs into LangChain.
from langchain.document_loaders import DirectoryLoader
path = "../RAG/rtdocs/pymilvus.readthedocs.io/en/latest/"
loader = DirectoryLoader(path, glob='*.html')
docs = loader.load()
from langchain.text_splitter import HTMLHeaderTextSplitter, RecursiveCharacterTextSplitter
from bs4 import BeautifulSoup
# Define the headers to split on for the HTMLHeaderTextSplitter
headers_to_split_on = [
("h1", "Header 1"),
("h2", "Header 2"),
]
# Create an instance of the HTMLHeaderTextSplitter
html_splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
# Specify chunk size and overlap.
chunk_size = 511
chunk_overlap = np.round(chunk_size * 0.10, 0)
print(f"chunk_size: {chunk_size}, chunk_overlap: {chunk_overlap}")
# Create an instance of the RecursiveCharacterTextSplitter
child_splitter = RecursiveCharacterTextSplitter(
chunk_size = chunk_size,
chunk_overlap = chunk_overlap,
length_function = len,
)
# Split the HTML text using the HTMLHeaderTextSplitter.
start_time = time.time()
html_header_splits = []
for doc in docs:
soup = BeautifulSoup(doc.page_content, 'html.parser')
splits = html_splitter.split_text(str(soup))
for split in splits:
# Add the source URL and header values to the metadata
metadata = {}
new_text = split.page_content
for header_name, metadata_header_name in headers_to_split_on:
# Handle exceptions if h1 does not exist.
try:
header_value = new_text.split("¶ ")[0].strip()[:100]
metadata[header_name] = header_value
except:
break
split.metadata = {
**metadata,
"source": doc.metadata["source"]
}
# Add the header to the text
split.page_content = split.page_content
html_header_splits.extend(splits)
# Split the documents further into smaller, recursive chunks.
chunks = child_splitter.split_documents(html_header_splits)
청크와 임베딩을 Milvus에 삽입합니다.
# STEP 5. INSERT CHUNKS AND EMBEDDINGS IN ZILLIZ.
# Convert chunks to a list of dictionaries.
chunk_list = []
for chunk in chunks:
# Generate the embeddings.
response = openai_client.embeddings.create(
input=chunk.page_content,
model=EMBEDDING_MODEL,
dimensions=EMBEDDING_DIM
)
embeddings = response.data[0].embedding
# Assemble embedding vector, original text chunk, metadata.
chunk_dict = {
'vector': embeddings,
'chunk': chunk.page_content,
'source': chunk.metadata['source'],
'h1': chunk.metadata['h1'][:50],
}
chunk_list.append(chunk_dict)
# Insert data into the Milvus collection.
insert_result = mc.insert(
COLLECTION_NAME,
data=chunk_list,
progress_bar=True)
# After the final entity is inserted, call flush to stop growing segments left in memory.
mc.flush(COLLECTION_NAME)
이제 Milvus 기술 문서 임베딩이 로드된 Milvus에 질문합니다.
# Define a sample question about your data.
SAMPLE_QUESTION = "What do the parameters for HNSW mean?"
# Embed the question using the same encoder.
response = openai_client.embeddings.create(
input=SAMPLE_QUESTION,
model=EMBEDDING_MODEL,
dimensions=EMBEDDING_DIM
)
query_embeddings = response.data[0].embedding
# Define output fields to return.
OUTPUT_FIELDS = ["h1", "h2", "source", "chunk"]
# 쿼리와 Milvus 벡터 데이터베이스를 사용하여 시맨틱 벡터 검색을 실행합니다.
start_time = time.time()
results = mc.search(
COLLECTION_NAME,
data=[query_embeddings],
output_fields=OUTPUT_FIELDS,
limit=2,
consistency_level="Eventually"
)
ChatGPT와 검색된 텍스트 컨텍스트 청크를 사용하여 답변을 생성합니다.
LLM_NAME = "gpt-3.5-turbo"
TEMPERATURE = 0.1
RANDOM_SEED = 415
# Separate all the context together by space.
contexts_combined = ' '.join(context)
SYSTEM_PROMPT = f"""Use the Context below to answer the user's question. Be clear, factual, complete, concise.
If the answer is not in the Context, say "I don't know".
Otherwise answer with fewer than 4 sentences and cite the grounding sources.
Context: contexts_combined
Answer: The answer to the question.
Grounding sources: {context_metadata[0]['source']}
"""
# Generate response using the OpenAI API.
response = openai_client.chat.completions.create(
messages=[
{"role": "system", "content": SYSTEM_PROMPT,},
{"role": "user", "content": f"question: {SAMPLE_QUESTION}",}
],
model=LLM_NAME,
temperature=TEMPERATURE,
seed=RANDOM_SEED,
)
# Print the question and answer along with grounding sources and citations.
print(f"Question: {SAMPLE_QUESTION}")
for i, choice in enumerate(response.choices, 1):
pprint.pprint(f"Answer: {choice.message.content}")
print("\n")
축소된 임베딩 dim=256을 사용한 text-embedding-3-small의 답변은, 적어도 이 RAG 예제에서는 dim=1536을 사용한 동일한 답변과 비교해 완벽합니다!
결론
이 블로그에서는 임베딩 모델이 RAG와 SBERT에서 어떻게 사용되는지, 그리고 가장 일반적인 유형의 임베딩 모델을 소개했습니다. 임베딩 모델의 MTEB 리더보드를 보여주고 사용 방법을 설명했습니다. 그런 다음 Zilliz Pipelines에 자동으로 포함된 여섯 가지 임베딩 모델을 살펴보았습니다. 서로 다른 임베딩 모델은 각기 다른 사용 사례에 가장 적합하며, 어떤 모델을 언제 선택해야 하는지 논의했습니다. 마지막으로 새로운 OpenAI 임베딩 모델을 호출하는 코드를 보여주었으며, 전체 코드는 저희 bootcamp github에 있습니다.
참고 자료
계속 읽기

How to Choose the Best Embedding Model for RAG in 2026: 10 Models Benchmarked
We benchmarked 10 embedding models on cross-modal, cross-lingual, long-document, and dimension compression tasks. See which one fits your RAG pipeline.

Zilliz Cloud Launches in AWS Australia, Expanding Global Reach to Australia and Neighboring Markets
We're thrilled to announce that Zilliz Cloud is now available in the AWS Sydney, Australia region (ap-southeast-2).

OpenAI o1: What Developers Need to Know
In this article, we will talk about the o1 series from a developer's perspective, exploring how these models can be implemented for sophisticated use cases.



