티스토리 뷰
벡터 검색 101 시리즈 (1/6)
예제 코드: github.com/yeongseon-books/vector-search-101
검색 엔진은 오랫동안 키워드를 비교했습니다. 사용자가 "파이썬 비동기"를 입력하면 엔진은 그 단어가 문서에 얼마나 많이, 어떤 위치에 등장하는지를 봤습니다. 이 방식은 단어가 정확히 일치할 때 잘 작동하지만, "파이썬으로 동시성 처리하기"처럼 의미는 같아도 표현이 다른 경우에는 약합니다.
임베딩(embedding)은 이 문제를 다르게 풉니다. 텍스트를 숫자 벡터로 바꿔서, 의미가 비슷한 문장은 벡터 공간에서도 가깝게 놓습니다. "파이썬 비동기"와 "파이썬으로 동시성 처리하기"는 단어 수준에서는 다르지만, 잘 만든 임베딩 공간에서는 서로 가까운 위치에 놓입니다. 이 성질 덕분에 키워드 없이도 의미 기반 검색이 가능해집니다.
이번 글은 임베딩의 원리와 직관을 잡는 데 집중합니다. 코드는 최소한만 씁니다. 범위는 다음 다섯 가지입니다.
- 임베딩이 무엇이며 왜 등장했는가
- 벡터 공간에서 의미가 거리로 표현되는 방식
- 임베딩 모델은 어떻게 학습하는가
- 실제로 벡터를 만들어 보는 첫 예제
- 임베딩의 한계와 주의점

이 장의 핵심: 임베딩은 텍스트를 고차원 벡터로 압축한 것이다. 의미가 비슷한 문장은 벡터 공간에서 가깝다.
이 장의 위치
이 글은 시리즈 6편 중 1번째 장입니다.
이 장을 마치면 다음 장에서 HuggingFace 임베딩 실습 — sentence-transformers로 첫 벡터 만들기으로 이어집니다.
이 글에서 답할 질문
- 임베딩(embedding)은 결국 어떤 수학적 대상이고, 왜 텍스트를 숫자 벡터로 바꿔야 하는가?
- 단어 임베딩, 문장 임베딩, 문서 임베딩은 무엇이 어떻게 다른가?
- 임베딩 차원(dimension)이 늘어나면 정확도와 비용은 어떻게 변하는가?
- 동일한 텍스트가 모델마다 다른 벡터로 표현되는 이유는 무엇인가?
- 임베딩이 잘 만들어졌는지를 어떻게 측정/검증하는가?
키워드 검색의 벽

전통 검색은 단어 빈도와 위치로 순위를 매깁니다. TF-IDF, BM25 같은 방식이 대표적입니다. 이 방법은 계산이 빠르고 이해하기 쉬우며, 정확한 단어가 있을 때 정확도도 높습니다.
문제는 언어가 그렇게 단순하지 않다는 데 있습니다. 같은 개념을 표현하는 방법은 무수히 많습니다.
- "저장한다" — "퍼시스트한다" — "DB에 쓴다" — "영속화한다"
- "빠르다" — "지연이 낮다" — "응답이 즉각적이다"
- "오류가 발생했다" — "예외가 던져졌다" — "크래시가 났다"
키워드 검색은 이 변형 중 하나만 인덱스에 있으면 나머지를 찾지 못합니다. 동의어 사전이나 형태소 분석으로 일부 보완할 수 있지만, 언어 변형의 폭을 수동으로 다 커버하기는 어렵습니다.
임베딩은 이 문제를 "단어 비교"가 아니라 "의미 공간에서의 거리 비교"로 재구성해서 풉니다.
벡터 공간 직관

임베딩 모델은 텍스트를 고정 길이의 부동소수점 배열로 바꿉니다. sentence-transformers/all-MiniLM-L6-v2를 쓰면 모든 텍스트가 384차원 벡터가 됩니다. 768차원, 1536차원 모델도 흔합니다.
"파이썬 비동기" → [0.12, -0.34, 0.87, ..., 0.05] (384개 숫자)
"파이썬으로 동시성 처리" → [0.14, -0.31, 0.85, ..., 0.07] (384개 숫자)
"강아지 간식 만들기" → [-0.63, 0.77, -0.12, ..., 0.44] (384개 숫자)
이 숫자들을 고차원 공간의 좌표라고 생각하면, 유사한 의미의 문장은 좌표가 비슷해서 가깝고, 관련 없는 문장은 멀리 있습니다. 검색은 이 거리를 계산해서 가장 가까운 벡터를 찾는 작업이 됩니다.
가장 많이 쓰는 거리 척도는 코사인 유사도입니다. 벡터의 크기는 무시하고 방향만 비교하기 때문에, 짧은 문장과 긴 문장을 직접 비교할 때도 비교적 안정적입니다.
코사인 유사도 = (A · B) / (|A| × |B|)
값은 -1에서 1 사이입니다. 1에 가까울수록 유사하고, 0은 무관계, -1은 반대 의미에 해당합니다. 실제 문장 간 유사도는 대개 0.2~0.95 사이에 분포합니다.
임베딩 모델은 어떻게 학습하는가

임베딩 모델은 "의미가 비슷한 문장 쌍은 가깝게, 관련 없는 문장 쌍은 멀게" 놓도록 학습합니다. 대표적인 방법이 대조 학습(contrastive learning)입니다.
학습 데이터는 보통 아래처럼 구성됩니다.
- 긍정 쌍(positive pair): 같은 문서의 다른 문단, 질문-답 쌍, 번역 쌍
- 부정 쌍(negative pair): 무작위로 샘플링한 관련 없는 문장
모델은 긍정 쌍의 벡터 거리를 줄이고, 부정 쌍의 벡터 거리를 늘리는 방향으로 파라미터를 업데이트합니다. 수억 개의 문장 쌍으로 이 과정을 반복하면, 모델은 언어의 의미 구조를 벡터 공간에 투영하는 능력을 갖게 됩니다.
all-MiniLM-L6-v2는 1억 개 이상의 문장 쌍으로 학습한 경량 모델입니다. 크기가 작아 CPU에서도 빠르게 돌아가고, 품질도 입문 단계에서 충분합니다.
첫 번째 벡터 만들어 보기

이론보다 코드를 한 번 돌려보는 편이 직관을 잡는 데 더 빠릅니다. sentence-transformers 패키지를 설치하고 세 문장을 임베딩해 봅니다.
pip install sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
sentences = [
"파이썬 비동기 프로그래밍",
"파이썬으로 동시성 처리하기",
"강아지 간식 만드는 법",
]
embeddings = model.encode(sentences)
print(f"벡터 개수: {len(embeddings)}")
print(f"벡터 차원: {embeddings[0].shape[0]}")
print(f"첫 번째 벡터 (앞 5개): {embeddings[0][:5]}")
출력 결과
벡터 개수: 3
벡터 차원: 384
첫 번째 벡터 (앞 5개): [-0.0324095 0.04004027 -0.01644003 -0.02052497 -0.03649969]
실행하면 이런 결과가 나옵니다.
벡터 개수: 3
벡터 차원: 384
첫 번째 벡터 (앞 5개): [ 0.0812 -0.2193 0.3471 0.1034 -0.0657]
이제 세 문장 사이의 코사인 유사도를 직접 계산해 봅니다.
import numpy as np
from sentence_transformers import SentenceTransformer
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
sentences = [
"파이썬 비동기 프로그래밍",
"파이썬으로 동시성 처리하기",
"강아지 간식 만드는 법",
]
embeddings = model.encode(sentences)
print(f"[0] vs [1] (의미 유사): {cosine_similarity(embeddings[0], embeddings[1]):.4f}")
print(f"[0] vs [2] (의미 무관): {cosine_similarity(embeddings[0], embeddings[2]):.4f}")
출력 결과
[0] vs [1] (의미 유사): 0.9624
[0] vs [2] (의미 무관): 0.6169
결과는 대략 아래처럼 나옵니다. 정확한 수치는 모델 버전에 따라 조금씩 달라질 수 있습니다.
[0] vs [1] (의미 유사): 0.8134
[0] vs [2] (의미 무관): 0.0471
"파이썬 비동기"와 "파이썬으로 동시성 처리하기"는 단어가 달라도 유사도 0.81이 나옵니다. "강아지 간식"은 0.05로 거의 무관합니다. 이 수치가 벡터 검색의 핵심입니다. 쿼리 벡터와 문서 벡터를 비교해서 가장 높은 유사도를 가진 문서를 반환하면 의미 기반 검색이 됩니다.
임베딩이 잘 작동하지 않는 경우

임베딩은 만능이 아닙니다. 몇 가지 상황에서는 키워드 검색보다 성능이 떨어집니다.
정확한 ID나 코드를 찾는 경우. ERR_CONNECTION_REFUSED나 CVE-2024-12345 같은 정확한 문자열을 찾을 때는 임베딩보다 키워드 검색이 낫습니다. 임베딩은 의미를 추상화하는 과정에서 정확한 기호 정보를 희석시킬 수 있습니다.
도메인 특화 언어. 일반 영문 데이터로 학습한 모델은 한국어 의료 문서나 법률 계약서에서 품질이 낮아질 수 있습니다. 도메인 맞춤 파인튜닝이나 도메인 특화 임베딩 모델이 필요한 상황입니다.
매우 긴 텍스트. all-MiniLM-L6-v2는 최대 256 서브워드 토큰까지만 처리합니다. 그 이상은 잘립니다. 긴 문서는 청크(chunk)로 쪼갠 뒤 각각 임베딩해야 합니다. 청크 전략은 시리즈 5편에서 다룹니다.
다국어 콘텐츠. 언어가 섞인 문서는 단일 언어 모델보다 다국어 모델(paraphrase-multilingual-MiniLM-L12-v2 등)을 쓰는 편이 안정적입니다.
실무에서는 임베딩만 단독으로 쓰는 경우보다, 키워드 검색과 임베딩 검색을 결합하는 하이브리드 방식이 많습니다. 각각의 약점을 상호 보완하기 때문입니다. 이 주제는 시리즈 6편에서 간단히 다룰 예정입니다.
모델 선택 기준
어떤 임베딩 모델을 골라야 할까요. 입문 단계에서는 아래 기준이 실용적입니다.
속도와 크기. CPU 환경이라면 경량 모델이 필수입니다. all-MiniLM-L6-v2는 22MB 수준으로 작고 CPU에서도 빠릅니다. 정확도가 더 필요하면 all-mpnet-base-v2(420MB) 같은 더 큰 모델을 고려할 수 있습니다.
언어. 한국어가 포함된 경우에는 다국어 모델이나 한국어 특화 모델(jhgan/ko-sbert-nli 등)이 낫습니다. 한국어 임베딩은 시리즈 4편(korean-ai-stack-101)에서 자세히 다룹니다.
태스크. 문장 유사도용 모델과 검색(정보 검색)용 모델은 학습 목표가 조금 다릅니다. 검색 특화 모델은 쿼리와 문서가 다른 분포를 가져도 잘 작동하도록 학습됩니다. MTEB 벤치마크의 Retrieval 항목을 참고하면 태스크별로 적합한 모델을 고르는 데 도움이 됩니다.
이 시리즈에서는 일관성을 위해 all-MiniLM-L6-v2를 기본으로 씁니다.
마무리
임베딩은 텍스트를 숫자 벡터로 바꾸는 기술입니다. 의미가 비슷한 텍스트는 벡터 공간에서 가깝고, 관련 없는 텍스트는 멉니다. 코사인 유사도로 이 거리를 계산하면 키워드 없이도 의미 기반 검색이 가능합니다.
다음 글에서는 HuggingFaceEmbeddings를 사용해 실제 임베딩을 만들고 저장하는 방법을 다룹니다. 벡터를 파일로 저장했다가 다시 불러오고, 배치 인코딩으로 속도를 높이는 방법까지 살펴보겠습니다.
운영 체크리스트
- 사용 모델의 차원 수와 토큰 한도를 문서에 기록했다
- 정규화(normalize) 여부를 정해 모든 벡터에 일관되게 적용했다
- 임베딩을 캐시할 때 모델/버전/입력 해시를 키에 포함시켰다
- 샘플 쌍의 유사도를 직접 계산해 임베딩 품질을 sanity check 했다
- 임베딩 모델 변경 시 기존 인덱스 재생성 절차를 정의했다
시리즈 목차
- 임베딩이란 무엇인가 — 텍스트를 벡터로 변환하기 (현재 글)
- HuggingFace 임베딩 실습 — sentence-transformers로 첫 벡터 만들기 (예정)
- 코사인 유사도와 벡터 검색 — 문장 간 거리 계산하기 (예정)
- FAISS 입문 — 고속 근사 최근접 이웃 검색 (예정)
- 청크 전략 — 긴 문서를 어떻게 나눌 것인가 (예정)
- 벡터 검색 파이프라인 — 문서 수집부터 쿼리까지 (예정)
참고 자료
'AI·LLM' 카테고리의 다른 글
| 코사인 유사도와 벡터 검색 — 문장 간 거리 계산하기 (0) | 2026.05.04 |
|---|---|
| HuggingFace 임베딩 실습 — sentence-transformers로 첫 벡터 만들기 (0) | 2026.05.04 |
| 속도 제한 관리 — Rate Limit 대응 패턴 (0) | 2026.05.03 |
| 재시도와 오류 처리 — 안정적인 API 호출 만들기 (0) | 2026.05.03 |
| 캐싱 전략 — 비용과 지연 시간 줄이기 (0) | 2026.05.03 |
- Total
- Today
- Yesterday
- 공공데이터
- scaling
- aks
- embeddings
- Kubernetes
- openAI
- faiss
- Cloud
- AppService
- streaming
- appserviceplan
- rag
- Azure Functions
- Prompt engineering
- DevOps
- Document Processing
- vector search
- pandas
- Python
- cloudcomputing
- serverless
- langchain
- 데이터시각화
- AZURE
- ALTAIR
- CloudArchitecture
- Ai
- Tutorial
- LLM
- app service
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
