“임베딩 차원이 높을수록 무조건 좋을까?”
RAG/검색/추천에서 자주 받는 질문을, 숫자·차원·메모리·속도 중심으로 정리했습니다.
차원 수(=d) 는 한 문장을 표현하는 벡터 좌표의 길이. 예: SBERT 768차원 → 길이 768의 실수 벡터.
차원이 높으면 표현력↑(미세한 의미 구분), 하지만 메모리·연산비용·“허브니스(hubness)” 리스크↑.
차원이 낮으면 메모리·속도↑(빠름), 하지만 정보 손실로 성능↓ 가능.
실무 기준:
dense_vector.dims == 모델 차원 꼭 맞추기.snunlp/KR-SBERT-... → 768차원직관:
- d가 크면 더 많은 “축”으로 의미를 분해해 담을 수 있음 (세밀한 구분).
- d가 작으면 압축이 심해져 서로 다른 의미가 섞일 수 있음(충돌/정보손실).
≈ N × d × 4byte(float32) 로 증가.실무 감
N × d × 4 bytes (float32 가정)예) N = 100,000 문서
팁:
- float16(2 bytes)나 **양자화(quantization, PQ/OPQ/INT8)**를 쓰면 절반 이하로 줄일 수 있음(검색 정확도 하락과 트레이드오프).
- Elasticsearch
dense_vector는 내부 구현 제약이 있어 저정밀도 저장/양자화는 보통 FAISS/HNSW 라이브러리가 더 유연함.
| 상황 | 추천 차원/전략 | 이유 |
|---|---|---|
| 문서 수 적음(≤ 수만), 다국어·전문용어 많음 | 768~1024 | 의미 공간을 넉넉히, 표현 손실 방지 |
| 대규모(≥ 수백만), 응답시간 엄격 | 384~512 + 2단계 검색 | 1단계 저차원 ANN으로 후보 200개, 2단계 고차원/크로스인코더 재랭크 |
| 메모리 빡셈(EC2 t3.small 등) | 256~384 + 양자화 | 비용 절감, 충분한 품질이면 승 |
| 짧은 질의·짧은 청크(FAQ, 타이틀 중심) | 384~512 | 과도한 차원은 이득↓ 비용↑ |
| 길고 복잡한 설명/약관(한국어 문단 위주) | 768 | 문맥 풍부, 한국어 SBERT 768과 궁합 |
dense_vector의 dims는 임베딩 길이와 반드시 같아야 색인됨.
Cosine 쓰려면 L2 정규화 권장(정규화 후 dot = cosine).
대규모·지연 민감이면 knn_vector + HNSW(근사 KNN) 고려.
스코어링 예시(정규화되어 있다면):
"script": {
"source": "cosineSimilarity(params.q, 'embedding') + 1.0",
"params": {"q": [ ... 768 floats ... ]}
}
두 단계 검색:
knn_vector(저차원 or 양자화)로 Top-k 후보from sklearn.decomposition import PCA
import numpy as np
base_vecs = ... # (N, 768)
for d in [128, 256, 384, 512]:
pca = PCA(n_components=d, random_state=42)
vecs_d = pca.fit_transform(base_vecs) # (N, d)
# 1) 이 vecs_d로 인덱싱
# 2) 동일한 쿼리셋으로 Recall@10, NDCG@10 측정
# 3) 비용/성능 비교해 선택
PCA는 “기존 768을 줄였을 때”의 실험용. 실제론 저차원 모델 자체를 쓰는 편이 관리가 쉬움(예: MiniLM 384).
⛔ 모델 바꿨는데 ES dims 안 바꿈 → 색인 에러
⛔ 임베딩 정규화 없이 cosine → 스코어 뒤틀림
⛔ 메타데이터 타입 충돌(문서마다 같은 키인데 숫자/문자 섞임) → 색인 실패
⛔ Hubness 무시 → 일부 문서가 과도하게 상위에 노출
from langchain_community.embeddings import HuggingFaceEmbeddings
from elasticsearch import Elasticsearch
model_name = "snunlp/KR-SBERT-V40K-klueNLI-augSTS"
emb = HuggingFaceEmbeddings(model_name=model_name)
probe = emb.embed_query("차원 확인")
dim = len(probe) # 예: 768
es = Elasticsearch("http://localhost:9200")
index = "my_index"
if es.indices.exists(index=index):
es.indices.delete(index=index)
es.indices.create(
index=index,
mappings={
"properties": {
"text": {"type": "text", "analyzer": "nori"},
"embedding": {"type": "dense_vector", "dims": dim}
}
}
)
print("dims =", dim)
import numpy as np
def l2norm(v):
v = np.asarray(v, dtype=np.float32)
n = np.linalg.norm(v)
return (v / n).tolist() if n else v.tolist()
vecs = [l2norm(v) for v in emb.embed_documents(texts)]
# vecs를 embedding으로 색인하면, dot == cosine
[낮은 차원 / 양자화 인덱스] --Top200--> [고차원 ES/FAISS] --Topk-->
[Cross-Encoder Rerank(Optional)] --Topk-->
한국어 문단 중심 RAG + 문서 수: 수만~수십만 → 768차원 무난
문서 수: 수백만+ & 지연 엄격 → 384~512 + 2단계 검색
메모리/비용 제한 → 256~384 + 양자화 고려
결과가 마음에 안 들면, 차원만 바꾸지 말고
임베딩 차원은 “성능 vs 비용”의 손잡이입니다. 데이터 크기, 지연 요구사항, 도메인 복잡성을 동시에 보고, 작은 실험으로 엘보 지점을 찾는 게 가장 빠른 길이에요.
필요하면, 현재 쓰는 인덱스 규모/응답 시간 목표 알려주면 권장 차원 + 인덱싱 전략까지 구체적으로 짜 드릴게!