LLM 엔지니어링7장 · 자체 임베딩과 벡터DB, 검색 품질15 / 22← 이전목차다음 →MetaContents

7장. 자체 임베딩과 벡터DB, 검색 품질

실습 2 — 사내 문서 의미 검색

가상 회사 "솔내기술"의 IT 지원 안내(it_helpdesk.md, 주제 30개)를 제목 단위로 나눠 임베딩하고, 의미 검색 함수를 만듭니다. 6장의 TF-IDF 키워드 검색과 같은 질문으로 견줍니다. 회사와 안내 내용은 모두 연습용으로 지어낸 것입니다.

1단계. 제목 단위로 나누고 임베딩하기

Python
import time
from sentence_transformers import SentenceTransformer

text = open("it_helpdesk.md", encoding="utf-8").read()
# "## 제목" 마다 나누고, 맨 앞의 문서 제목과 소개는 뺀다
chunks = ["## " + s.strip() for s in text.split("\n## ")[1:]]
print("조각 수:", len(chunks))
print(chunks[3].replace("\n", " / "))     # 줄바꿈을 / 로 바꿔 한 줄로 본다

emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
start = time.time()
# 문서 조각에는 passage: 를 붙인다. 이 임베딩은 한 번 만들어 두고 계속 쓴다
E = emb.encode(["passage: " + c for c in chunks], normalize_embeddings=True)
print("임베딩 모양:", E.shape, f"· {time.time() - start:.1f}초")
출력
조각 수: 30
## 계정 잠김 / 비밀번호를 5번 틀리면 계정이 30분 동안 잠깁니다. 급하면 IT지원센터(내선 1234)에 전화해 본인 확인 뒤 바로 풀 수 있습니다.
임베딩 모양: (30, 384) · 0.4초

2단계. 의미 검색 함수

Python
import numpy as np

def title(c):
    """조각의 첫 줄 '## 제목' 에서 제목만"""
    return c.split("\n")[0].removeprefix("## ")

def semantic_search(question, k=3):
    """질문과 코사인 유사도가 높은 조각 k 개의 (번호, 점수)"""
    q = emb.encode("query: " + question, normalize_embeddings=True)
    scores = E @ q
    top = np.argsort(-scores)[:k]
    return [(int(i), round(float(scores[i]), 3)) for i in top]

for i, s in semantic_search("로그인을 여러 번 실패해서 들어갈 수가 없어요."):
    print(s, title(chunks[i]))
출력
0.867 계정 잠김
0.864 신규 입사자
0.851 원격 지원

3단계. 키워드 검색과 견주기

Python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# 6장의 키워드 검색: 2~3 글자 조각의 TF-IDF
vec = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3))
X = vec.fit_transform(chunks)
def keyword_search(question, k=3):
    scores = cosine_similarity(vec.transform([question]), X)[0]
    return [(int(i), round(float(scores[i]), 3)) for i in np.argsort(-scores)[:k]]

for q in ["암호는 얼마나 자주 새로 정해야 하나요?", "로그인을 여러 번 실패해서 들어갈 수가 없어요.", "예전에 쓰던 암호를 다시 써도 되나요?"]:
    print("질문:", q)
    print("  키워드:", [title(chunks[i]) for i, _ in keyword_search(q)])
    print("  의미  :", [title(chunks[i]) for i, _ in semantic_search(q)])
출력
질문: 암호는 얼마나 자주 새로 정해야 하나요?
  키워드: ['모니터 추가', 'VPN 접속', '개인 기기']
  의미  : ['비밀번호 바꾸기', '신규 입사자', '보안 인쇄']
질문: 로그인을 여러 번 실패해서 들어갈 수가 없어요.
  키워드: ['화상회의', '신규 입사자', '원격 지원']
  의미  : ['계정 잠김', '신규 입사자', '원격 지원']
질문: 예전에 쓰던 암호를 다시 써도 되나요?
  키워드: ['비밀번호 바꾸기', 'IT지원센터 운영', '유료 소프트웨어']
  의미  : ['신규 입사자', '유료 소프트웨어', '원격 지원']

이 실습이 뜻하는 것

이 실습은 회사 안내 게시판의 글을 제목별로 떼어 지도에 미리 꽂아 두고, 질문이 오면 그 질문의 위치에서 가장 가까운 글 셋을 집어 주는 것과 같습니다. 안내문을 제목마다 나눠 모두 지도에 꽂고(1단계), 질문의 위치를 찾아 가까운 글 셋을 고르는 함수를 만들고(2단계), 같은 질문을 6장의 글자 색인 검색에도 물어 견줍니다(3단계). 여기서 게시판 글이 it_helpdesk.md 의 조각 30개, 미리 꽂아 둔 위치가 임베딩 E, 가까운 글 셋을 고르는 함수가 semantic_search(), 글자 색인이 TF-IDF(Term Frequency-Inverse Document Frequency, 단어 빈도-역문서 빈도) 키워드 검색입니다.

단계출력뜻
1. 조각조각 수: 30안내문의 주제 30개가 조각 30개가 되었습니다. 예로 "계정 잠김" 조각에는 5번 틀리면 30분 잠긴다는 안내가 들어 있습니다
1. 임베딩(30, 384) · 0.4초30조각을 숫자 384개씩으로 바꾸는 데 CPU(Central Processing Unit, 중앙 처리 장치)로 0.4초 걸렸습니다. 한 번 만들어 두면 다시 만들 필요가 없습니다
2. 검색0.867 계정 잠김, 0.864 신규 입사자, 0.851 원격 지원로그인 실패 질문에 계정 잠김이 1등입니다. 2등과 차이가 0.003 뿐인 아슬아슬한 1등입니다
3. 암호 주기키워드 1등 모니터 추가, 의미 1등 비밀번호 바꾸기글자가 겹치지 않는 질문에서 키워드 검색은 엉뚱한 조각을, 의미 검색은 맞는 조각을 1등에 올렸습니다
3. 로그인 실패키워드 1등 화상회의, 의미 1등 계정 잠김같은 꼴입니다. 의미 검색이 뜻으로 찾았습니다
3. 예전 암호키워드 1등 비밀번호 바꾸기, 의미는 위 3개에 없음이번에는 거꾸로 키워드 검색만 맞혔습니다

한 줄로 줄이면, 의미 검색은 낱말이 다른 질문을 잘 찾지만 늘 이기지는 않는다는 것을 질문 셋으로 보인 실습입니다. 실습 3은 이 임베딩을 벡터DB(벡터 데이터베이스, 임베딩을 모아 두고 가까운 것을 찾아 주는 저장소)에 넣고, 실습 4는 질문 36개로 두 검색의 성적을 숫자로 잽니다. 질문 셋은 고른 예시라, 어느 검색이 더 나은지는 이 실습만으로 말할 수 없습니다.

코드 설명

  • 조각 임베딩 E 는 한 번만 만들고, 질문이 올 때마다 질문 하나만 임베딩해 E @ q 로 견줍니다. 본문 "의미 검색"의 그림에서 1, 2번과 3~5번을 나눈 까닭입니다.
  • 조각은 30개이고, 30개를 모두 임베딩하는 데 CPU 로 0.4초 걸렸습니다. 문서가 바뀌지 않는 한 이 임베딩은 다시 만들 필요가 없습니다.
  • "로그인을 여러 번 실패해서 들어갈 수가 없어요."의 1등은 계정 잠김(0.867)입니다. 그런데 2등 신규 입사자(0.864)와 차이가 0.003 뿐이라, 순서가 쉽게 뒤바뀔 수 있는 아슬아슬한 1등입니다.
  • 앞의 두 질문은 키워드 검색이 "모니터 추가", "화상회의"처럼 엉뚱한 조각을 1등에 올리고, 의미 검색은 비밀번호 바꾸기와 계정 잠김을 바로 찾았습니다. 셋째 질문(예전에 쓰던 암호)은 거꾸로, 키워드 검색이 "비밀번호 바꾸기"를 1등에 올리고 의미 검색은 위 3개 안에 넣지 못했습니다. 의미 검색이 늘 이기는 것은 아니라서, 실습 4에서 질문 36개로 잽니다.
← 이전 절7장 · 실습 1 — 문장을 임베딩으로 바꾸기다음 절 →7장 · 실습 3 — 벡터DB 에 넣고 찾기