rag 검색에서 의미적으로 비슷한 문서를 찾는 것만으로는 충분하지 않다. “로그인 실패”와 “OAuth 토큰 갱신 오류”를 연결하는 것은 임베딩(embedding)의 역할이지만, 해당 티켓이 현재 열려 있는지, 어느 팀 소유인지, 최신 기간에 해당하는지는 메타데이터가 판단해야 한다. 이 글은 로컬 Python만으로 두 신호를 결합하는 최소 구현을 구성한다.
준비
pip install sentence-transformers numpyall-MiniLM-L6-v2는 문장을 384차원 벡터로 변환하며 CPU에서도 실행할 수 있다. 문서 임베딩을 정규화하면 코사인 유사도(cosine similarity)는 내적 한 번으로 계산된다.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(
[ticket["text"] for ticket in tickets],
normalize_embeddings=True,
)검색은 필터 뒤에 점수를 계산한다
메타데이터 조건에 맞지 않는 문서를 먼저 제외하면 결과 정확도와 계산 비용을 동시에 개선할 수 있다.
import numpy as np
def search(query, team=None, status=None, top_k=5):
q_vec = model.encode([query], normalize_embeddings=True)[0]
mask = np.ones(len(tickets), dtype=bool)
for i, doc in enumerate(tickets):
if team and doc["team"] != team:
mask[i] = False
if status and doc["status"] != status:
mask[i] = False
candidates = np.where(mask)[0]
scores = embeddings[candidates] @ q_vec
order = np.argsort(scores)[::-1][:top_k]
return [(tickets[candidates[i]], float(scores[i])) for i in order]필터로 저장해야 할 값
| 메타데이터 | 유용한 질의 |
|---|---|
team, product | 특정 담당 영역의 장애만 찾기 |
status, priority | 열린 고우선순위 이슈만 검색 |
created, updated | 최근 정책이나 장애로 범위 제한 |
tenant, acl | 사용자가 볼 수 있는 문서만 검색 |
권한 필터는 검색 결과를 만든 뒤 숨기는 것이 아니라, 후보 집합을 만들 때부터 적용해야 정보 유출을 막을 수 있다.
임베딩을 재계산하지 않는다
문서 임베딩은 색인 시 한 번 계산해 저장하고, 요청 시에는 질의 벡터만 생성한다.
import json
import numpy as np
np.save("ticket_embeddings.npy", embeddings)
with open("ticket_metadata.json", "w") as f:
json.dump(tickets, f)프로덕션에서는 이 최소 구현을 벡터 데이터베이스, 하이브리드 검색, 리랭커, 인덱스 버전 관리로 확장한다. 문서 수가 커지는 시점의 설계는 rag-tips-scale-million-documents와 rag-tips-production을 함께 참고한다.
참고 자료
- Building Context-Aware Search in Python with LLM Embeddings + Metadata – Machine Learning Mastery (2026-05-22)