검색 적합성(Retrieval Relevance) 측정하기
검색 적합성(Retrieval Relevance) 측정하기 (retrieval-relevance)
| 시간: 40분 | 난이도: 중급 |
|---|
검색 시스템을 만들다 보면 "결과가 진짜 사용자 의도와 잘 맞는가?"라는 질문에 부딪혀요. 이 튜토리얼이 다루는 **검색 적합성(retrieval relevance)**이 바로 그 질문에 대한 답이에요. 검색 적합성을 측정하려면, 쿼리와 그에 기대되는 관련 문서를 짝지어 놓은 레이블 데이터셋(흔히 golden query set 또는 ground truth라 불러요)이 필요해요. 여기서는 그 데이터셋을 만들고, Qdrant에 돌려서 적합성 지표를 계산하는 방법까지 전부 다뤄요.
검색 평가의 다른 축을 다루는 관련 튜토리얼도 함께 알아두면 좋아요. ANN 재현율(ANN Recall) 측정은 근사 인덱스가 정확한 kNN과 어느 정도 일치하는지를 보고, 파이프라인 출력 품질 평가는 엔드투엔드 파이프라인이 올바른 출력을 내는지를 봐요.
사전 준비사항. 문서를 포인트(벡터 + 선택적 페이로드)로 채워 둔 Qdrant 컬렉션, 평가 시점에 쿼리를 인코딩할 수 있는 임베딩 모델, 그리고 ranx가 설치된 Python 환경이 필요해요.
Golden Set 만드는 세 가지 방법
Golden set을 만드는 데는 실용적인 접근 3가지가 있어요. 각각 품질과 비용, 규모를 맞바꾸는(trade-off) 구조예요.
1. 사람이 직접 레이블링 (Human Annotation)
도메인 전문가가 이진(관련/비관련) 또는 등급(0/1/2 또는 1–5) 척도로 적합성 점수를 매겨요. 사람이 레이블한 데이터는 신뢰도가 가장 높은 신호를 주고, 등급형 레이블(graded labels)의 1차 공급원이에요. 다만 전문가의 시간이 병목이 되기 때문에, 보통 소수의 가치 높은 쿼리에만 적용하게 돼요.
2. 로그에서 추출한 실제 사용자 쿼리 (Real User Queries from Logs)
프로덕션 로그에서 쿼리-문서 쌍을 샘플링하고, 클릭이나 명시적 피드백(좋아요/싫어요, 평점)을 적합성 신호로 사용해요. 실제 사용자 쿼리는 합성 생성이 따라잡을 수 없는 의도와 어휘를 담고 있어요. 다만 충분한 트래픽과 적합성으로 해석 가능한 신호가 필요해요.
샘플을 만들 땐 빈번한 쿼리가 희소한 쿼리를 밀어내지 않도록 균형을 맞춰야 해요. 쿼리 유형, 주제, 의도 클래스별로 그룹핑하세요. 처음엔 큰 지표 차이를 탐지할 수 있도록 수백 개의 레이블 쌍으로 시작하는 게 좋고, 구간별(slice) 분석이나 작은 랭킹 차이를 보려면 훨씬 더 많은 데이터가 필요해요.
3. LLM 기반 합성 생성 (LLM-Based Synthetic Generation)
LLM으로 말뭉치에서 샘플링한 문서에 대해 그럴듯한 쿼리를 생성할 수 있어요. 이 방식은 수천 개의 쌍을 저렴하게 확장할 수 있지만, 합성 쿼리는 실제 사용자 쿼리보다 검색이 쉬운 경향이 있어서 오프라인 점수를 부풀려요. 매우 큰 말뭉치라면 로그 기반 샘플링이 더 실용적인 경우가 많아요.
LLM에 넣는 문서(즉 소스 문서 source document)는 그것이 생성한 모든 쿼리의 적합성 레이블이 돼요:
You are helping build an evaluation dataset for a search system. Generate 3 realistic search queries for the document below. Each query should be what a real user would type to find it. Phrase queries naturally, not as paraphrases of the document. Return exactly 3 lines, one query per line. No numbering, no bullets, no preamble. Example: how does X work best way to configure Y what is Z used for Document: {document_text}
프롬프트를 말뭉치에 맞게 조정하세요:
- 쿼리 스타일 — FAQ/RAG에는 질문형, e-커머스에는 키워드 구절, 코드 검색에는 의도 구절, 전문 도메인에는 기술 용어.
- 문서당 개수 —
3이 기본값. 문서 길이와 golden set 크기에 맞춰 조정. - 페르소나 — 일반적인 "사용자"가 광범위하게 잘 동작하지만, 의료·법률·기술 같은 전문 말뭉치라면 특화 페르소나가 더 유리해요.
- 언어 — 기본은 영어. 다국어 필요 시 명시적으로 지정하세요.
Golden Set 사용하기
ranx는 랭킹 지표 평가를 위한 Python 라이브러리예요. 표준 랭킹 지표(recall@k, MRR, NDCG@k, Precision@k, MAP 등)를 하나의 일관된 인터페이스로 지원해서, 지표마다 직접 구현하거나 여러 라이브러리를 번갈아 쓰지 않아도 돼요.
평가는 세 단계로 진행돼요: 레이블된 쿼리를 ranx가 기대하는 형태로 불러오고, 각각을 Qdrant에 돌린 다음, 지표를 계산해요.
1. 로드하고 조립하기. 레이블된 각 쿼리에 대해 query_id, query_text, labels로 항목을 만드세요:
{ "query_id" : "q1" , "query_text" : "how does X work" , "labels" : { "doc_42" : 1 }, # 합성 쿼리의 소스 문서, 아니면 관련 문서 }
생성 파이프라인이 만든 출력을 이 형태로 정규화한 뒤 반복문으로 전체 golden_set을 만드세요:
# 생성 파이프라인 출력을 이 형태로 정규화:
# - 합성: 생성된 쿼리당 항목 하나, labels = {source_doc_id: 1}
# - 로그: 쿼리-클릭 쌍당 항목 하나, labels = {clicked_doc_id: 1}
# - 사람: 주석된 쿼리당 항목 하나, labels = {doc_id: score, ...}
labeled_data = [
{ "query_text" : "how does X work" , "labels" : { "doc_42" : 1 }},
{ "query_text" : "what is Y used for" , "labels" : { "doc_55" : 1 , "doc_88" : 1 }},
# ...레이블된 쿼리마다 항목 하나
]
golden_set = []
for i , item in enumerate ( labeled_data ):
golden_set . append ({
"query_id" : f "q { i } " ,
"query_text" : item [ "query_text" ],
"labels" : item [ "labels" ],
})
2. Qrels와 Run 만들기. ranx는 두 입력({query_id: {doc_id: score}} 형태)을 비교해요:
Qrels(쿼리 적합성 판정) — 레이블된 ground truth. 이진 레이블은1, 등급 레이블은 원래0/1/2값을 사용.Run(검색 출력) — 각 쿼리에 대해 Qdrant가 반환한 결과와 유사도 점수.
from qdrant_client import QdrantClient
from ranx import Qrels , Run , evaluate
from your_embedding_model import embed # Qdrant 컬렉션이 쓰는 모델과 일치해야 함
client = QdrantClient ( "http://localhost:6333" )
# Qdrant Cloud라면: QdrantClient(url="https://<id>.cloud.qdrant.io", api_key="...")
def retrieval_run ( golden_set : list , collection : str , k : int = 10 ) -> Run :
run = {}
for entry in golden_set :
results = client . query_points (
collection_name = collection ,
query = embed ( entry [ "query_text" ]),
limit = k ,
) . points
# p.id 타입은 labels의 doc_id 타입과 일치해야 함 (ranx는 동등성으로 매칭)
run [ entry [ "query_id" ]] = { p . id : p . score for p in results }
return Run ( run )
qrels = Qrels ({ entry [ "query_id" ]: entry [ "labels" ] for entry in golden_set })
run = retrieval_run ( golden_set , collection = "my_collection" , k = 10 )
3. 지표 계산하기. evaluate(qrels, run, [...])가 두 입력을 비교해서 지표 이름→float 딕셔너리를 반환해요.
metrics = evaluate ( qrels , run , [ "recall@10" , "mrr" , "ndcg@10" ])
evaluate()가 반환하는 예:
{ "recall@10" : 0.82 , "mrr" : 0.71 , "ndcg@10" : 0.76 }
세 지표 모두 높을수록 좋아요.
올바른 지표 고르기
어떤 지표가 중요한지는 파이프라인이 결과를 어떻게 사용하는지에 달려 있어요:
| 시나리오 | 권장 지표 | 이유 |
|---|---|---|
| RAG 파이프라인 (LLM이 top-k 청크를 읽음) | Recall@k |
관련 문서가 3번째냐 1번째냐는 LLM이 복구할 수 있지만, 아예 빠지면 타격이 큼 |
| 단일 답변 검색 (FAQ나 Q&A) | MRR 또는 Hits@1 |
사용자가 행동하는 건 첫 번째 결과. 낮은 랭크는 중요도가 낮음 |
| 재랭킹 또는 추천 피드 | NDCG@k |
결과 목록 안의 순서가 중요. 매우 관련 높은 문서가 5번째에 있으면 1번째보다 나쁨 |
NDCG(Normalized Discounted Cumulative Gain)는 등급 레이블(예: 쿼리-문서 쌍당 0/1/2 점수)이 필요해요. 이진 레이블이라면 recall@k와 MRR(Mean Reciprocal Rank)를 쓰세요. 전체 지표 목록(Precision@k, MAP, ERR 등)은 ranx 문서를 참고해요.
k를 고를 땐 실제 사용과 맞추세요. 앱이 사용자에게 5개 결과를 보여준다면 @5를 측정하고, RAG 파이프라인이 LLM에 10개 청크를 넘긴다면 @10을 측정해요. 5개만 보여주는 UI에 @100을 보고하면 지표가 인위적으로 좋아 보여요.
CI에서 재실행하기
검색 스택이 바뀔 때마다 다시 실행하세요: 새 임베딩 모델(쿼리 재임베딩과 재인덱싱도 필요), 새 인덱스 설정, 새 재랭커. CI에서는 고정된 golden set에 대해 recall@10을 계산하고, 점수가 목표 임계값 아래로 떨어지면 잡(Job)을 실패 처리해요.
주의할 함정들 (Pitfalls to Watch For)
Golden set에서 데이터 누출(data leakage) 이란 오프라인 지표가 프로덕션 현실보다 좋아 보이게 만드는 어떤 설정이든 말해요. 고전적인 train/test 누출과 달리, 문제는 종종 평가 설계에 있어요. 소스 문서는 인덱스에 남겨 두세요(그것들이 기대되는 관련 답이니까). 다음 위험들에 집중하세요:
- 합성 쿼리의 비현실성 — LLM이 원문 표현을 그대로 반영해 실제 사용자 입력보다 쉬운 쿼리를 만들어 오프라인 점수를 부풀려요. 소스 문서를 보지 못한 사용자처럼 쿼리를 생성하도록 LLM에 지시하고, 합성 쿼리와 실제 쿼리의 분포(길이·구체성)를 비교해서 완화하세요.
- 임베딩 모델 오염(contamination) — 임베딩 모델이 golden set과 겹치는 쌍으로 학습됐다면, 결과가 진짜 일반화 능력보다 좋게 보여요. 호스팅 모델은 가능하면 공개된 학습 데이터를 검토하고, 자체 파인튜닝은 train/eval 분리를 엄격히 유지하세요.
- 거의 중복인 문서(near-duplicate) — 검색이 레이블 집합에 없는 레이블 문서의 거의-중복을 반환할 수 있어요. 그러면 지표가 나빠 보여요. 레이블이 불완전하기 때문이지 검색이 실패해서가 아니에요. 점수 하락이 보이면 검색을 튜닝하기 전에 레이블을 감사하라는 신호예요. 레이블링 전에 중복을 제거(예: 코사인 유사도 > 0.95)하거나 중복 클러스터를 함께 레이블하세요.
- 시간 경과 드리프트(temporal drift) — 레이블링 후 말뭉치가 바뀌면 레이블이 낡아요. 참조 문서가 삭제되거나 새 버전으로 대체될 수 있어요. 실행마다 말뭉치 스냅샷을 고정하고, 말뭉치에 실질적 변화가 생기면 golden set을 재생성하세요.
- 재현성(setup reproducibility) — 말뭉치 스냅샷, 레이블 생성 방식, 전처리 임계값까지 전체 평가 설정을 버전 관리하세요. 그래야 나중 점수 하락이 모델/인덱스 회귀인지 데이터셋 드리프트인지 구분할 수 있어요.
다음 단계
검색 적합성이 목표치에 도달하면, 다음 레이어는 파이프라인 출력 품질이에요. 검색 결과가 소비자(LLM 생성기, 랭커, UI)로 흘러 들어갈 때 전체 파이프라인이 올바른 출력을 내는지 보는 거예요. 파이프라인 출력 품질 평가를 참고하세요.