본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 3장: 검색 품질 메트릭과 평가
2026년 2월 7일·AI / ML·

3장: 검색 품질 메트릭과 평가

Precision, Recall, NDCG, MRR, MAP 등 검색 품질 메트릭의 원리와 계산법, 오프라인/온라인 평가 방법론, A/B 테스트와 평가 데이터셋 구축을 다룹니다.

15분722자7개 섹션
searchai
공유
ai-search3 / 11
1234567891011
이전2장: 시맨틱 검색 아키텍처다음4장: 쿼리 이해와 확장

학습 목표

  • 주요 검색 품질 메트릭(Precision, Recall, NDCG, MRR, MAP)의 원리와 계산법을 이해합니다.
  • 오프라인 평가와 온라인 평가의 차이와 적용 시점을 파악합니다.
  • A/B 테스트와 인터리빙 기법의 장단점을 비교합니다.
  • 평가 데이터셋을 구축하는 실전 방법을 학습합니다.

왜 검색 품질을 측정해야 하는가

검색 시스템을 개선하려면 현재 품질을 객관적으로 측정할 수 있어야 합니다. "검색 결과가 좋아 보인다"는 주관적 판단으로는 임베딩 모델 교체, 리랭킹 도입, 파라미터 조정 등의 변경이 실제로 개선을 가져왔는지 확인할 수 없습니다.

검색 품질 메트릭은 크게 두 가지 관점에서 측정됩니다.

  • 관련성(Relevance): 반환된 결과가 사용자 의도에 얼마나 부합하는가
  • 순위 품질(Ranking Quality): 관련 문서가 상위에 얼마나 잘 배치되어 있는가

핵심 메트릭 상세 해설

Precision at K (P@K)

상위 K개 결과 중 관련 문서의 비율입니다. 사용자가 실제로 보는 결과의 품질을 직접 반영합니다.

precision_at_k.py
python
def precision_at_k(retrieved: list[int], relevant: set[int], k: int) -> float:
    """
    P@K 계산
    retrieved: 검색 결과 문서 ID 리스트 (순서대로)
    relevant: 관련 문서 ID 집합
    k: 상위 K개
    """
    top_k = retrieved[:k]
    relevant_in_top_k = sum(1 for doc_id in top_k if doc_id in relevant)
    return relevant_in_top_k / k
 
# 예시: 상위 5개 중 3개가 관련 문서
retrieved = [101, 205, 303, 404, 102]
relevant = {101, 102, 303, 507}
print(f"P@5 = {precision_at_k(retrieved, relevant, 5)}")  # 0.6

Recall at K (R@K)

전체 관련 문서 중 상위 K개에 포함된 비율입니다. 관련 문서를 얼마나 빠짐없이 찾아내는지를 측정합니다.

recall_at_k.py
python
def recall_at_k(retrieved: list[int], relevant: set[int], k: int) -> float:
    """R@K 계산"""
    top_k = retrieved[:k]
    relevant_in_top_k = sum(1 for doc_id in top_k if doc_id in relevant)
    return relevant_in_top_k / len(relevant) if relevant else 0.0
 
# 전체 관련 문서 4개 중 3개를 상위 5개에서 찾음
print(f"R@5 = {recall_at_k(retrieved, relevant, 5)}")  # 0.75
Info

Precision과 Recall은 트레이드오프 관계입니다. K를 늘리면 Recall은 오르지만 Precision은 떨어질 수 있습니다. 실제 서비스에서는 P@5나 P@10처럼 사용자가 실제로 보는 범위에 맞춰 K를 설정합니다.

MRR (Mean Reciprocal Rank)

첫 번째 관련 문서의 순위 역수를 평균한 값입니다. "원하는 결과를 얼마나 빨리 찾을 수 있는가"를 측정합니다.

mrr.py
python
def reciprocal_rank(retrieved: list[int], relevant: set[int]) -> float:
    """단일 쿼리의 Reciprocal Rank"""
    for i, doc_id in enumerate(retrieved):
        if doc_id in relevant:
            return 1.0 / (i + 1)
    return 0.0
 
def mean_reciprocal_rank(queries_results: list[tuple[list[int], set[int]]]) -> float:
    """MRR: 여러 쿼리의 RR 평균"""
    rr_scores = [reciprocal_rank(ret, rel) for ret, rel in queries_results]
    return sum(rr_scores) / len(rr_scores)
 
# 예시: 3개 쿼리
queries = [
    ([10, 20, 30], {20}),    # 첫 관련 문서가 2위 -> RR = 0.5
    ([40, 50, 60], {40}),    # 첫 관련 문서가 1위 -> RR = 1.0
    ([70, 80, 90], {90}),    # 첫 관련 문서가 3위 -> RR = 0.333
]
print(f"MRR = {mean_reciprocal_rank(queries):.4f}")  # 0.6111

MAP (Mean Average Precision)

각 관련 문서가 발견되는 위치에서의 Precision을 평균한 값입니다. 관련 문서의 순위 분포를 종합적으로 반영합니다.

map_score.py
python
def average_precision(retrieved: list[int], relevant: set[int]) -> float:
    """단일 쿼리의 Average Precision"""
    hits = 0
    sum_precision = 0.0
 
    for i, doc_id in enumerate(retrieved):
        if doc_id in relevant:
            hits += 1
            sum_precision += hits / (i + 1)
 
    return sum_precision / len(relevant) if relevant else 0.0
 
def mean_average_precision(queries_results: list[tuple[list[int], set[int]]]) -> float:
    """MAP: 여러 쿼리의 AP 평균"""
    ap_scores = [average_precision(ret, rel) for ret, rel in queries_results]
    return sum(ap_scores) / len(ap_scores)

NDCG (Normalized Discounted Cumulative Gain)

**NDCG(Normalized Discounted Cumulative Gain)**는 가장 널리 사용되는 검색 품질 메트릭입니다. 관련성 판단이 이진(관련/비관련)이 아닌 다단계(0, 1, 2, 3...)일 때 특히 유용합니다.

ndcg.py
python
import numpy as np
 
def dcg_at_k(relevance_scores: list[float], k: int) -> float:
    """DCG@K: 순위에 따른 감가 누적 이득"""
    relevance = np.array(relevance_scores[:k])
    positions = np.arange(1, len(relevance) + 1)
    return np.sum(relevance / np.log2(positions + 1))
 
def ndcg_at_k(relevance_scores: list[float], k: int) -> float:
    """NDCG@K: 이상적 순서 대비 정규화된 점수"""
    dcg = dcg_at_k(relevance_scores, k)
    ideal_scores = sorted(relevance_scores, reverse=True)
    idcg = dcg_at_k(ideal_scores, k)
    return dcg / idcg if idcg > 0 else 0.0
 
# 예시: 관련성 점수 (3=매우 관련, 2=관련, 1=약간 관련, 0=무관)
scores = [3, 0, 2, 1, 0]
print(f"NDCG@5 = {ndcg_at_k(scores, 5):.4f}")  # 0.8542

NDCG의 핵심 아이디어는 두 가지입니다.

  1. 할인(Discounting): 하위 순위일수록 기여도가 감소합니다. log2(position + 1)로 나누어 상위 순위에 가중치를 부여합니다.
  2. 정규화(Normalization): 이상적인 순서(가장 관련 있는 문서부터 정렬)의 DCG로 나누어 0과 1 사이의 값으로 정규화합니다.

메트릭 비교 요약

메트릭관련성 유형순위 고려주요 관점
P@K이진부분적 (K 범위)상위 결과 정밀도
R@K이진부분적관련 문서 커버리지
MRR이진첫 관련 결과 위치빠른 답변 찾기
MAP이진전체 순위종합적 순위 품질
NDCG@K다단계전체 순위등급별 관련성 반영

오프라인 평가

**오프라인 평가(Offline Evaluation)**는 미리 구축한 평가 데이터셋을 사용하여 검색 시스템의 품질을 측정하는 방식입니다. 시스템 변경 전후의 메트릭을 비교하여 개선 효과를 확인합니다.

평가 데이터셋 구축

평가 데이터셋은 쿼리-문서 쌍에 관련성 레이블을 부여한 것입니다.

evaluation_dataset.py
python
from dataclasses import dataclass
 
@dataclass
class RelevanceJudgment:
    query_id: str
    query_text: str
    doc_id: str
    relevance: int  # 0: 무관, 1: 약간 관련, 2: 관련, 3: 매우 관련
 
# 평가 데이터셋 예시
judgments = [
    RelevanceJudgment("q1", "파이썬 웹 프레임워크", "d101", 3),
    RelevanceJudgment("q1", "파이썬 웹 프레임워크", "d102", 2),
    RelevanceJudgment("q1", "파이썬 웹 프레임워크", "d103", 0),
    RelevanceJudgment("q2", "데이터베이스 인덱스", "d201", 3),
    RelevanceJudgment("q2", "데이터베이스 인덱스", "d202", 1),
]
Warning

평가 데이터셋 구축은 비용이 많이 드는 작업입니다. 최소 50-100개의 쿼리와 쿼리당 10-30개의 문서에 대한 관련성 레이블이 필요합니다. 레이블러 간의 일관성을 위해 명확한 가이드라인과 복수 레이블러의 합의가 중요합니다.

오프라인 평가 자동화

offline_evaluation.py
python
def evaluate_search_system(search_fn, judgments: list[RelevanceJudgment], k: int = 10):
    """검색 시스템 오프라인 평가"""
    queries = {}
    for j in judgments:
        if j.query_id not in queries:
            queries[j.query_id] = {"text": j.query_text, "relevance": {}}
        queries[j.query_id]["relevance"][j.doc_id] = j.relevance
 
    ndcg_scores = []
    for query_id, query_data in queries.items():
        results = search_fn(query_data["text"], k=k)
        relevance_scores = [
            query_data["relevance"].get(doc_id, 0) for doc_id in results
        ]
        ndcg_scores.append(ndcg_at_k(relevance_scores, k))
 
    return {
        "mean_ndcg": sum(ndcg_scores) / len(ndcg_scores),
        "per_query_ndcg": dict(zip(queries.keys(), ndcg_scores)),
    }

온라인 평가

오프라인 평가만으로는 실제 사용자 경험을 완전히 반영하기 어렵습니다. **온라인 평가(Online Evaluation)**는 실제 트래픽을 활용하여 검색 품질을 측정합니다.

A/B 테스트

두 가지 검색 시스템(대조군 A와 실험군 B)에 트래픽을 무작위로 분배하고, 사용자 행동 지표를 비교합니다.

온라인 메트릭으로 주로 사용되는 지표는 다음과 같습니다.

  • CTR(Click-Through Rate): 검색 결과 클릭률
  • 평균 클릭 순위: 사용자가 클릭하는 결과의 평균 순위 (낮을수록 좋음)
  • 제로 결과율: 결과가 0개인 쿼리의 비율 (낮을수록 좋음)
  • 세션 성공률: 검색 후 원하는 행동(구매, 조회 등)을 완료한 비율
  • 이탈률: 검색 결과를 보고 즉시 떠나는 비율

인터리빙(Interleaving)

A/B 테스트의 대안으로, 두 시스템의 결과를 하나의 목록에 섞어서 보여주는 방법입니다. 같은 사용자가 두 시스템의 결과를 동시에 평가하므로, A/B 테스트보다 적은 트래픽으로 빠르게 우열을 판단할 수 있습니다.

team_draft_interleaving.py
python
def team_draft_interleave(results_a: list, results_b: list, k: int) -> tuple[list, dict]:
    """팀 드래프트 인터리빙"""
    interleaved = []
    team_a = set()
    team_b = set()
    idx_a, idx_b = 0, 0
 
    while len(interleaved) < k:
        if len(team_a) <= len(team_b):
            # A에서 선택
            while idx_a < len(results_a) and results_a[idx_a] in interleaved:
                idx_a += 1
            if idx_a < len(results_a):
                interleaved.append(results_a[idx_a])
                team_a.add(results_a[idx_a])
                idx_a += 1
        else:
            # B에서 선택
            while idx_b < len(results_b) and results_b[idx_b] in interleaved:
                idx_b += 1
            if idx_b < len(results_b):
                interleaved.append(results_b[idx_b])
                team_b.add(results_b[idx_b])
                idx_b += 1
 
    return interleaved, {"team_a": team_a, "team_b": team_b}
Tip

인터리빙은 A/B 테스트 대비 약 10배 적은 트래픽으로 동일한 통계적 검정력을 달성할 수 있다고 알려져 있습니다. 트래픽이 제한된 서비스에서 특히 유용합니다.


클릭 로그 분석

실제 서비스에서는 사용자 클릭 로그가 검색 품질의 중요한 신호입니다. 다만 클릭 데이터에는 여러 편향이 존재하므로 주의가 필요합니다.

위치 편향(Position Bias): 상위 결과가 단순히 위치 때문에 더 많이 클릭됩니다. 관련성과 무관하게 1위 결과의 CTR이 가장 높습니다.

신뢰 편향(Trust Bias): 사용자가 검색 엔진을 신뢰하여, 상위 결과가 관련 있을 것이라고 가정하고 클릭합니다.

이러한 편향을 보정하기 위해 IPW(Inverse Propensity Weighting), 위치 모델(Position Model) 등의 기법을 적용합니다.


정리

이번 장에서는 검색 품질을 객관적으로 측정하는 메트릭과 평가 방법론을 다루었습니다. P@K, R@K, MRR, MAP, NDCG 각각의 특성과 계산법을 이해했고, 오프라인 평가(레이블 데이터셋 기반)와 온라인 평가(A/B 테스트, 인터리빙)의 차이와 적용 시점을 학습했습니다. 클릭 로그의 편향 문제도 확인했습니다.

다음 장에서는 검색의 시작점인 사용자 쿼리를 깊이 있게 다룹니다. 쿼리 분류, 의도 인식, 동의어 확장부터 LLM 기반 쿼리 확장, HyDE(가상 문서 생성)까지, 쿼리 이해와 확장 기법을 살펴보겠습니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#search#ai

관련 글

AI / ML

4장: 쿼리 이해와 확장

쿼리 분류, 의도 인식, 엔티티 인식부터 LLM 기반 쿼리 확장, HyDE(가상 문서 생성), 다국어 처리까지 쿼리 이해 파이프라인을 다룹니다.

2026년 2월 9일·16분
AI / ML

2장: 시맨틱 검색 아키텍처

Bi-encoder 기반 시맨틱 검색의 작동 원리, 임베딩 모델 선택, 문서 청킹 전략, ANN 검색, 벡터 데이터베이스 연동을 Python 구현과 함께 다룹니다.

2026년 2월 5일·13분
AI / ML

5장: 랭킹 모델 — Bi-encoder와 Cross-encoder

Bi-encoder와 Cross-encoder의 구조적 차이, Elastic Rerank의 DeBERTa v3 모델, 점수 퓨전(RRF), 학습 랭킹(LTR)을 심층적으로 다룹니다.

2026년 2월 11일·14분
이전 글2장: 시맨틱 검색 아키텍처
다음 글4장: 쿼리 이해와 확장

댓글

목차

약 15분 남음
  • 학습 목표
  • 왜 검색 품질을 측정해야 하는가
  • 핵심 메트릭 상세 해설
    • Precision at K (P@K)
    • Recall at K (R@K)
    • MRR (Mean Reciprocal Rank)
    • MAP (Mean Average Precision)
    • NDCG (Normalized Discounted Cumulative Gain)
    • 메트릭 비교 요약
  • 오프라인 평가
    • 평가 데이터셋 구축
    • 오프라인 평가 자동화
  • 온라인 평가
    • A/B 테스트
    • 인터리빙(Interleaving)
  • 클릭 로그 분석
  • 정리