개념
임베딩이란 무엇인가?
임베딩(Embedding)은 텍스트의 의미를 나타내는 부동소수점 숫자의 조밀한 벡터(Dense Vector)입니다. 여기서 "조밀한"이 중요합니다. 대부분의 차원이 0인 희소 표현(sparse representation; bag-of-words, TF-IDF)과 달리, 모든 차원이 정보를 담습니다.
"The cat sat on the mat"는 [0.023, -0.041, 0.087, ..., 0.012] 같은 벡터가 됩니다. 모델에 따라 768개에서 3072개의 숫자 목록입니다. 이 숫자는 의미를 인코딩합니다. 직접 눈으로 해석하지는 않습니다. 비교합니다.
Word2Vec의 돌파구
2013년 Google의 Tomas Mikolov와 동료들은 Word2Vec을 발표했습니다. 핵심 통찰은 이렇습니다. 이웃 단어로부터 단어를 예측하거나 단어로부터 이웃 단어를 예측하도록 신경망을 학습하면, 은닉층 가중치가 의미 있는 벡터 표현이 됩니다.
유명한 결과는 다음입니다.
king - man + woman = queen
단어 임베딩에 대한 벡터 산술(Vector Arithmetic)은 의미 관계를 포착합니다. "man"에서 "woman"으로 가는 방향은 대략 "king"에서 "queen"으로 가는 방향과 같습니다. 이 순간 학계는 기하학이 의미를 인코딩할 수 있음을 깨달았습니다.
Word2Vec은 300차원 벡터를 만들었습니다. 각 단어는 문맥과 상관없이 하나의 벡터를 받았습니다. "river bank"의 "bank"와 "bank account"의 "bank"가 같은 임베딩을 가졌습니다. 이 한계가 다음 10년의 연구를 이끌었습니다.
단어에서 문장으로
단어 임베딩은 단일 토큰을 표현합니다. 프로덕션 시스템은 전체 문장, 문단, 문서를 임베딩해야 합니다. 네 가지 접근이 등장했습니다.
평균내기(Averaging): 문장 안의 모든 단어 벡터 평균을 냅니다. 저렴하고 손실이 있지만, 짧은 텍스트에서는 의외로 괜찮습니다. 단어 순서를 완전히 잃습니다. "dog bites man"과 "man bites dog"가 같은 임베딩을 얻습니다.
CLS 토큰(CLS Token): 트랜스포머(Transformer) 모델(BERT, 2018)은 전체 입력을 대표하는 특수 [CLS] 토큰 임베딩을 출력합니다. 평균보다는 낫지만, [CLS] 토큰은 유사도용이 아니라 다음 문장 예측(next-sentence prediction)을 위해 학습되었습니다.
대조 학습(Contrastive Learning): 비슷한 쌍은 가까이 모으고, 다른 쌍은 멀리 밀어내도록 모델을 명시적으로 학습합니다. Sentence-BERT(Reimers & Gurevych, 2019)가 이 접근을 사용했고 현대 임베딩 모델의 기반이 되었습니다. "How do I reset my password?"와 "I need to change my password"는 거의 같은 벡터를 가져야 한다고 학습합니다.
지시 튜닝 임베딩(Instruction-Tuned Embeddings): 최신 접근입니다. E5, GTE 같은 모델은 "search_query:", "search_document:" 같은 작업 접두어(prefix)를 받아 어떤 종류의 임베딩을 만들지 알려줍니다. 하나의 모델이 여러 작업을 처리할 수 있게 합니다.
graph LR
subgraph "2013: Word2Vec"
W1["king"] --> V1["[0.2, -0.1, ...]"]
W2["queen"] --> V2["[0.3, -0.2, ...]"]
end
subgraph "2019: Sentence-BERT"
S1["How do I reset my password?"] --> E1["[0.04, 0.12, ...]"]
S2["I need to change my password"] --> E2["[0.05, 0.11, ...]"]
end
subgraph "2024: Instruction-Tuned"
I1["search_query: password reset"] --> T1["[0.08, 0.09, ...]"]
I2["search_document: To reset your password, click..."] --> T2["[0.07, 0.10, ...]"]
end
현대 임베딩 모델
프로덕션급 선택지는 몇 가지로 좁혀졌습니다. 아래 MTEB 점수는 2026년 초 기준 MTEB v2입니다.
| 모델 | 제공자 | 차원 | MTEB | 컨텍스트 | 1M 토큰당 비용 |
|---|
| Gemini Embedding 2 | Google | 3072 (Matryoshka) | 67.7 (retrieval) | 8192 | $0.15 |
| embed-v4 | Cohere | 1024 (Matryoshka) | 65.2 | 128K | $0.12 |
| voyage-4 | Voyage AI | 1024/2048 (Matryoshka) | 66.8 | 32K | $0.12 |
| text-embedding-3-large | OpenAI | 3072 (Matryoshka) | 64.6 | 8192 | $0.13 |
| text-embedding-3-small | OpenAI | 1536 (Matryoshka) | 62.3 | 8192 | $0.02 |
| BGE-M3 | BAAI | 1024 (dense+sparse+ColBERT) | 63.0 multilingual | 8192 | Open-weight |
| Qwen3-Embedding | Alibaba | 4096 (Matryoshka) | 66.9 | 32K | Open-weight |
| Nomic-embed-v2 | Nomic | 768 (Matryoshka) | 63.1 | 8192 | Open-weight |
MTEB(Massive Text Embedding Benchmark) v2는 검색(retrieval), 분류(classification), 군집화(clustering), 재순위화(reranking), 요약(summarization) 등 100개 이상의 작업을 포함합니다. 점수는 높을수록 좋습니다. 2026년에는 오픈 웨이트(open-weight) 모델(Qwen3-Embedding, BGE-M3)이 대부분의 축에서 폐쇄형 호스팅(hosted) 모델과 같거나 더 좋은 성능을 보입니다. Gemini Embedding 2는 순수 검색 작업에서 앞서고, Voyage와 Cohere는 특정 도메인(금융, 법률, 코드)에서 강합니다. 실제 도입 전에는 항상 자신의 질의(query)로 벤치마크해야 합니다.
유사도 측정 방식(Similarity Metrics)
두 임베딩 벡터가 주어졌을 때, 얼마나 비슷한지 측정하는 방법은 세 가지가 있습니다.
코사인 유사도(Cosine Similarity): 두 벡터 사이 각도의 코사인입니다. -1(반대 방향)에서 1(같은 방향)까지의 값을 가집니다. 크기는 무시합니다. 10단어 문장과 500단어 문서도 같은 방향이면 1.0을 받을 수 있습니다. 사용 사례의 90%에서 기본값으로 사용합니다.
cosine_sim(a, b) = dot(a, b) / (||a|| * ||b||)
내적(Dot Product): 두 벡터의 원시 내적(raw inner product) 값입니다. 벡터가 정규화되어 단위 길이(unit length)라면 코사인 유사도와 동일합니다. 계산이 더 빠릅니다. OpenAI 임베딩은 정규화되어 있으므로 내적과 코사인은 같은 순위를 줍니다.
dot(a, b) = sum(a_i * b_i)
유클리드 거리(Euclidean/L2 Distance): 벡터 공간에서의 직선 거리입니다. 작을수록 더 비슷합니다. 크기 차이에 민감합니다. 방향만이 아니라 공간 안의 절대 위치가 중요할 때 사용합니다.
L2(a, b) = sqrt(sum((a_i - b_i)^2))
언제 무엇을 쓸지는 다음과 같습니다.
| 측정 방식 | 사용할 때 | 피할 때 |
|---|
| 코사인 유사도(Cosine similarity) | 길이가 다른 텍스트 비교, 대부분의 검색(retrieval) 작업 | 크기가 정보를 담을 때 |
| 내적(Dot product) | 임베딩이 이미 정규화되어 있고 속도가 중요할 때 | 벡터 크기가 다양할 때 |
| 유클리드 거리(Euclidean distance) | 군집화, 공간적 최근접 이웃(nearest-neighbor) 문제 | 길이가 크게 다른 문서 비교 |
벡터 데이터베이스(Vector Database)와 HNSW
브루트포스(Brute-Force) 유사도 검색은 질의를 저장된 모든 벡터와 비교합니다. 1536차원 벡터 100만 개라면 질의 하나당 15억 번의 곱-덧셈(multiply-add) 연산이 필요합니다. 너무 느립니다.
벡터 데이터베이스(Vector Database)는 근사 최근접 이웃(Approximate Nearest Neighbor; ANN) 알고리즘으로 이를 해결합니다. 지배적인 알고리즘은 HNSW(Hierarchical Navigable Small World)입니다.
- 벡터의 다층 그래프를 만듭니다.
- 상위 계층(layer)은 희소합니다. 멀리 떨어진 군집(cluster) 사이의 장거리 연결(long-range connection)을 둡니다.
- 하위 계층은 조밀합니다. 가까운 벡터 사이의 세밀한 연결(fine-grained connection)을 둡니다.
- 검색은 상위 계층에서 시작해 탐욕적으로(greedy) 내려가며 정밀도를 높입니다.
- O(n) 대신 O(log n) 시간에 근사 top-k 결과를 반환합니다.
HNSW는 작은 정확도 손실(보통 95-99% 재현율)을 엄청난 속도 개선과 맞바꿉니다. 벡터 1,000만 개에서 브루트포스는 초 단위가 걸리지만 HNSW는 밀리초 단위입니다.
graph TD
subgraph "HNSW Layers"
L2["Layer 2 (sparse)"] -->|"long jumps"| L1["Layer 1 (medium)"]
L1 -->|"shorter jumps"| L0["Layer 0 (dense, all vectors)"]
end
Q["Query vector"] -->|"enter at top"| L2
L0 -->|"nearest neighbors"| R["Top-k results"]
프로덕션 선택지는 다음과 같습니다.
| 데이터베이스 | 유형 | 적합한 경우 | 최대 규모 |
|---|
| Pinecone | 관리형 SaaS | 무운영(Zero-ops) 프로덕션 | 수십억 |
| Weaviate | 오픈 소스 | 자체 호스팅, 하이브리드 검색 | 1억+ |
| Qdrant | 오픈 소스 | 고성능, 필터링 | 1억+ |
| ChromaDB | 임베디드 | 프로토타이핑, 로컬 개발 | 100만 |
| pgvector | Postgres 확장 | 이미 Postgres를 사용 중일 때 | 1,000만 |
| FAISS | 라이브러리 | 인-프로세스(in-process), 연구용 | 10억+ |
청킹 전략(Chunking Strategies)
문서는 단일 벡터로 임베딩하기에 너무 깁니다. 50페이지 PDF는 수십 개 주제를 다룹니다. 전체 임베딩은 모든 것의 평균이 되어 어떤 구체적 내용과도 잘 맞지 않습니다. 문서를 청크로 나누고 각 청크를 임베딩해야 합니다.
고정 크기 청킹(Fixed-Size Chunking): N개 토큰마다 자르고 M개 토큰을 겹치게 둡니다. 단순하고 예측 가능합니다. 문서에 뚜렷한 구조가 없을 때 잘 작동합니다. 512 토큰 청크와 50 토큰 겹침(overlap)이라면 첫 번째 청크는 토큰 0511, 두 번째 청크는 토큰 462973이 됩니다.
문장 기반 청킹(Sentence-Based Chunking): 문장 경계에서 자르고, 토큰 한도(token limit)에 도달할 때까지 문장을 묶습니다. 각 청크는 최소한 하나의 완전한 문장입니다. 하나의 생각을 중간에 자르지 않으므로 고정 크기보다 좋습니다.
재귀 청킹(Recursive Chunking): 가장 큰 경계부터 시도합니다. 먼저 섹션 헤더(section header), 너무 크면 문단(paragraph), 그다음 문장(sentence), 마지막으로 글자 수 한도(character limit) 순서입니다. LangChain의 RecursiveCharacterTextSplitter가 이 방식이며, 혼합 형식 말뭉치(corpus)에서 잘 작동합니다.
의미 기반 청킹(Semantic Chunking): 각 문장을 임베딩한 다음, 임베딩이 비슷한 연속 문장끼리 묶습니다. 임베딩 유사도가 임계값(threshold) 아래로 떨어지면 새 청크를 시작합니다. 비용이 크지만, 가장 일관된 청크를 만듭니다.
| 전략 | 복잡도 | 품질 | 가장 적합한 경우 |
|---|
| 고정 크기(Fixed-size) | 낮음 | 보통 | 비구조 텍스트, 로그 |
| 문장 기반(Sentence-based) | 낮음 | 좋음 | 기사, 이메일 |
| 재귀(Recursive) | 중간 | 좋음 | Markdown, HTML, 혼합 형식 문서 |
| 의미 기반(Semantic) | 높음 | 최고 | 검색 품질이 매우 중요한 경우 |
대부분의 시스템에서 좋은 시작점은 256~512 토큰 청크와 50 토큰 겹침입니다.
바이 인코더(Bi-Encoder)와 크로스 인코더(Cross-Encoder)
바이 인코더(Bi-Encoder)는 질의와 문서를 독립적으로 임베딩한 뒤 벡터를 비교합니다. 빠릅니다. 질의를 한 번만 임베딩하고 사전에 계산된 문서 임베딩과 비교하면 됩니다. 검색(retrieval) 단계에서 사용하는 방식입니다.
크로스 인코더(Cross-Encoder)는 질의와 문서를 하나의 입력으로 받아 관련성 점수(relevance score)를 출력합니다. 느립니다. 각 질의-문서 쌍을 전체 모델에 통과시켜야 합니다. 그러나 질의와 문서 토큰이 동시에 서로에게 어텐션(attention)을 적용할 수 있으므로 훨씬 정확합니다.
프로덕션 패턴은 바이 인코더로 상위 100개 후보를 가져오고, 크로스 인코더로 상위 10개까지 재순위화(rerank)하는 것입니다. 이것이 검색-후-재순위화(retrieve-then-rerank) 파이프라인입니다.
graph LR
Q["Query"] --> BE["Bi-Encoder: embed query"]
BE --> VS["Vector search: top 100"]
VS --> CE["Cross-Encoder: rerank"]
CE --> R["Top 10 results"]
재순위화 모델에는 Cohere Rerank 3.5(1,000 질의당 $2), BGE-reranker-v2(무료, 오픈 소스), Jina Reranker v2(무료, 오픈 소스)가 있습니다.
마트료시카(Matryoshka) 임베딩
전통적인 임베딩은 전부냐 전무냐(all-or-nothing) 방식입니다. 1536차원 벡터는 1536개의 부동소수점(float) 값을 사용합니다. 재학습 없이 256차원으로 자를 수 없습니다.
마트료시카 표현 학습(Matryoshka Representation Learning; Kusupati et al., 2022)이 이를 해결합니다. 모델은 첫 N개 차원이 가장 중요한 정보를 담도록 학습됩니다. 러시아 인형처럼 안쪽부터 중요한 정보가 들어갑니다. 1536차원 마트료시카 임베딩을 256차원으로 잘라도 정확도는 조금 떨어지지만 기능은 유지됩니다.
OpenAI의 text-embedding-3-small과 text-embedding-3-large는 dimensions 파라미터로 마트료시카 절단(Matryoshka truncation)을 지원합니다. 1536차원 대신 256차원을 요청하면 저장 공간은 6배 줄고 MTEB 벤치마크에서 정확도는 대략 3~5% 손실됩니다.
이진 양자화(Binary Quantization)
1536차원 임베딩을 float32로 저장하면 한 벡터당 6,144바이트를 사용합니다. 문서 1,000만 개면 벡터만 61GB입니다.
이진 양자화(Binary Quantization)는 각 부동소수점 값을 단일 비트(bit)로 바꿉니다. 양수는 1, 음수는 0이 됩니다. 저장 공간은 6,144바이트에서 192바이트로 줄어 32배 감소합니다. 유사도는 서로 다른 비트 수를 세는 해밍 거리(Hamming distance)로 계산하며, CPU는 이를 단일 명령(instruction)으로 처리할 수 있습니다.
검색 재현율(recall) 기준 정확도 손실은 약 5~10%입니다. 흔한 패턴은 수백만 벡터에 대한 1차 검색에는 이진 양자화를 사용하고, 상위 1,000개를 완전 정밀도(full-precision) 벡터로 다시 점수화하는 것입니다. 이렇게 하면 32배 적은 메모리로 완전 정밀도 정확도의 95% 이상을 얻을 수 있습니다.
직접 만들기
의미 기반 검색 엔진을 처음부터 만듭니다. 벡터 데이터베이스도, 외부 임베딩 API도 쓰지 않습니다. 수학 계산은 Python과 numpy를 사용합니다.
Step 1: 텍스트 청킹
def chunk_text(text, chunk_size=200, overlap=50):
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + chunk_size
chunk = " ".join(words[start:end])
chunks.append(chunk)
start += chunk_size - overlap
return chunks
def chunk_by_sentences(text, max_chunk_tokens=200):
sentences = text.replace("\n", " ").split(".")
sentences = [s.strip() + "." for s in sentences if s.strip()]
chunks = []
current_chunk = []
current_length = 0
for sentence in sentences:
sentence_length = len(sentence.split())
if current_length + sentence_length > max_chunk_tokens and current_chunk:
chunks.append(" ".join(current_chunk))
current_chunk = []
current_length = 0
current_chunk.append(sentence)
current_length += sentence_length
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
Step 2: 처음부터 임베딩 만들기
TF-IDF와 L2 정규화를 사용해 간단한 조밀 임베딩(dense embedding)을 구현합니다. 신경망 임베딩은 아니지만 같은 계약(contract)을 따릅니다. 텍스트가 들어오고, 고정 크기 벡터가 나오며, 비슷한 텍스트는 비슷한 벡터를 만듭니다.
import math
import numpy as np
from collections import Counter
class SimpleEmbedder:
def __init__(self):
self.vocab = []
self.idf = []
self.word_to_idx = {}
def fit(self, documents):
vocab_set = set()
for doc in documents:
vocab_set.update(doc.lower().split())
self.vocab = sorted(vocab_set)
self.word_to_idx = {w: i for i, w in enumerate(self.vocab)}
n = len(documents)
self.idf = np.zeros(len(self.vocab))
for i, word in enumerate(self.vocab):
doc_count = sum(1 for doc in documents if word in doc.lower().split())
self.idf[i] = math.log((n + 1) / (doc_count + 1)) + 1
def embed(self, text):
words = text.lower().split()
count = Counter(words)
total = len(words) if words else 1
vec = np.zeros(len(self.vocab))
for word, freq in count.items():
if word in self.word_to_idx:
tf = freq / total
vec[self.word_to_idx[word]] = tf * self.idf[self.word_to_idx[word]]
norm = np.linalg.norm(vec)
if norm > 0:
vec = vec / norm
return vec
Step 3: 유사도 함수
def cosine_similarity(a, b):
dot = np.dot(a, b)
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
if norm_a == 0 or norm_b == 0:
return 0.0
return float(dot / (norm_a * norm_b))
def dot_product(a, b):
return float(np.dot(a, b))
def euclidean_distance(a, b):
return float(np.linalg.norm(a - b))
Step 4: 브루트포스 검색을 사용하는 벡터 인덱스(Vector Index)
class VectorIndex:
def __init__(self):
self.vectors = []
self.texts = []
self.metadata = []
def add(self, vector, text, meta=None):
self.vectors.append(vector)
self.texts.append(text)
self.metadata.append(meta or {})
def search(self, query_vector, top_k=5, metric="cosine"):
scores = []
for i, vec in enumerate(self.vectors):
if metric == "cosine":
score = cosine_similarity(query_vector, vec)
elif metric == "dot":
score = dot_product(query_vector, vec)
elif metric == "euclidean":
score = -euclidean_distance(query_vector, vec)
else:
raise ValueError(f"Unknown metric: {metric}")
scores.append((i, score))
scores.sort(key=lambda x: x[1], reverse=True)
results = []
for idx, score in scores[:top_k]:
results.append({
"text": self.texts[idx],
"score": score,
"metadata": self.metadata[idx],
"index": idx
})
return results
def size(self):
return len(self.vectors)
Step 5: 의미 기반 검색 엔진
class SemanticSearchEngine:
def __init__(self, chunk_size=200, overlap=50):
self.embedder = SimpleEmbedder()
self.index = VectorIndex()
self.chunk_size = chunk_size
self.overlap = overlap
def index_documents(self, documents, source_names=None):
all_chunks = []
all_sources = []
for i, doc in enumerate(documents):
chunks = chunk_text(doc, self.chunk_size, self.overlap)
all_chunks.extend(chunks)
name = source_names[i] if source_names else f"doc_{i}"
all_sources.extend([name] * len(chunks))
self.embedder.fit(all_chunks)
for chunk, source in zip(all_chunks, all_sources):
vec = self.embedder.embed(chunk)
self.index.add(vec, chunk, {"source": source})
return len(all_chunks)
def search(self, query, top_k=5, metric="cosine"):
query_vec = self.embedder.embed(query)
return self.index.search(query_vec, top_k, metric)
def search_with_scores(self, query, top_k=5):
results = self.search(query, top_k)
return [
{
"text": r["text"][:200],
"source": r["metadata"].get("source", "unknown"),
"score": round(r["score"], 4)
}
for r in results
]
Step 6: 유사도 측정 방식 비교
def compare_metrics(engine, query, top_k=3):
results = {}
for metric in ["cosine", "dot", "euclidean"]:
hits = engine.search(query, top_k=top_k, metric=metric)
results[metric] = [
{"score": round(h["score"], 4), "preview": h["text"][:80]}
for h in hits
]
return results
사용해보기
프로덕션 임베딩 API를 사용해도 아키텍처는 동일합니다. 바뀌는 것은 임베더(embedder)뿐입니다.
from openai import OpenAI
client = OpenAI()
def openai_embed(texts, model="text-embedding-3-small", dimensions=None):
kwargs = {"model": model, "input": texts}
if dimensions:
kwargs["dimensions"] = dimensions
response = client.embeddings.create(**kwargs)
return [item.embedding for item in response.data]
OpenAI로 마트료시카 절단을 사용하는 예시입니다. 같은 모델, 더 적은 차원, 더 낮은 저장 비용입니다.
full = openai_embed(["semantic search query"], dimensions=1536)
compact = openai_embed(["semantic search query"], dimensions=256)
256차원 벡터는 저장 공간을 6배 덜 사용합니다. 문서 1,000만 개 기준 61GB가 아니라 10GB입니다. 표준 벤치마크에서 정확도 손실은 대략 3~5%입니다.
Cohere로 재순위화하는 예시는 다음과 같습니다.
import cohere
co = cohere.ClientV2()
results = co.rerank(
model="rerank-v3.5",
query="What is the refund policy?",
documents=["Full refund within 30 days...", "No refunds after 90 days..."],
top_n=3
)
API 의존성 없이 로컬 임베딩을 사용하는 예시는 다음과 같습니다.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-en-v1.5")
embeddings = model.encode(["semantic search query", "another document"])
직접 만든 VectorIndex 클래스는 이들 중 어느 방식과도 함께 작동합니다. 임베딩 함수만 바꾸고 검색 로직은 유지합니다.
산출물 만들기
이 lesson은 다음 산출물을 만듭니다.
outputs/prompt-embedding-advisor.md -- 특정 사용 사례에 맞는 임베딩 모델과 전략을 고르는 프롬프트(prompt)
outputs/skill-embedding-patterns.md -- 에이전트(agent)가 프로덕션에서 임베딩을 효과적으로 사용하도록 가르치는 스킬(skill)
연습문제
-
유사도 측정 방식 비교 (쉬움): 같은 5개 질의를 샘플 문서에 대해 코사인 유사도, 내적, 유클리드 거리로 실행하세요. 각 상위 3개 결과를 기록합니다. 어떤 질의에서 측정 방식별 결과가 갈라지나요? 왜 그런가요?
-
청크 크기 실험 (중간): 청크 크기를 50, 100, 200, 500 단어로 바꿔 샘플 문서를 인덱싱하세요. 각각 5개 질의를 실행하고 상위 1개 유사도 점수를 기록합니다. 청크 크기와 검색 품질의 관계를 그립니다. 큰 청크가 도리어 해로워지는 지점을 찾으세요.
-
마트료시카 시뮬레이션 (중간): 500차원 벡터를 만드는 SimpleEmbedder를 만드세요. 50, 100, 200, 500차원으로 잘라 검색 재현율이 어떻게 감소하는지 측정합니다. 실제 학습 기법 없이 마트료시카 동작을 시뮬레이션합니다.
-
이진 양자화 (어려움): 검색 엔진의 임베딩을 이진 벡터로 바꾸세요. 양수면 1, 음수면 0입니다. 해밍 거리 검색을 구현합니다. 상위 10개 결과를 완전 정밀도 코사인 유사도와 비교하고 겹침 비율(overlap percentage)을 측정하세요.
-
문장 기반 청킹 (중간): 고정 크기 청킹을 chunk_by_sentences로 바꿉니다. 같은 질의를 실행하고 검색 점수를 비교합니다. 문장 경계를 지키는 것이 결과를 개선하나요?
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 임베딩(Embedding) | "텍스트를 숫자로 바꾸기" | 기하적 근접성이 의미적 유사성을 인코딩하는 조밀한 벡터(dense vector) |
| Word2Vec | "원조 임베딩" | 문맥 단어(context word) 예측으로 단어 벡터(word vector)를 학습한 2013년 모델. 벡터 산술이 의미를 인코딩함을 보였습니다. |
| 코사인 유사도(Cosine Similarity) | "두 벡터가 얼마나 비슷한가" | 벡터 사이 각도의 코사인. 1은 같은 방향, 0은 직교, -1은 반대 방향입니다. |
| HNSW | "빠른 벡터 검색" | Hierarchical Navigable Small World 그래프. O(log n) 근사 최근접 이웃 검색을 가능하게 하는 다층 구조입니다. |
| 바이 인코더(Bi-encoder) | "따로 임베딩하고 빠르게 비교" | 질의와 문서를 독립적으로 벡터화해 사전 계산과 빠른 검색을 가능하게 합니다. |
| 크로스 인코더(Cross-encoder) | "느리지만 정확한 재순위화기" | 질의-문서 쌍을 전체 모델에 함께 넣어 처리합니다. 정확도는 높지만 사전 계산이 불가능합니다. |
| 마트료시카 임베딩(Matryoshka embeddings) | "자를 수 있는 벡터" | 첫 N개 차원이 가장 중요한 정보를 담도록 학습되어 가변 크기 저장(variable-size storage)이 가능한 임베딩 |
| 이진 양자화(Binary Quantization) | "1비트 임베딩" | 부동소수점 벡터를 부호 비트(sign bit)만 남긴 이진값으로 바꿔 해밍 거리 검색을 수행하고 저장 공간을 32배 줄이는 방식 |
| 청킹(Chunking) | "문서를 임베딩용으로 나누기" | 문서를 독립적으로 임베딩하고 검색할 수 있는 256~512 토큰 단위로 나누는 작업 |
| 벡터 데이터베이스(Vector Database) | "임베딩용 검색 엔진" | 벡터 저장과 대규모 근사 최근접 이웃 검색에 최적화된 데이터 저장소 |
| 대조 학습(Contrastive Learning) | "비교하며 학습하기" | 비슷한 쌍의 임베딩은 가깝게, 다른 쌍의 임베딩은 멀게 밀도록 학습하는 접근 |
| MTEB | "임베딩 벤치마크" | Massive Text Embedding Benchmark. 8개 작업 범주의 56개 데이터셋으로 임베딩 모델을 비교하는 표준입니다. |
더 읽을거리
- Mikolov et al., "Efficient Estimation of Word Representations in Vector Space" (2013) -- 왕-여왕 유추(king-queen analogy)로 임베딩 혁명을 시작한 Word2Vec 논문입니다.
- Reimers & Gurevych, "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks" (2019) -- 문장 수준 유사도를 위한 바이 인코더 학습 방법입니다. 현대 임베딩 모델의 기반입니다.
- Kusupati et al., "Matryoshka Representation Learning" (2022) -- OpenAI가 text-embedding-3에 채택한 가변 차원 임베딩(variable-dimension embedding) 기법입니다.
- Malkov & Yashunin, "Efficient and Robust Approximate Nearest Neighbor using Hierarchical Navigable Small World Graphs" (2018) -- 대부분의 프로덕션 벡터 검색 뒤에 있는 HNSW 논문입니다.
- OpenAI Embeddings Guide -- 마트료시카 차원 축소를 포함한 text-embedding-3 모델의 실무 참고 문서입니다.
- MTEB Leaderboard -- 모든 임베딩 모델을 작업과 언어별로 비교하는 실시간 벤치마크입니다.
- Muennighoff et al., "MTEB: Massive Text Embedding Benchmark" (EACL 2023) -- 리더보드가 보고하는 8개 작업 범주(task category)를 정의한 벤치마크 논문입니다. 단일 MTEB 점수를 믿기 전에 읽어볼 만합니다.
- Sentence Transformers documentation -- 바이 인코더와 크로스 인코더의 비교, 풀링 전략(pooling strategy), 수집-분할-임베딩-저장(ingest-split-embed-store) RAG 파이프라인을 설명하는 표준 참고 문서입니다.