목차
임베딩 모델로 데이터 의미 압축하기
질문: “환불은 언제까지 가능한가요?”
문서: “구매 후 7일 이내에 결제를 취소할 수 있습니다.”
두 문장에 공통 단어가 거의 없어도 같은 주제를 다룬다는 사실을 찾아야 한다. 임베딩(Embedding)은 문장이나 문서를 고정 길이 숫자 벡터로 바꾸어 이런 검색을 가능하게 한다. 여기서 압축은 원문을 복원할 수 있는 ZIP 압축이 아니다. 검색에 유용한 의미 정보를 제한된 차원에 담는다는 뜻이다.
flowchart LR A[질문] --> B[질문 임베딩] C[문서 1] --> D[문서 임베딩] E[문서 2] --> F[문서 임베딩] B --> G[유사도 계산] D --> G F --> G G --> H[가까운 문서 순위]
토큰 임베딩과 문장 임베딩
2장에서 본 nn.Embedding은 토큰 ID를 학습 가능한 벡터로 바꾼다. 검색에서는 문장 전체를 비교해야 하므로 여러 토큰의 정보를 하나의 벡터로 모으는 문장 임베딩 모델이 필요하다. 토큰 벡터를 단순히 평균 낼 수도 있지만, 검색용으로 학습된 모델은 관련 문장끼리 가까워지도록 표현을 조정한다.
예를 들어 다음 세 문서가 있다고 하자.
| ID | 문서 |
|---|---|
A | 구매 후 7일 안에 결제를 취소할 수 있습니다. |
B | 배송지 변경은 출고 전까지 가능합니다. |
C | 환불 금액은 원래 결제 수단으로 지급됩니다. |
질문 “취소 기한이 궁금해요”의 정답은 A다. C에도 환불과 관련된 말이 있지만 기한을 답하지 않는다. 따라서 벡터 거리만 보는 검색이라도, 어떤 문서가 정답인지 평가할 때는 질문의 의도까지 확인해야 한다.
코사인 유사도로 비교하기
벡터 a, b의 코사인 유사도는 a·b / (||a|| ||b||)다. 방향이 비슷할수록 값이 커진다. 벡터를 미리 정규화했다면 내적만으로 같은 순위를 얻을 수 있다. 그러나 숫자 자체가 정답 확률은 아니다. 다른 모델, 문서 집합, 질문 분포에서 나온 0.8을 같은 기준으로 해석하면 안 된다.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
documents = [
"구매 후 7일 안에 결제를 취소할 수 있습니다.",
"배송지 변경은 출고 전까지 가능합니다.",
"환불 금액은 원래 결제 수단으로 지급됩니다.",
]
question = "취소 기한이 궁금해요"
document_vectors = model.encode_document(documents, normalize_embeddings=True)
question_vector = model.encode_query(question, normalize_embeddings=True)
scores = document_vectors @ question_vector
for index in scores.argsort()[::-1]:
print(index, round(float(scores[index]), 3), documents[index])
encode_query와 encode_document는 질문과 문서의 역할을 구분해 인코딩한다. 모델에 역할별 프롬프트가 없다면 일반 encode와 같은 결과가 나올 수도 있다. 위 순위는 실행해서 보장한 결과가 아니라 확인할 대상이다. 한국어 데이터에 맞는 모델인지, 첫 번째 문서가 실제로 1위인지 직접 평가해야 한다. Sentence Transformers의 의미 검색 문서가 이 API와 비대칭 검색을 설명한다.
문서를 어떻게 나눌까?
길고 서로 다른 주제가 섞인 문서 하나를 한 벡터로 만들면 원하는 문장이 묻힌다. 문서를 청크(chunk)로 나눌 때는 문단, 제목, 표의 경계를 보존한다.
- 원본에 문서 ID, 제목, 갱신 시각, 접근 권한을 붙인다.
- 제목과 관련 문단을 묶어 청크를 만든다. 문장 중간에서 자르지 않는다.
- 청크에
document_id,chunk_id, 위치를 기록한다. - 문서와 질문에 같은 임베딩 모델 버전을 적용한다.
- 검색 결과에서 원문과 출처를 다시 찾아 제시한다.
너무 큰 청크는 여러 주제를 섞고, 너무 작은 청크는 답을 이해하는 데 필요한 앞뒤 문맥을 잃는다. 고정된 “정답 크기” 대신 실제 질문으로 청크 크기와 겹침을 비교한다. 표의 열 이름과 값이 서로 다른 청크로 갈라지면 검색되더라도 답이 틀릴 수 있다.
무엇으로 성능을 확인할까?
질문마다 관련 문서 ID를 붙인 평가 집합을 만든다. Recall@5는 정답 문서가 상위 5개 안에 있는 질문의 비율이고, MRR@10은 정답이 높은 순위에 있을수록 큰 점수를 준다. 예를 들어 정답이 첫 번째면 역순위는 1, 네 번째면 1/4다.
| 현상 | 확인할 지점 |
|---|---|
| 정확한 상품 코드가 검색되지 않음 | 숫자·약어를 잘 다루는지, 키워드 검색을 함께 쓸지 |
| 비슷한 문서는 나오지만 답이 없음 | 청크 경계, 질문과 문서의 표현 차이 |
| 검색 결과가 오래됨 | 원본 갱신과 임베딩 재생성 흐름 |
| 서로 다른 고객의 문서가 섞임 | 벡터 검색 전후의 접근 권한 필터 |
임베딩은 관련 후보를 찾는 단계다. 검색된 문서가 질문에 답하는지 확인하는 재정렬과, 답변에 근거를 표시하는 RAG 단계가 이어진다. 다음 장에서는 이 후보 검색을 자신의 데이터에 맞게 개선한다.
작은 벡터로 점수의 뜻 확인하기
위의 검색 모델을 실행하면 모델 버전에 따라 숫자가 달라질 수 있다. 코사인 유사도의 해석을 먼저 단순한 2차원 벡터로 확인해 보자. 질문 벡터가 [1, 0]이고 문서 A가 [0.8, 0.6], 문서 B가 [0, 1]이라면 둘 다 길이가 1이다. 질문과 A의 내적은 0.8, 질문과 B의 내적은 0이다. 따라서 A를 먼저 검색한다. 여기서 0.8은 질문에 답할 확률 80%가 아니라 두 벡터 방향이 가까운 정도다. 질문과 문서가 실제로 같은 주제를 다루는지는 평가 데이터가 말해 준다.
import numpy as np
question = np.array([1.0, 0.0])
documents = np.array([[0.8, 0.6], [0.0, 1.0]])
scores = documents @ question
print(scores.tolist()) # [0.8, 0.0]
실제 임베딩 모델의 벡터는 이보다 차원이 높고 각 좌표를 사람의 개념 하나로 해석할 수 없다. 정규화 여부가 섞이면 내적은 코사인 유사도와 같지 않다. 벡터 데이터베이스의 거리 연산자를 고를 때도 인코딩 시 정규화했는지, 색인이 같은 거리를 사용하고 있는지 확인해야 한다.
청크 경계가 답을 갈라놓는 사례
문서에 환불 신청: 결제일부터 7일이라는 표가 있고 제목은 앞 청크, 7일은 다음 청크로 갈렸다고 하자. 질문 “환불 신청 기한은?”에 숫자 청크만 검색되면 모델은 그 7일이 무엇을 뜻하는지 알기 어렵다. 제목 청크만 검색되면 기한 자체를 못 찾는다. 문단별 분할이 표나 목록의 관계를 망가뜨릴 때는 행 헤더를 각 청크에 반복하거나 표 전체를 하나의 단위로 보관한다.
반대로 한 청크에 배송·환불·교환 정책을 모두 넣으면 검색 모델이 세 주제의 평균적인 표현을 만든다. 환불 질문에 배송 정보가 섞이고, 생성 모델에는 필요 없는 토큰을 많이 보낸다. 그래서 청크 크기를 토큰 수 하나로만 정하지 않고 질문 하나에 답하기 충분한 단위로 결정한다. 경계가 애매한 경우에는 원본 문서의 앞뒤 문단을 검색 후 추가로 붙이되, 그 문단도 사용자 권한을 통과해야 한다.
검색 평가가 어떻게 의사결정으로 이어지는가
가상의 질문 50개 중 40개에는 정답 청크가 있다. Recall@5 = 36/40 = 90%라면 4개 질문에서는 생성 모델이 정답 근거를 볼 수 없다. 이 네 건은 생성 프롬프트를 다듬어도 해결되지 않는다. 정답이 없는 나머지 10개에서는 관련 없어 보이는 문서를 억지로 반환하고 단정적인 답을 만드는지 따로 본다. “검색 결과가 5개 나왔다”는 사실과 “답할 근거가 있다”는 판단은 다르다.
정확한 상품 코드나 계약 번호에 약한 사례가 많으면 키워드 검색을 함께 사용해 후보를 합친다. 의미 검색 모델만 재학습하는 것보다 간단한 키워드 인덱스가 더 나을 수 있다. 같은 평가셋에서 Recall@5, 결과 재정렬 뒤 첫 정답 순위, p95 검색 시간, 색인 크기를 비교해 채택한다. 다음 장의 미세 튜닝은 이 과정을 거쳐 도메인 의미가 실제 병목으로 확인됐을 때 시도한다.